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