001    /*
002     *  Licensed to the Apache Software Foundation (ASF) under one
003     *  or more contributor license agreements.  See the NOTICE file
004     *  distributed with this work for additional information
005     *  regarding copyright ownership.  The ASF licenses this file
006     *  to you under the Apache License, Version 2.0 (the
007     *  "License"); you may not use this file except in compliance
008     *  with the License.  You may obtain a copy of the License at
009     *  
010     *    http://www.apache.org/licenses/LICENSE-2.0
011     *  
012     *  Unless required by applicable law or agreed to in writing,
013     *  software distributed under the License is distributed on an
014     *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015     *  KIND, either express or implied.  See the License for the
016     *  specific language governing permissions and limitations
017     *  under the License. 
018     *  
019     */
020    package org.apache.directory.server.core.authz;
021    
022    
023    import java.util.HashMap;
024    import java.util.HashSet;
025    import java.util.List;
026    import java.util.Map;
027    import java.util.Set;
028    
029    import javax.naming.directory.SearchControls;
030    
031    import org.apache.directory.server.constants.ServerDNConstants;
032    import org.apache.directory.server.core.CoreSession;
033    import org.apache.directory.server.core.filtering.EntryFilteringCursor;
034    import org.apache.directory.server.core.interceptor.context.SearchOperationContext;
035    import org.apache.directory.server.core.partition.PartitionNexus;
036    import org.apache.directory.server.i18n.I18n;
037    import org.apache.directory.shared.ldap.constants.SchemaConstants;
038    import org.apache.directory.shared.ldap.entry.StringValue;
039    import org.apache.directory.shared.ldap.entry.EntryAttribute;
040    import org.apache.directory.shared.ldap.entry.Modification;
041    import org.apache.directory.shared.ldap.entry.ModificationOperation;
042    import org.apache.directory.shared.ldap.entry.ServerEntry;
043    import org.apache.directory.shared.ldap.entry.Value;
044    import org.apache.directory.shared.ldap.exception.LdapException;
045    import org.apache.directory.shared.ldap.filter.BranchNode;
046    import org.apache.directory.shared.ldap.filter.EqualityNode;
047    import org.apache.directory.shared.ldap.filter.OrNode;
048    import org.apache.directory.shared.ldap.message.AliasDerefMode;
049    import org.apache.directory.shared.ldap.name.DN;
050    import org.apache.directory.shared.ldap.schema.AttributeType;
051    import org.apache.directory.shared.ldap.schema.SchemaManager;
052    import org.apache.directory.shared.ldap.schema.normalizers.OidNormalizer;
053    import org.slf4j.Logger;
054    import org.slf4j.LoggerFactory;
055    
056    
057    /**
058     * A cache for tracking static group membership.
059     *
060     * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
061     * @version $Rev: 928945 $
062     */
063    public class GroupCache
064    {
065        /** the logger for this class */
066        private static final Logger LOG = LoggerFactory.getLogger( GroupCache.class );
067    
068        /** Speedup for logs */
069        private static final boolean IS_DEBUG = LOG.isDebugEnabled();
070    
071        /** String key for the DN of a group to a Set (HashSet) for the Strings of member DNs */
072        private final Map<String, Set<String>> groups = new HashMap<String, Set<String>>();
073    
074        /** a handle on the partition nexus */
075        private final PartitionNexus nexus;
076    
077        /** A storage for the member attributeType */
078        private AttributeType memberAT;
079    
080        /** A storage for the uniqueMember attributeType */
081        private AttributeType uniqueMemberAT;
082    
083        /**
084         * The OIDs normalizer map
085         */
086        private Map<String, OidNormalizer> normalizerMap;
087    
088        /** the normalized dn of the administrators group */
089        private DN administratorsGroupDn;
090    
091        private static final Set<DN> EMPTY_GROUPS = new HashSet<DN>();
092    
093    
094        /**
095         * Creates a static group cache.
096         *
097         * @param directoryService the directory service core
098         * @throws LdapException if there are failures on initialization 
099         */
100        public GroupCache( CoreSession session ) throws Exception
101        {
102            SchemaManager schemaManager = session.getDirectoryService().getSchemaManager();
103            normalizerMap = schemaManager.getNormalizerMapping();
104            nexus = session.getDirectoryService().getPartitionNexus();
105            memberAT = schemaManager.lookupAttributeTypeRegistry( SchemaConstants.MEMBER_AT_OID );
106            uniqueMemberAT = schemaManager.lookupAttributeTypeRegistry( SchemaConstants.UNIQUE_MEMBER_AT_OID );
107    
108            // stuff for dealing with the admin group
109            administratorsGroupDn = parseNormalized( ServerDNConstants.ADMINISTRATORS_GROUP_DN );
110    
111            initialize( session );
112        }
113    
114    
115        private DN parseNormalized( String name ) throws LdapException
116        {
117            DN dn = new DN( name );
118            dn.normalize( normalizerMap );
119            return dn;
120        }
121    
122    
123        private void initialize( CoreSession session ) throws Exception
124        {
125            // search all naming contexts for static groups and generate
126            // normalized sets of members to cache within the map
127    
128            BranchNode filter = new OrNode();
129            filter.addNode( new EqualityNode<String>( SchemaConstants.OBJECT_CLASS_AT, new StringValue(
130                SchemaConstants.GROUP_OF_NAMES_OC ) ) );
131            filter.addNode( new EqualityNode<String>( SchemaConstants.OBJECT_CLASS_AT, new StringValue(
132                SchemaConstants.GROUP_OF_UNIQUE_NAMES_OC ) ) );
133    
134            Set<String> suffixes = nexus.listSuffixes( null );
135    
136            for ( String suffix:suffixes )
137            {
138                DN baseDn = new DN( suffix ).normalize( normalizerMap );
139                SearchControls ctls = new SearchControls();
140                ctls.setSearchScope( SearchControls.SUBTREE_SCOPE );
141                
142                SearchOperationContext searchOperationContext = new SearchOperationContext( session,
143                    baseDn, filter, ctls );
144                searchOperationContext.setAliasDerefMode( AliasDerefMode.DEREF_ALWAYS );
145                EntryFilteringCursor results = nexus.search( searchOperationContext );
146    
147                while ( results.next() )
148                {
149                    ServerEntry result = results.get();
150                    DN groupDn = result.getDn().normalize( normalizerMap );
151                    EntryAttribute members = getMemberAttribute( result );
152    
153                    if ( members != null )
154                    {
155                        Set<String> memberSet = new HashSet<String>( members.size() );
156                        addMembers( memberSet, members );
157                        groups.put( groupDn.getNormName(), memberSet );
158                    }
159                    else
160                    {
161                        LOG.warn( "Found group '{}' without any member or uniqueMember attributes", groupDn.getName() );
162                    }
163                }
164    
165                results.close();
166            }
167    
168            if ( IS_DEBUG )
169            {
170                LOG.debug( "group cache contents on startup:\n {}", groups );
171            }
172        }
173    
174    
175        /**
176         * Gets the member attribute regardless of whether groupOfNames or
177         * groupOfUniqueNames is used.
178         *
179         * @param entry the entry inspected for member attributes
180         * @return the member attribute
181         */
182        private EntryAttribute getMemberAttribute( ServerEntry entry ) throws LdapException
183        {
184            EntryAttribute oc = entry.get( SchemaConstants.OBJECT_CLASS_AT );
185    
186            if ( oc == null )
187            {
188                EntryAttribute member = entry.get( memberAT );
189    
190                if ( member != null )
191                {
192                    return member;
193                }
194    
195                EntryAttribute uniqueMember = entry.get( uniqueMemberAT );
196    
197                if ( uniqueMember != null )
198                {
199                    return uniqueMember;
200                }
201    
202                return null;
203            }
204    
205            if ( oc.contains( SchemaConstants.GROUP_OF_NAMES_OC ) || oc.contains( SchemaConstants.GROUP_OF_NAMES_OC_OID ) )
206            {
207                return entry.get( memberAT );
208            }
209    
210            if ( oc.contains( SchemaConstants.GROUP_OF_UNIQUE_NAMES_OC )
211                || oc.contains( SchemaConstants.GROUP_OF_UNIQUE_NAMES_OC_OID ) )
212            {
213                return entry.get( uniqueMemberAT );
214            }
215    
216            return null;
217        }
218    
219    
220        /**
221         * Adds normalized member DNs to the set of normalized member names.
222         *
223         * @param memberSet the set of member Dns (Strings)
224         * @param members the member attribute values being added
225         * @throws LdapException if there are problems accessing the attr values
226         */
227        private void addMembers( Set<String> memberSet, EntryAttribute members ) throws LdapException
228        {
229            for ( Value<?> value : members )
230            {
231    
232                // get and normalize the DN of the member
233                String memberDn = value.getString();
234    
235                try
236                {
237                    memberDn = parseNormalized( memberDn ).getNormName();
238                }
239                catch ( LdapException e )
240                {
241                    LOG.warn( "Malformed member DN in groupOf[Unique]Names entry.  Member not added to GroupCache.", e );
242                }
243    
244                memberSet.add( memberDn );
245            }
246        }
247    
248    
249        /**
250         * Removes a set of member names from an existing set.
251         *
252         * @param memberSet the set of normalized member DNs
253         * @param members the set of member values
254         * @throws LdapException if there are problems accessing the attr values
255         */
256        private void removeMembers( Set<String> memberSet, EntryAttribute members ) throws LdapException
257        {
258            for ( Value<?> value : members )
259            {
260                // get and normalize the DN of the member
261                String memberDn = value.getString();
262    
263                try
264                {
265                    memberDn = parseNormalized( memberDn ).getNormName();
266                }
267                catch ( LdapException e )
268                {
269                    LOG.warn( "Malformed member DN in groupOf[Unique]Names entry.  Member not removed from GroupCache.", e );
270                }
271    
272                memberSet.remove( memberDn );
273            }
274        }
275    
276    
277        /**
278         * Adds a groups members to the cache.  Called by interceptor to account for new
279         * group additions.
280         *
281         * @param name the user provided name for the group entry
282         * @param entry the group entry's attributes
283         * @throws LdapException if there are problems accessing the attr values
284         */
285        public void groupAdded( DN name, ServerEntry entry ) throws LdapException
286        {
287            EntryAttribute members = getMemberAttribute( entry );
288    
289            if ( members == null )
290            {
291                return;
292            }
293    
294            Set<String> memberSet = new HashSet<String>( members.size() );
295            addMembers( memberSet, members );
296            groups.put( name.getNormName(), memberSet );
297    
298            if ( IS_DEBUG )
299            {
300                LOG.debug( "group cache contents after adding '{}' :\n {}", name.getName(), groups );
301            }
302        }
303    
304    
305        /**
306         * Deletes a group's members from the cache.  Called by interceptor to account for
307         * the deletion of groups.
308         *
309         * @param name the normalized DN of the group entry
310         * @param entry the attributes of entry being deleted
311         */
312        public void groupDeleted( DN name, ServerEntry entry ) throws LdapException
313        {
314            EntryAttribute members = getMemberAttribute( entry );
315    
316            if ( members == null )
317            {
318                return;
319            }
320    
321            groups.remove( name.getNormName() );
322    
323            if ( IS_DEBUG )
324            {
325                LOG.debug( "group cache contents after deleting '{}' :\n {}", name.getName(), groups );
326            }
327        }
328    
329    
330        /**
331         * Utility method to modify a set of member names based on a modify operation
332         * that changes the members of a group.
333         *
334         * @param memberSet the set of members to be altered
335         * @param modOp the type of modify operation being performed
336         * @param members the members being added, removed or replaced
337         * @throws LdapException if there are problems accessing attribute values
338         */
339        private void modify( Set<String> memberSet, ModificationOperation modOp, EntryAttribute members )
340            throws LdapException
341        {
342    
343            switch ( modOp )
344            {
345                case ADD_ATTRIBUTE:
346                    addMembers( memberSet, members );
347                    break;
348    
349                case REPLACE_ATTRIBUTE:
350                    if ( members.size() > 0 )
351                    {
352                        memberSet.clear();
353                        addMembers( memberSet, members );
354                    }
355    
356                    break;
357    
358                case REMOVE_ATTRIBUTE:
359                    removeMembers( memberSet, members );
360                    break;
361    
362                default:
363                    throw new InternalError( I18n.err( I18n.ERR_235, modOp ) );
364            }
365        }
366    
367    
368        /**
369         * Modifies the cache to reflect changes via modify operations to the group entries.
370         * Called by the interceptor to account for modify ops on groups.
371         *
372         * @param name the normalized name of the group entry modified
373         * @param mods the modification operations being performed
374         * @param entry the group entry being modified
375         * @throws LdapException if there are problems accessing attribute  values
376         */
377        public void groupModified( DN name, List<Modification> mods, ServerEntry entry, SchemaManager schemaManager )
378            throws LdapException
379        {
380            EntryAttribute members = null;
381            String memberAttrId = null;
382            EntryAttribute oc = entry.get( SchemaConstants.OBJECT_CLASS_AT );
383    
384            if ( oc.contains( SchemaConstants.GROUP_OF_NAMES_OC ) )
385            {
386                members = entry.get( memberAT );
387                memberAttrId = SchemaConstants.MEMBER_AT;
388            }
389    
390            if ( oc.contains( SchemaConstants.GROUP_OF_UNIQUE_NAMES_OC ) )
391            {
392                members = entry.get( uniqueMemberAT );
393                memberAttrId = SchemaConstants.UNIQUE_MEMBER_AT;
394            }
395    
396            if ( members == null )
397            {
398                return;
399            }
400    
401            for ( Modification modification : mods )
402            {
403                if ( memberAttrId.equalsIgnoreCase( modification.getAttribute().getId() ) )
404                {
405                    Set<String> memberSet = groups.get( name.getNormName() );
406    
407                    if ( memberSet != null )
408                    {
409                        modify( memberSet, modification.getOperation(), modification.getAttribute() );
410                    }
411    
412                    break;
413                }
414            }
415    
416            if ( IS_DEBUG )
417            {
418                LOG.debug( "group cache contents after modifying '{}' :\n {}", name.getName(), groups );
419            }
420        }
421    
422    
423        /**
424         * Modifies the cache to reflect changes via modify operations to the group entries.
425         * Called by the interceptor to account for modify ops on groups.
426         *
427         * @param name the normalized name of the group entry modified
428         * @param modOp the modify operation being performed
429         * @param mods the modifications being performed
430         * @throws LdapException if there are problems accessing attribute  values
431         */
432        public void groupModified( DN name, ModificationOperation modOp, ServerEntry mods ) throws LdapException
433        {
434            EntryAttribute members = getMemberAttribute( mods );
435    
436            if ( members == null )
437            {
438                return;
439            }
440    
441            Set<String> memberSet = groups.get( name.getNormName() );
442    
443            if ( memberSet != null )
444            {
445                modify( memberSet, modOp, members );
446            }
447    
448            if ( IS_DEBUG )
449            {
450                LOG.debug( "group cache contents after modifying '{}' :\n {}", name.getName(), groups );
451            }
452        }
453    
454    
455        /**
456         * An optimization.  By having this method here we can directly access the group
457         * membership information and lookup to see if the principalDn is contained within.
458         * 
459         * @param principalDn the normalized DN of the user to check if they are an admin
460         * @return true if the principal is an admin or the admin
461         */
462        public final boolean isPrincipalAnAdministrator( DN principalDn )
463        {
464            if ( principalDn.getNormName().equals( ServerDNConstants.ADMIN_SYSTEM_DN_NORMALIZED ) )
465            {
466                return true;
467            }
468    
469            Set<String> members = groups.get( administratorsGroupDn.getNormName() );
470    
471            if ( members == null )
472            {
473                LOG.warn( "What do you mean there is no administrators group? This is bad news." );
474                return false;
475            }
476    
477            return members.contains( principalDn.getNormName() );
478        }
479    
480    
481        /**
482         * Gets the set of groups a user is a member of.  The groups are returned
483         * as normalized Name objects within the set.
484         *
485         * @param member the member (user) to get the groups for
486         * @return a Set of Name objects representing the groups
487         * @throws LdapException if there are problems accessing attribute  values
488         */
489        public Set<DN> getGroups( String member ) throws LdapException
490        {
491            DN normMember;
492    
493            try
494            {
495                normMember = parseNormalized( member );
496            }
497            catch ( LdapException e )
498            {
499                LOG
500                    .warn(
501                        "Malformed member DN.  Could not find groups for member '{}' in GroupCache. Returning empty set for groups!",
502                        member, e );
503                return EMPTY_GROUPS;
504            }
505    
506            Set<DN> memberGroups = null;
507    
508            for ( String group : groups.keySet() )
509            {
510                Set<String> members = groups.get( group );
511    
512                if ( members == null )
513                {
514                    continue;
515                }
516    
517                if ( members.contains( normMember.getNormName() ) )
518                {
519                    if ( memberGroups == null )
520                    {
521                        memberGroups = new HashSet<DN>();
522                    }
523    
524                    memberGroups.add( parseNormalized( group ) );
525                }
526            }
527    
528            if ( memberGroups == null )
529            {
530                return EMPTY_GROUPS;
531            }
532    
533            return memberGroups;
534        }
535    
536    
537        public boolean groupRenamed( DN oldName, DN newName )
538        {
539            Set<String> members = groups.remove( oldName.getNormName() );
540    
541            if ( members != null )
542            {
543                groups.put( newName.getNormName(), members );
544    
545                if ( IS_DEBUG )
546                {
547                    LOG.debug( "group cache contents after renaming '{}' :\n{}", oldName.getName(), groups );
548                }
549    
550                return true;
551            }
552    
553            return false;
554        }
555    }