001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2008-2009 Sun Microsystems, Inc.
015 * Portions Copyright 2011-2016 ForgeRock AS.
016 */
017package org.opends.server.extensions;
018
019import java.util.HashSet;
020import java.util.List;
021import java.util.Set;
022
023import org.forgerock.i18n.slf4j.LocalizedLogger;
024import org.forgerock.opendj.ldap.ByteString;
025import org.forgerock.opendj.ldap.ConditionResult;
026import org.forgerock.opendj.ldap.DN;
027import org.forgerock.opendj.ldap.SearchScope;
028import org.forgerock.opendj.server.config.server.IsMemberOfVirtualAttributeCfg;
029import org.opends.server.api.Group;
030import org.opends.server.api.VirtualAttributeProvider;
031import org.opends.server.core.DirectoryServer;
032import org.opends.server.core.SearchOperation;
033import org.forgerock.opendj.ldap.schema.AttributeType;
034import org.opends.server.types.*;
035
036import static org.opends.server.util.ServerConstants.*;
037
038/**
039 * This class implements a virtual attribute provider that is meant to serve the
040 * isMemberOf operational attribute.  This attribute will be used to provide a
041 * list of all groups in which the specified user is a member.
042 */
043public class IsMemberOfVirtualAttributeProvider
044       extends VirtualAttributeProvider<IsMemberOfVirtualAttributeCfg>
045{
046  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
047
048  /** Creates a new instance of this entryDN virtual attribute provider. */
049  public IsMemberOfVirtualAttributeProvider()
050  {
051    super();
052
053    // All initialization should be performed in the
054    // initializeVirtualAttributeProvider method.
055  }
056
057  @Override
058  public boolean isMultiValued()
059  {
060    return true;
061  }
062
063  @Override
064  public Attribute getValues(Entry entry, VirtualAttributeRule rule)
065  {
066    // FIXME -- This probably isn't the most efficient implementation.
067    AttributeBuilder builder = new AttributeBuilder(rule.getAttributeType());
068    for (Group<?> g : DirectoryServer.getGroupManager().getGroupInstances())
069    {
070      try
071      {
072        if (g.isMember(entry))
073        {
074          builder.add(g.getGroupDN().toString());
075        }
076      }
077      catch (Exception e)
078      {
079        logger.traceException(e);
080      }
081    }
082    return builder.toAttribute();
083  }
084
085  @Override
086  public boolean hasValue(Entry entry, VirtualAttributeRule rule)
087  {
088    // FIXME -- This probably isn't the most efficient implementation.
089    for (Group<?> g : DirectoryServer.getGroupManager().getGroupInstances())
090    {
091      try
092      {
093        if (g.isMember(entry))
094        {
095          return true;
096        }
097      }
098      catch (Exception e)
099      {
100        logger.traceException(e);
101      }
102    }
103
104    return false;
105  }
106
107  @Override
108  public boolean hasValue(Entry entry, VirtualAttributeRule rule,
109                          ByteString value)
110  {
111    try
112    {
113      DN groupDN = DN.valueOf(value);
114      Group<?> g = DirectoryServer.getGroupManager().getGroupInstance(groupDN);
115      return g != null && g.isMember(entry);
116    }
117    catch (Exception e)
118    {
119      logger.traceException(e);
120
121      return false;
122    }
123  }
124
125  @Override
126  public ConditionResult matchesSubstring(Entry entry,
127                                          VirtualAttributeRule rule,
128                                          ByteString subInitial,
129                                          List<ByteString> subAny,
130                                          ByteString subFinal)
131  {
132    // DNs cannot be used in substring matching.
133    return ConditionResult.UNDEFINED;
134  }
135
136  @Override
137  public ConditionResult greaterThanOrEqualTo(Entry entry,
138                              VirtualAttributeRule rule,
139                              ByteString value)
140  {
141    // DNs cannot be used in ordering matching.
142    return ConditionResult.UNDEFINED;
143  }
144
145  @Override
146  public ConditionResult lessThanOrEqualTo(Entry entry,
147                              VirtualAttributeRule rule,
148                              ByteString value)
149  {
150    // DNs cannot be used in ordering matching.
151    return ConditionResult.UNDEFINED;
152  }
153
154  @Override
155  public ConditionResult approximatelyEqualTo(Entry entry,
156                              VirtualAttributeRule rule,
157                              ByteString value)
158  {
159    // DNs cannot be used in approximate matching.
160    return ConditionResult.UNDEFINED;
161  }
162
163  /**
164   * {@inheritDoc}.  This virtual attribute will support search operations only
165   * if one of the following is true about the search filter:
166   * <UL>
167   *   <LI>It is an equality filter targeting the associated attribute
168   *       type.</LI>
169   *   <LI>It is an AND filter in which at least one of the components is an
170   *       equality filter targeting the associated attribute type.</LI>
171   * </UL>
172   * Searching for this virtual attribute cannot be pre-indexed and thus,
173   * it should not be searchable when pre-indexed is required.
174   */
175  @Override
176  public boolean isSearchable(VirtualAttributeRule rule,
177                              SearchOperation searchOperation,
178                              boolean isPreIndexed)
179  {
180    return !isPreIndexed &&
181        isSearchable(rule.getAttributeType(), searchOperation.getFilter(), 0);
182  }
183
184  /**
185   * Indicates whether the provided search filter is one that may be used with
186   * this virtual attribute provider, optionally operating in a recursive manner
187   * to make the determination.
188   *
189   * @param  attributeType  The attribute type used to hold the entryDN value.
190   * @param  filter         The search filter for which to make the
191   *                        determination.
192   * @param  depth          The current recursion depth for this processing.
193   *
194   * @return  {@code true} if the provided filter may be used with this virtual
195   *          attribute provider, or {@code false} if not.
196   */
197  private boolean isSearchable(AttributeType attributeType, SearchFilter filter,
198                               int depth)
199  {
200    switch (filter.getFilterType())
201    {
202      case AND:
203        if (depth >= MAX_NESTED_FILTER_DEPTH)
204        {
205          return false;
206        }
207
208        for (SearchFilter f : filter.getFilterComponents())
209        {
210          if (isSearchable(attributeType, f, depth+1))
211          {
212            return true;
213          }
214        }
215        return false;
216
217      case EQUALITY:
218        return filter.getAttributeType().equals(attributeType);
219
220      default:
221        return false;
222    }
223  }
224
225  @Override
226  public void processSearch(VirtualAttributeRule rule,
227                            SearchOperation searchOperation)
228  {
229    Group<?> group = extractGroup(rule.getAttributeType(), searchOperation.getFilter());
230    if (group == null)
231    {
232      return;
233    }
234
235    try
236    {
237      // Check for nested groups to see if we need to keep track of returned entries
238      List<DN> nestedGroupsDNs = group.getNestedGroupDNs();
239      Set<ByteString> returnedDNs = null;
240      if (!nestedGroupsDNs.isEmpty())
241      {
242        returnedDNs = new HashSet<>();
243      }
244      if (!returnGroupMembers(searchOperation, group.getMembers(), returnedDNs))
245      {
246        return;
247      }
248      // Now check members of nested groups
249      for (DN dn : nestedGroupsDNs)
250      {
251        group = DirectoryServer.getGroupManager().getGroupInstance(dn);
252        if (!returnGroupMembers(searchOperation, group.getMembers(), returnedDNs))
253        {
254          return;
255        }
256      }
257    }
258    catch (DirectoryException de)
259    {
260      searchOperation.setResponseData(de);
261    }
262  }
263
264  /**
265   * @param searchOperation the search operation being processed.
266   * @param memberList the list of members of the group being processed.
267   * @param returnedDNs a set to store the normalized DNs of entries already returned,
268   *                    null if there's no need to track for entries.
269   * @return  <CODE>true</CODE> if the caller should continue processing the
270   *          search request and sending additional entries and references, or
271   *          <CODE>false</CODE> if not for some reason (e.g., the size limit
272   *          has been reached or the search has been abandoned).
273   * @throws DirectoryException If a problem occurs while attempting to send
274   *          the entry to the client and the search should be terminated.
275   */
276  private boolean returnGroupMembers(SearchOperation searchOperation,
277                                  MemberList memberList, Set<ByteString> returnedDNs)
278          throws DirectoryException
279  {
280    DN baseDN = searchOperation.getBaseDN();
281    SearchScope scope  = searchOperation.getScope();
282    SearchFilter filter = searchOperation.getFilter();
283    while (memberList.hasMoreMembers())
284    {
285      try
286      {
287        Entry e = memberList.nextMemberEntry();
288        if (e.matchesBaseAndScope(baseDN, scope)
289            && filter.matchesEntry(e)
290            // The set of returned DNs is only used for detecting set membership
291            // so it's ok to use the irreversible representation of the DN
292            && (returnedDNs == null || returnedDNs.add(e.getName().toNormalizedByteString()))
293            && !searchOperation.returnEntry(e, null))
294        {
295          return false;
296        }
297      }
298      catch (Exception e)
299      {
300        logger.traceException(e);
301      }
302    }
303    return true;
304  }
305
306  /**
307   * Extracts the first group DN encountered in the provided filter, operating
308   * recursively as necessary.
309   *
310   * @param  attributeType  The attribute type holding the entryDN value.
311   * @param  filter         The search filter to be processed.
312   *
313   * @return  The first group encountered in the provided filter, or
314   *          {@code null} if there is no match.
315   */
316  private Group<?> extractGroup(AttributeType attributeType,
317      SearchFilter filter)
318  {
319    switch (filter.getFilterType())
320    {
321      case AND:
322        for (SearchFilter f : filter.getFilterComponents())
323        {
324          Group<?> g = extractGroup(attributeType, f);
325          if (g != null)
326          {
327            return g;
328          }
329        }
330        break;
331
332      case EQUALITY:
333        if (filter.getAttributeType().equals(attributeType))
334        {
335          try
336          {
337            DN dn = DN.valueOf(filter.getAssertionValue());
338            return DirectoryServer.getGroupManager().getGroupInstance(dn);
339          }
340          catch (Exception e)
341          {
342            logger.traceException(e);
343          }
344        }
345        break;
346    }
347
348    return null;
349  }
350}