001/* 002 * CDDL HEADER START 003 * 004 * The contents of this file are subject to the terms of the 005 * Common Development and Distribution License, Version 1.0 only 006 * (the "License"). You may not use this file except in compliance 007 * with the License. 008 * 009 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt 010 * or http://forgerock.org/license/CDDLv1.0.html. 011 * See the License for the specific language governing permissions 012 * and limitations under the License. 013 * 014 * When distributing Covered Code, include this CDDL HEADER in each 015 * file and include the License file at legal-notices/CDDLv1_0.txt. 016 * If applicable, add the following below this CDDL HEADER, with the 017 * fields enclosed by brackets "[]" replaced with your own identifying 018 * information: 019 * Portions Copyright [yyyy] [name of copyright owner] 020 * 021 * CDDL HEADER END 022 * 023 * 024 * Copyright 2008-2010 Sun Microsystems, Inc. 025 * Portions Copyright 2011-2015 ForgeRock AS 026 */ 027package org.opends.server.extensions; 028 029import java.util.Collections; 030import java.util.LinkedHashSet; 031import java.util.LinkedList; 032import java.util.List; 033import java.util.Set; 034 035import org.forgerock.i18n.LocalizableMessage; 036import org.forgerock.i18n.LocalizedIllegalArgumentException; 037import org.forgerock.i18n.slf4j.LocalizedLogger; 038import org.forgerock.opendj.adapter.server3x.Converters; 039import org.forgerock.opendj.config.server.ConfigException; 040import org.forgerock.opendj.ldap.ByteString; 041import org.forgerock.opendj.ldap.DN.CompactDn; 042import org.forgerock.opendj.ldap.ModificationType; 043import org.forgerock.opendj.ldap.ResultCode; 044import org.forgerock.opendj.ldap.SearchScope; 045import org.opends.server.admin.std.server.GroupImplementationCfg; 046import org.opends.server.admin.std.server.StaticGroupImplementationCfg; 047import org.opends.server.api.Group; 048import org.opends.server.core.DirectoryServer; 049import org.opends.server.core.ModifyOperation; 050import org.opends.server.core.ModifyOperationBasis; 051import org.opends.server.core.ServerContext; 052import org.opends.server.protocols.ldap.LDAPControl; 053import org.opends.server.types.Attribute; 054import org.opends.server.types.AttributeType; 055import org.opends.server.types.Attributes; 056import org.opends.server.types.Control; 057import org.opends.server.types.DN; 058import org.opends.server.types.DirectoryConfig; 059import org.opends.server.types.DirectoryException; 060import org.opends.server.types.Entry; 061import org.opends.server.types.InitializationException; 062import org.opends.server.types.MemberList; 063import org.opends.server.types.MembershipException; 064import org.opends.server.types.Modification; 065import org.opends.server.types.SearchFilter; 066 067import static org.opends.messages.ExtensionMessages.*; 068import static org.opends.server.core.DirectoryServer.*; 069import static org.opends.server.protocols.internal.InternalClientConnection.*; 070import static org.opends.server.util.CollectionUtils.*; 071import static org.opends.server.util.ServerConstants.*; 072import static org.forgerock.util.Reject.*; 073 074/** 075 * A static group implementation, in which the DNs of all members are explicitly 076 * listed. 077 * <p> 078 * There are three variants of static groups: 079 * <ul> 080 * <li>one based on the {@code groupOfNames} object class: which stores the 081 * member list in the {@code member} attribute</li> 082 * <li>one based on the {@code groupOfEntries} object class, which also stores 083 * the member list in the {@code member} attribute</li> 084 * <li>one based on the {@code groupOfUniqueNames} object class, which stores 085 * the member list in the {@code uniqueMember} attribute.</li> 086 * </ul> 087 */ 088public class StaticGroup extends Group<StaticGroupImplementationCfg> 089{ 090 private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); 091 092 /** The attribute type used to hold the membership list for this group. */ 093 private AttributeType memberAttributeType; 094 095 /** The DN of the entry that holds the definition for this group. */ 096 private DN groupEntryDN; 097 098 /** The set of the DNs of the members for this group. */ 099 private LinkedHashSet<CompactDn> memberDNs; 100 101 /** The list of nested group DNs for this group. */ 102 private LinkedList<DN> nestedGroups = new LinkedList<>(); 103 104 /** Passed to the group manager to see if the nested group list needs to be refreshed. */ 105 private long nestedGroupRefreshToken = DirectoryServer.getGroupManager().refreshToken(); 106 107 private ServerContext serverContext; 108 109 /** 110 * Creates an uninitialized static group. This is intended for internal use 111 * only, to allow {@code GroupManager} to dynamically create a group. 112 */ 113 public StaticGroup() 114 { 115 super(); 116 } 117 118 /** 119 * Creates a new static group instance with the provided information. 120 * 121 * @param groupEntryDN The DN of the entry that holds the definition 122 * for this group. 123 * @param memberAttributeType The attribute type used to hold the membership 124 * list for this group. 125 * @param memberDNs The set of the DNs of the members for this 126 * group. 127 */ 128 private StaticGroup(ServerContext serverContext, DN groupEntryDN, AttributeType memberAttributeType, 129 LinkedHashSet<CompactDn> memberDNs) 130 { 131 super(); 132 ifNull(groupEntryDN, memberAttributeType, memberDNs); 133 134 this.serverContext = serverContext; 135 this.groupEntryDN = groupEntryDN; 136 this.memberAttributeType = memberAttributeType; 137 this.memberDNs = memberDNs; 138 } 139 140 /** {@inheritDoc} */ 141 @Override 142 public void initializeGroupImplementation(StaticGroupImplementationCfg configuration) 143 throws ConfigException, InitializationException 144 { 145 // No additional initialization is required. 146 } 147 148 /** {@inheritDoc} */ 149 @Override 150 public StaticGroup newInstance(ServerContext serverContext, Entry groupEntry) throws DirectoryException 151 { 152 ifNull(groupEntry); 153 154 // Determine whether it is a groupOfNames, groupOfEntries or 155 // groupOfUniqueNames entry. If not, then that's a problem. 156 AttributeType someMemberAttributeType; 157 boolean hasGroupOfEntriesClass = hasObjectClass(groupEntry, OC_GROUP_OF_ENTRIES_LC); 158 boolean hasGroupOfNamesClass = hasObjectClass(groupEntry, OC_GROUP_OF_NAMES_LC); 159 boolean hasGroupOfUniqueNamesClass = hasObjectClass(groupEntry, OC_GROUP_OF_UNIQUE_NAMES_LC); 160 if (hasGroupOfEntriesClass) 161 { 162 if (hasGroupOfNamesClass) 163 { 164 LocalizableMessage message = ERR_STATICGROUP_INVALID_OC_COMBINATION.get( 165 groupEntry.getName(), OC_GROUP_OF_ENTRIES, OC_GROUP_OF_NAMES); 166 throw new DirectoryException(ResultCode.OBJECTCLASS_VIOLATION, message); 167 } 168 else if (hasGroupOfUniqueNamesClass) 169 { 170 LocalizableMessage message = ERR_STATICGROUP_INVALID_OC_COMBINATION.get( 171 groupEntry.getName(), OC_GROUP_OF_ENTRIES, OC_GROUP_OF_UNIQUE_NAMES); 172 throw new DirectoryException(ResultCode.OBJECTCLASS_VIOLATION, message); 173 } 174 175 someMemberAttributeType = DirectoryServer.getAttributeTypeOrDefault(ATTR_MEMBER); 176 } 177 else if (hasGroupOfNamesClass) 178 { 179 if (hasGroupOfUniqueNamesClass) 180 { 181 LocalizableMessage message = ERR_STATICGROUP_INVALID_OC_COMBINATION.get( 182 groupEntry.getName(), OC_GROUP_OF_NAMES, OC_GROUP_OF_UNIQUE_NAMES); 183 throw new DirectoryException(ResultCode.OBJECTCLASS_VIOLATION, message); 184 } 185 186 someMemberAttributeType = DirectoryServer.getAttributeTypeOrDefault(ATTR_MEMBER); 187 } 188 else if (hasGroupOfUniqueNamesClass) 189 { 190 someMemberAttributeType = DirectoryServer.getAttributeTypeOrDefault(ATTR_UNIQUE_MEMBER_LC); 191 } 192 else 193 { 194 LocalizableMessage message = 195 ERR_STATICGROUP_NO_VALID_OC.get(groupEntry.getName(), OC_GROUP_OF_NAMES, OC_GROUP_OF_UNIQUE_NAMES); 196 throw new DirectoryException(ResultCode.OBJECTCLASS_VIOLATION, message); 197 } 198 199 List<Attribute> memberAttrList = groupEntry.getAttribute(someMemberAttributeType); 200 int membersCount = 0; 201 if (memberAttrList != null) 202 { 203 for (Attribute a : memberAttrList) 204 { 205 membersCount += a.size(); 206 } 207 } 208 LinkedHashSet<CompactDn> someMemberDNs = new LinkedHashSet<>(membersCount); 209 if (memberAttrList != null) 210 { 211 for (Attribute a : memberAttrList) 212 { 213 for (ByteString v : a) 214 { 215 try 216 { 217 someMemberDNs.add(org.forgerock.opendj.ldap.DN.valueOf(v.toString()).compact()); 218 } 219 catch (LocalizedIllegalArgumentException e) 220 { 221 logger.traceException(e); 222 logger.error(ERR_STATICGROUP_CANNOT_DECODE_MEMBER_VALUE_AS_DN, v, 223 someMemberAttributeType.getNameOrOID(), groupEntry.getName(), e.getMessageObject()); 224 } 225 } 226 } 227 } 228 return new StaticGroup(serverContext, groupEntry.getName(), someMemberAttributeType, someMemberDNs); 229 } 230 231 /** {@inheritDoc} */ 232 @Override 233 public SearchFilter getGroupDefinitionFilter() 234 throws DirectoryException 235 { 236 // FIXME -- This needs to exclude enhanced groups once we have support for them. 237 String filterString = 238 "(&(|(objectClass=groupOfNames)(objectClass=groupOfUniqueNames)" + 239 "(objectClass=groupOfEntries))" + 240 "(!(objectClass=ds-virtual-static-group)))"; 241 return SearchFilter.createFilterFromString(filterString); 242 } 243 244 /** {@inheritDoc} */ 245 @Override 246 public boolean isGroupDefinition(Entry entry) 247 { 248 ifNull(entry); 249 250 // FIXME -- This needs to exclude enhanced groups once we have support for them. 251 if (hasObjectClass(entry, OC_VIRTUAL_STATIC_GROUP)) 252 { 253 return false; 254 } 255 256 boolean hasGroupOfEntriesClass = hasObjectClass(entry, OC_GROUP_OF_ENTRIES_LC); 257 boolean hasGroupOfNamesClass = hasObjectClass(entry, OC_GROUP_OF_NAMES_LC); 258 boolean hasGroupOfUniqueNamesClass = hasObjectClass(entry, OC_GROUP_OF_UNIQUE_NAMES_LC); 259 if (hasGroupOfEntriesClass) 260 { 261 return !hasGroupOfNamesClass 262 && !hasGroupOfUniqueNamesClass; 263 } 264 else if (hasGroupOfNamesClass) 265 { 266 return !hasGroupOfUniqueNamesClass; 267 } 268 else 269 { 270 return hasGroupOfUniqueNamesClass; 271 } 272 } 273 274 private boolean hasObjectClass(Entry entry, String ocName) 275 { 276 return entry.hasObjectClass(DirectoryConfig.getObjectClass(ocName, true)); 277 } 278 279 /** {@inheritDoc} */ 280 @Override 281 public DN getGroupDN() 282 { 283 return groupEntryDN; 284 } 285 286 /** {@inheritDoc} */ 287 @Override 288 public void setGroupDN(DN groupDN) 289 { 290 groupEntryDN = groupDN; 291 } 292 293 /** {@inheritDoc} */ 294 @Override 295 public boolean supportsNestedGroups() 296 { 297 return true; 298 } 299 300 /** {@inheritDoc} */ 301 @Override 302 public List<DN> getNestedGroupDNs() 303 { 304 try { 305 reloadIfNeeded(); 306 } catch (DirectoryException ex) { 307 return Collections.<DN>emptyList(); 308 } 309 return nestedGroups; 310 } 311 312 /** {@inheritDoc} */ 313 @Override 314 public void addNestedGroup(DN nestedGroupDN) 315 throws UnsupportedOperationException, DirectoryException 316 { 317 ifNull(nestedGroupDN); 318 319 synchronized (this) 320 { 321 if (nestedGroups.contains(nestedGroupDN)) 322 { 323 LocalizableMessage msg = ERR_STATICGROUP_ADD_NESTED_GROUP_ALREADY_EXISTS.get(nestedGroupDN, groupEntryDN); 324 throw new DirectoryException(ResultCode.ATTRIBUTE_OR_VALUE_EXISTS, msg); 325 } 326 327 ModifyOperation modifyOperation = newModifyOperation(ModificationType.ADD, nestedGroupDN); 328 modifyOperation.run(); 329 if (modifyOperation.getResultCode() != ResultCode.SUCCESS) 330 { 331 LocalizableMessage msg = ERR_STATICGROUP_ADD_MEMBER_UPDATE_FAILED.get( 332 nestedGroupDN, groupEntryDN, modifyOperation.getErrorMessage()); 333 throw new DirectoryException(modifyOperation.getResultCode(), msg); 334 } 335 336 LinkedList<DN> newNestedGroups = new LinkedList<>(nestedGroups); 337 newNestedGroups.add(nestedGroupDN); 338 nestedGroups = newNestedGroups; 339 //Add it to the member DN list. 340 LinkedHashSet<CompactDn> newMemberDNs = new LinkedHashSet<>(memberDNs); 341 newMemberDNs.add(toCompactDn(nestedGroupDN)); 342 memberDNs = newMemberDNs; 343 } 344 } 345 346 /** {@inheritDoc} */ 347 @Override 348 public void removeNestedGroup(DN nestedGroupDN) 349 throws UnsupportedOperationException, DirectoryException 350 { 351 ifNull(nestedGroupDN); 352 353 synchronized (this) 354 { 355 if (! nestedGroups.contains(nestedGroupDN)) 356 { 357 throw new DirectoryException(ResultCode.NO_SUCH_ATTRIBUTE, 358 ERR_STATICGROUP_REMOVE_NESTED_GROUP_NO_SUCH_GROUP.get(nestedGroupDN, groupEntryDN)); 359 } 360 361 ModifyOperation modifyOperation = newModifyOperation(ModificationType.DELETE, nestedGroupDN); 362 modifyOperation.run(); 363 if (modifyOperation.getResultCode() != ResultCode.SUCCESS) 364 { 365 LocalizableMessage message = ERR_STATICGROUP_REMOVE_MEMBER_UPDATE_FAILED.get( 366 nestedGroupDN, groupEntryDN, modifyOperation.getErrorMessage()); 367 throw new DirectoryException(modifyOperation.getResultCode(), message); 368 } 369 370 LinkedList<DN> newNestedGroups = new LinkedList<>(nestedGroups); 371 newNestedGroups.remove(nestedGroupDN); 372 nestedGroups = newNestedGroups; 373 //Remove it from the member DN list. 374 LinkedHashSet<CompactDn> newMemberDNs = new LinkedHashSet<>(memberDNs); 375 newMemberDNs.remove(toCompactDn(nestedGroupDN)); 376 memberDNs = newMemberDNs; 377 } 378 } 379 380 /** {@inheritDoc} */ 381 @Override 382 public boolean isMember(DN userDN, Set<DN> examinedGroups) throws DirectoryException 383 { 384 reloadIfNeeded(); 385 CompactDn compactUserDN = toCompactDn(userDN); 386 if (memberDNs.contains(compactUserDN)) 387 { 388 return true; 389 } 390 else if (!examinedGroups.add(getGroupDN())) 391 { 392 return false; 393 } 394 else 395 { 396 for(DN nestedGroupDN : nestedGroups) 397 { 398 Group<? extends GroupImplementationCfg> group = getGroupManager().getGroupInstance(nestedGroupDN); 399 if (group != null && group.isMember(userDN, examinedGroups)) 400 { 401 return true; 402 } 403 } 404 } 405 return false; 406 } 407 408 /** {@inheritDoc} */ 409 @Override 410 public boolean isMember(Entry userEntry, Set<DN> examinedGroups) 411 throws DirectoryException 412 { 413 return isMember(userEntry.getName(), examinedGroups); 414 } 415 416 /** 417 * Check if the group manager has registered a new group instance or removed a 418 * a group instance that might impact this group's membership list. 419 */ 420 private void reloadIfNeeded() throws DirectoryException 421 { 422 //Check if group instances have changed by passing the group manager 423 //the current token. 424 if (DirectoryServer.getGroupManager().hasInstancesChanged(nestedGroupRefreshToken)) 425 { 426 synchronized (this) 427 { 428 Group<?> thisGroup = DirectoryServer.getGroupManager().getGroupInstance(groupEntryDN); 429 // Check if the group itself has been removed 430 if (thisGroup == null) { 431 throw new DirectoryException(ResultCode.NO_SUCH_ATTRIBUTE, 432 ERR_STATICGROUP_GROUP_INSTANCE_INVALID.get(groupEntryDN)); 433 } else if (thisGroup != this) { 434 LinkedHashSet<CompactDn> newMemberDNs = new LinkedHashSet<>(); 435 MemberList memberList = thisGroup.getMembers(); 436 while (memberList.hasMoreMembers()) 437 { 438 try 439 { 440 newMemberDNs.add(toCompactDn(memberList.nextMemberDN())); 441 } 442 catch (MembershipException ex) 443 { 444 // TODO: should we throw an exception there instead of silently fail ? 445 } 446 } 447 memberDNs = newMemberDNs; 448 } 449 LinkedList<DN> newNestedGroups = new LinkedList<>(); 450 for (CompactDn compactDn : memberDNs) 451 { 452 DN dn = fromCompactDn(compactDn); 453 Group<?> group = DirectoryServer.getGroupManager().getGroupInstance(dn); 454 if (group != null) 455 { 456 newNestedGroups.add(group.getGroupDN()); 457 } 458 } 459 nestedGroupRefreshToken = DirectoryServer.getGroupManager().refreshToken(); 460 nestedGroups=newNestedGroups; 461 } 462 } 463 } 464 465 /** {@inheritDoc} */ 466 @Override 467 public MemberList getMembers() throws DirectoryException 468 { 469 reloadIfNeeded(); 470 return new SimpleStaticGroupMemberList(groupEntryDN, memberDNs); 471 } 472 473 /** {@inheritDoc} */ 474 @Override 475 public MemberList getMembers(DN baseDN, SearchScope scope, SearchFilter filter) throws DirectoryException 476 { 477 reloadIfNeeded(); 478 if (baseDN == null && filter == null) 479 { 480 return new SimpleStaticGroupMemberList(groupEntryDN, memberDNs); 481 } 482 return new FilteredStaticGroupMemberList(groupEntryDN, memberDNs, baseDN, scope, filter); 483 } 484 485 /** {@inheritDoc} */ 486 @Override 487 public boolean mayAlterMemberList() 488 { 489 return true; 490 } 491 492 /** {@inheritDoc} */ 493 @Override 494 public void addMember(Entry userEntry) throws UnsupportedOperationException, DirectoryException 495 { 496 ifNull(userEntry); 497 498 synchronized (this) 499 { 500 DN userDN = userEntry.getName(); 501 CompactDn compactUserDN = toCompactDn(userDN); 502 503 if (memberDNs.contains(compactUserDN)) 504 { 505 LocalizableMessage message = ERR_STATICGROUP_ADD_MEMBER_ALREADY_EXISTS.get(userDN, groupEntryDN); 506 throw new DirectoryException(ResultCode.ATTRIBUTE_OR_VALUE_EXISTS, message); 507 } 508 509 ModifyOperation modifyOperation = newModifyOperation(ModificationType.ADD, userDN); 510 modifyOperation.run(); 511 if (modifyOperation.getResultCode() != ResultCode.SUCCESS) 512 { 513 throw new DirectoryException(modifyOperation.getResultCode(), 514 ERR_STATICGROUP_ADD_MEMBER_UPDATE_FAILED.get(userDN, groupEntryDN, modifyOperation.getErrorMessage())); 515 } 516 517 LinkedHashSet<CompactDn> newMemberDNs = new LinkedHashSet<CompactDn>(memberDNs); 518 newMemberDNs.add(compactUserDN); 519 memberDNs = newMemberDNs; 520 } 521 } 522 523 /** {@inheritDoc} */ 524 @Override 525 public void removeMember(DN userDN) throws UnsupportedOperationException, DirectoryException 526 { 527 ifNull(userDN); 528 529 CompactDn compactUserDN = toCompactDn(userDN); 530 synchronized (this) 531 { 532 if (! memberDNs.contains(compactUserDN)) 533 { 534 LocalizableMessage message = ERR_STATICGROUP_REMOVE_MEMBER_NO_SUCH_MEMBER.get(userDN, groupEntryDN); 535 throw new DirectoryException(ResultCode.NO_SUCH_ATTRIBUTE, message); 536 } 537 538 ModifyOperation modifyOperation = newModifyOperation(ModificationType.DELETE, userDN); 539 modifyOperation.run(); 540 if (modifyOperation.getResultCode() != ResultCode.SUCCESS) 541 { 542 throw new DirectoryException(modifyOperation.getResultCode(), 543 ERR_STATICGROUP_REMOVE_MEMBER_UPDATE_FAILED.get(userDN, groupEntryDN, modifyOperation.getErrorMessage())); 544 } 545 546 LinkedHashSet<CompactDn> newMemberDNs = new LinkedHashSet<>(memberDNs); 547 newMemberDNs.remove(compactUserDN); 548 memberDNs = newMemberDNs; 549 //If it is in the nested group list remove it. 550 if(nestedGroups.contains(userDN)) { 551 LinkedList<DN> newNestedGroups = new LinkedList<>(nestedGroups); 552 newNestedGroups.remove(userDN); 553 nestedGroups = newNestedGroups; 554 } 555 } 556 } 557 558 private ModifyOperation newModifyOperation(ModificationType modType, DN userDN) 559 { 560 Attribute attr = Attributes.create(memberAttributeType, userDN.toString()); 561 LinkedList<Modification> mods = newLinkedList(new Modification(modType, attr)); 562 Control control = new LDAPControl(OID_INTERNAL_GROUP_MEMBERSHIP_UPDATE, false); 563 564 return new ModifyOperationBasis(getRootConnection(), nextOperationID(), nextMessageID(), 565 newLinkedList(control), groupEntryDN, mods); 566 } 567 568 /** {@inheritDoc} */ 569 @Override 570 public void toString(StringBuilder buffer) 571 { 572 buffer.append("StaticGroup("); 573 buffer.append(groupEntryDN); 574 buffer.append(")"); 575 } 576 577 /** 578 * Convert the provided DN to a compact DN. 579 * 580 * @param dn 581 * The DN 582 * @return the compact representation of the DN 583 */ 584 private CompactDn toCompactDn(DN dn) 585 { 586 return Converters.from(dn).compact(); 587 } 588 589 /** 590 * Convert the provided compact DN to a DN. 591 * 592 * @param compactDn 593 * Compact representation of a DN 594 * @return the regular DN 595 */ 596 static DN fromCompactDn(CompactDn compactDn) 597 { 598 return Converters.to(compactDn.toDn()); 599 } 600} 601