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