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}