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