/* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License, Version 1.0 only * (the "License"). You may not use this file except in compliance * with the License. * * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt * or http://forgerock.org/license/CDDLv1.0.html. * See the License for the specific language governing permissions * and limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each * file and include the License file at legal-notices/CDDLv1_0.txt. * If applicable, add the following below this CDDL HEADER, with the * fields enclosed by brackets "[]" replaced with your own identifying * information: * Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END * * * Copyright 2008-2010 Sun Microsystems, Inc. * Portions Copyright 2011-2015 ForgeRock AS */ package org.opends.server.extensions; import java.util.Collections; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; import org.forgerock.i18n.LocalizableMessage; import org.forgerock.i18n.LocalizedIllegalArgumentException; import org.forgerock.i18n.slf4j.LocalizedLogger; import org.forgerock.opendj.adapter.server3x.Converters; import org.forgerock.opendj.config.server.ConfigException; import org.forgerock.opendj.ldap.ByteString; import org.forgerock.opendj.ldap.DN.CompactDn; import org.forgerock.opendj.ldap.ModificationType; import org.forgerock.opendj.ldap.ResultCode; import org.forgerock.opendj.ldap.SearchScope; import org.opends.server.admin.std.server.GroupImplementationCfg; import org.opends.server.admin.std.server.StaticGroupImplementationCfg; import org.opends.server.api.Group; import org.opends.server.core.DirectoryServer; import org.opends.server.core.ModifyOperation; import org.opends.server.core.ModifyOperationBasis; import org.opends.server.core.ServerContext; import org.opends.server.protocols.ldap.LDAPControl; import org.opends.server.types.Attribute; import org.opends.server.types.AttributeType; import org.opends.server.types.Attributes; import org.opends.server.types.Control; import org.opends.server.types.DN; import org.opends.server.types.DirectoryConfig; import org.opends.server.types.DirectoryException; import org.opends.server.types.Entry; import org.opends.server.types.InitializationException; import org.opends.server.types.MemberList; import org.opends.server.types.MembershipException; import org.opends.server.types.Modification; import org.opends.server.types.SearchFilter; import static org.opends.messages.ExtensionMessages.*; import static org.opends.server.core.DirectoryServer.*; import static org.opends.server.protocols.internal.InternalClientConnection.*; import static org.opends.server.util.CollectionUtils.*; import static org.opends.server.util.ServerConstants.*; import static org.forgerock.util.Reject.*; /** * A static group implementation, in which the DNs of all members are explicitly * listed. *

* There are three variants of static groups: *

*/ public class StaticGroup extends Group { private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); /** The attribute type used to hold the membership list for this group. */ private AttributeType memberAttributeType; /** The DN of the entry that holds the definition for this group. */ private DN groupEntryDN; /** The set of the DNs of the members for this group. */ private LinkedHashSet memberDNs; /** The list of nested group DNs for this group. */ private LinkedList nestedGroups = new LinkedList<>(); /** Passed to the group manager to see if the nested group list needs to be refreshed. */ private long nestedGroupRefreshToken = DirectoryServer.getGroupManager().refreshToken(); private ServerContext serverContext; /** * Creates an uninitialized static group. This is intended for internal use * only, to allow {@code GroupManager} to dynamically create a group. */ public StaticGroup() { super(); } /** * Creates a new static group instance with the provided information. * * @param groupEntryDN The DN of the entry that holds the definition * for this group. * @param memberAttributeType The attribute type used to hold the membership * list for this group. * @param memberDNs The set of the DNs of the members for this * group. */ private StaticGroup(ServerContext serverContext, DN groupEntryDN, AttributeType memberAttributeType, LinkedHashSet memberDNs) { super(); ifNull(groupEntryDN, memberAttributeType, memberDNs); this.serverContext = serverContext; this.groupEntryDN = groupEntryDN; this.memberAttributeType = memberAttributeType; this.memberDNs = memberDNs; } /** {@inheritDoc} */ @Override public void initializeGroupImplementation(StaticGroupImplementationCfg configuration) throws ConfigException, InitializationException { // No additional initialization is required. } /** {@inheritDoc} */ @Override public StaticGroup newInstance(ServerContext serverContext, Entry groupEntry) throws DirectoryException { ifNull(groupEntry); // Determine whether it is a groupOfNames, groupOfEntries or // groupOfUniqueNames entry. If not, then that's a problem. AttributeType someMemberAttributeType; boolean hasGroupOfEntriesClass = hasObjectClass(groupEntry, OC_GROUP_OF_ENTRIES_LC); boolean hasGroupOfNamesClass = hasObjectClass(groupEntry, OC_GROUP_OF_NAMES_LC); boolean hasGroupOfUniqueNamesClass = hasObjectClass(groupEntry, OC_GROUP_OF_UNIQUE_NAMES_LC); if (hasGroupOfEntriesClass) { if (hasGroupOfNamesClass) { LocalizableMessage message = ERR_STATICGROUP_INVALID_OC_COMBINATION.get( groupEntry.getName(), OC_GROUP_OF_ENTRIES, OC_GROUP_OF_NAMES); throw new DirectoryException(ResultCode.OBJECTCLASS_VIOLATION, message); } else if (hasGroupOfUniqueNamesClass) { LocalizableMessage message = ERR_STATICGROUP_INVALID_OC_COMBINATION.get( groupEntry.getName(), OC_GROUP_OF_ENTRIES, OC_GROUP_OF_UNIQUE_NAMES); throw new DirectoryException(ResultCode.OBJECTCLASS_VIOLATION, message); } someMemberAttributeType = DirectoryServer.getAttributeTypeOrDefault(ATTR_MEMBER); } else if (hasGroupOfNamesClass) { if (hasGroupOfUniqueNamesClass) { LocalizableMessage message = ERR_STATICGROUP_INVALID_OC_COMBINATION.get( groupEntry.getName(), OC_GROUP_OF_NAMES, OC_GROUP_OF_UNIQUE_NAMES); throw new DirectoryException(ResultCode.OBJECTCLASS_VIOLATION, message); } someMemberAttributeType = DirectoryServer.getAttributeTypeOrDefault(ATTR_MEMBER); } else if (hasGroupOfUniqueNamesClass) { someMemberAttributeType = DirectoryServer.getAttributeTypeOrDefault(ATTR_UNIQUE_MEMBER_LC); } else { LocalizableMessage message = ERR_STATICGROUP_NO_VALID_OC.get(groupEntry.getName(), OC_GROUP_OF_NAMES, OC_GROUP_OF_UNIQUE_NAMES); throw new DirectoryException(ResultCode.OBJECTCLASS_VIOLATION, message); } List memberAttrList = groupEntry.getAttribute(someMemberAttributeType); int membersCount = 0; if (memberAttrList != null) { for (Attribute a : memberAttrList) { membersCount += a.size(); } } LinkedHashSet someMemberDNs = new LinkedHashSet<>(membersCount); if (memberAttrList != null) { for (Attribute a : memberAttrList) { for (ByteString v : a) { try { someMemberDNs.add(org.forgerock.opendj.ldap.DN.valueOf(v.toString()).compact()); } catch (LocalizedIllegalArgumentException e) { logger.traceException(e); logger.error(ERR_STATICGROUP_CANNOT_DECODE_MEMBER_VALUE_AS_DN, v, someMemberAttributeType.getNameOrOID(), groupEntry.getName(), e.getMessageObject()); } } } } return new StaticGroup(serverContext, groupEntry.getName(), someMemberAttributeType, someMemberDNs); } /** {@inheritDoc} */ @Override public SearchFilter getGroupDefinitionFilter() throws DirectoryException { // FIXME -- This needs to exclude enhanced groups once we have support for them. String filterString = "(&(|(objectClass=groupOfNames)(objectClass=groupOfUniqueNames)" + "(objectClass=groupOfEntries))" + "(!(objectClass=ds-virtual-static-group)))"; return SearchFilter.createFilterFromString(filterString); } /** {@inheritDoc} */ @Override public boolean isGroupDefinition(Entry entry) { ifNull(entry); // FIXME -- This needs to exclude enhanced groups once we have support for them. if (hasObjectClass(entry, OC_VIRTUAL_STATIC_GROUP)) { return false; } boolean hasGroupOfEntriesClass = hasObjectClass(entry, OC_GROUP_OF_ENTRIES_LC); boolean hasGroupOfNamesClass = hasObjectClass(entry, OC_GROUP_OF_NAMES_LC); boolean hasGroupOfUniqueNamesClass = hasObjectClass(entry, OC_GROUP_OF_UNIQUE_NAMES_LC); if (hasGroupOfEntriesClass) { return !hasGroupOfNamesClass && !hasGroupOfUniqueNamesClass; } else if (hasGroupOfNamesClass) { return !hasGroupOfUniqueNamesClass; } else { return hasGroupOfUniqueNamesClass; } } private boolean hasObjectClass(Entry entry, String ocName) { return entry.hasObjectClass(DirectoryConfig.getObjectClass(ocName, true)); } /** {@inheritDoc} */ @Override public DN getGroupDN() { return groupEntryDN; } /** {@inheritDoc} */ @Override public void setGroupDN(DN groupDN) { groupEntryDN = groupDN; } /** {@inheritDoc} */ @Override public boolean supportsNestedGroups() { return true; } /** {@inheritDoc} */ @Override public List getNestedGroupDNs() { try { reloadIfNeeded(); } catch (DirectoryException ex) { return Collections.emptyList(); } return nestedGroups; } /** {@inheritDoc} */ @Override public void addNestedGroup(DN nestedGroupDN) throws UnsupportedOperationException, DirectoryException { ifNull(nestedGroupDN); synchronized (this) { if (nestedGroups.contains(nestedGroupDN)) { LocalizableMessage msg = ERR_STATICGROUP_ADD_NESTED_GROUP_ALREADY_EXISTS.get(nestedGroupDN, groupEntryDN); throw new DirectoryException(ResultCode.ATTRIBUTE_OR_VALUE_EXISTS, msg); } ModifyOperation modifyOperation = newModifyOperation(ModificationType.ADD, nestedGroupDN); modifyOperation.run(); if (modifyOperation.getResultCode() != ResultCode.SUCCESS) { LocalizableMessage msg = ERR_STATICGROUP_ADD_MEMBER_UPDATE_FAILED.get( nestedGroupDN, groupEntryDN, modifyOperation.getErrorMessage()); throw new DirectoryException(modifyOperation.getResultCode(), msg); } LinkedList newNestedGroups = new LinkedList<>(nestedGroups); newNestedGroups.add(nestedGroupDN); nestedGroups = newNestedGroups; //Add it to the member DN list. LinkedHashSet newMemberDNs = new LinkedHashSet<>(memberDNs); newMemberDNs.add(toCompactDn(nestedGroupDN)); memberDNs = newMemberDNs; } } /** {@inheritDoc} */ @Override public void removeNestedGroup(DN nestedGroupDN) throws UnsupportedOperationException, DirectoryException { ifNull(nestedGroupDN); synchronized (this) { if (! nestedGroups.contains(nestedGroupDN)) { throw new DirectoryException(ResultCode.NO_SUCH_ATTRIBUTE, ERR_STATICGROUP_REMOVE_NESTED_GROUP_NO_SUCH_GROUP.get(nestedGroupDN, groupEntryDN)); } ModifyOperation modifyOperation = newModifyOperation(ModificationType.DELETE, nestedGroupDN); modifyOperation.run(); if (modifyOperation.getResultCode() != ResultCode.SUCCESS) { LocalizableMessage message = ERR_STATICGROUP_REMOVE_MEMBER_UPDATE_FAILED.get( nestedGroupDN, groupEntryDN, modifyOperation.getErrorMessage()); throw new DirectoryException(modifyOperation.getResultCode(), message); } LinkedList newNestedGroups = new LinkedList<>(nestedGroups); newNestedGroups.remove(nestedGroupDN); nestedGroups = newNestedGroups; //Remove it from the member DN list. LinkedHashSet newMemberDNs = new LinkedHashSet<>(memberDNs); newMemberDNs.remove(toCompactDn(nestedGroupDN)); memberDNs = newMemberDNs; } } /** {@inheritDoc} */ @Override public boolean isMember(DN userDN, Set examinedGroups) throws DirectoryException { reloadIfNeeded(); CompactDn compactUserDN = toCompactDn(userDN); if (memberDNs.contains(compactUserDN)) { return true; } else if (!examinedGroups.add(getGroupDN())) { return false; } else { for(DN nestedGroupDN : nestedGroups) { Group group = getGroupManager().getGroupInstance(nestedGroupDN); if (group != null && group.isMember(userDN, examinedGroups)) { return true; } } } return false; } /** {@inheritDoc} */ @Override public boolean isMember(Entry userEntry, Set examinedGroups) throws DirectoryException { return isMember(userEntry.getName(), examinedGroups); } /** * Check if the group manager has registered a new group instance or removed a * a group instance that might impact this group's membership list. */ private void reloadIfNeeded() throws DirectoryException { //Check if group instances have changed by passing the group manager //the current token. if (DirectoryServer.getGroupManager().hasInstancesChanged(nestedGroupRefreshToken)) { synchronized (this) { Group thisGroup = DirectoryServer.getGroupManager().getGroupInstance(groupEntryDN); // Check if the group itself has been removed if (thisGroup == null) { throw new DirectoryException(ResultCode.NO_SUCH_ATTRIBUTE, ERR_STATICGROUP_GROUP_INSTANCE_INVALID.get(groupEntryDN)); } else if (thisGroup != this) { LinkedHashSet newMemberDNs = new LinkedHashSet<>(); MemberList memberList = thisGroup.getMembers(); while (memberList.hasMoreMembers()) { try { newMemberDNs.add(toCompactDn(memberList.nextMemberDN())); } catch (MembershipException ex) { // TODO: should we throw an exception there instead of silently fail ? } } memberDNs = newMemberDNs; } LinkedList newNestedGroups = new LinkedList<>(); for (CompactDn compactDn : memberDNs) { DN dn = fromCompactDn(compactDn); Group group = DirectoryServer.getGroupManager().getGroupInstance(dn); if (group != null) { newNestedGroups.add(group.getGroupDN()); } } nestedGroupRefreshToken = DirectoryServer.getGroupManager().refreshToken(); nestedGroups=newNestedGroups; } } } /** {@inheritDoc} */ @Override public MemberList getMembers() throws DirectoryException { reloadIfNeeded(); return new SimpleStaticGroupMemberList(groupEntryDN, memberDNs); } /** {@inheritDoc} */ @Override public MemberList getMembers(DN baseDN, SearchScope scope, SearchFilter filter) throws DirectoryException { reloadIfNeeded(); if (baseDN == null && filter == null) { return new SimpleStaticGroupMemberList(groupEntryDN, memberDNs); } return new FilteredStaticGroupMemberList(groupEntryDN, memberDNs, baseDN, scope, filter); } /** {@inheritDoc} */ @Override public boolean mayAlterMemberList() { return true; } /** {@inheritDoc} */ @Override public void addMember(Entry userEntry) throws UnsupportedOperationException, DirectoryException { ifNull(userEntry); synchronized (this) { DN userDN = userEntry.getName(); CompactDn compactUserDN = toCompactDn(userDN); if (memberDNs.contains(compactUserDN)) { LocalizableMessage message = ERR_STATICGROUP_ADD_MEMBER_ALREADY_EXISTS.get(userDN, groupEntryDN); throw new DirectoryException(ResultCode.ATTRIBUTE_OR_VALUE_EXISTS, message); } ModifyOperation modifyOperation = newModifyOperation(ModificationType.ADD, userDN); modifyOperation.run(); if (modifyOperation.getResultCode() != ResultCode.SUCCESS) { throw new DirectoryException(modifyOperation.getResultCode(), ERR_STATICGROUP_ADD_MEMBER_UPDATE_FAILED.get(userDN, groupEntryDN, modifyOperation.getErrorMessage())); } LinkedHashSet newMemberDNs = new LinkedHashSet(memberDNs); newMemberDNs.add(compactUserDN); memberDNs = newMemberDNs; } } /** {@inheritDoc} */ @Override public void removeMember(DN userDN) throws UnsupportedOperationException, DirectoryException { ifNull(userDN); CompactDn compactUserDN = toCompactDn(userDN); synchronized (this) { if (! memberDNs.contains(compactUserDN)) { LocalizableMessage message = ERR_STATICGROUP_REMOVE_MEMBER_NO_SUCH_MEMBER.get(userDN, groupEntryDN); throw new DirectoryException(ResultCode.NO_SUCH_ATTRIBUTE, message); } ModifyOperation modifyOperation = newModifyOperation(ModificationType.DELETE, userDN); modifyOperation.run(); if (modifyOperation.getResultCode() != ResultCode.SUCCESS) { throw new DirectoryException(modifyOperation.getResultCode(), ERR_STATICGROUP_REMOVE_MEMBER_UPDATE_FAILED.get(userDN, groupEntryDN, modifyOperation.getErrorMessage())); } LinkedHashSet newMemberDNs = new LinkedHashSet<>(memberDNs); newMemberDNs.remove(compactUserDN); memberDNs = newMemberDNs; //If it is in the nested group list remove it. if(nestedGroups.contains(userDN)) { LinkedList newNestedGroups = new LinkedList<>(nestedGroups); newNestedGroups.remove(userDN); nestedGroups = newNestedGroups; } } } private ModifyOperation newModifyOperation(ModificationType modType, DN userDN) { Attribute attr = Attributes.create(memberAttributeType, userDN.toString()); LinkedList mods = newLinkedList(new Modification(modType, attr)); Control control = new LDAPControl(OID_INTERNAL_GROUP_MEMBERSHIP_UPDATE, false); return new ModifyOperationBasis(getRootConnection(), nextOperationID(), nextMessageID(), newLinkedList(control), groupEntryDN, mods); } /** {@inheritDoc} */ @Override public void toString(StringBuilder buffer) { buffer.append("StaticGroup("); buffer.append(groupEntryDN); buffer.append(")"); } /** * Convert the provided DN to a compact DN. * * @param dn * The DN * @return the compact representation of the DN */ private CompactDn toCompactDn(DN dn) { return Converters.from(dn).compact(); } /** * Convert the provided compact DN to a DN. * * @param compactDn * Compact representation of a DN * @return the regular DN */ static DN fromCompactDn(CompactDn compactDn) { return Converters.to(compactDn.toDn()); } }