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