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 2006-2008 Sun Microsystems, Inc.
025 *      Portions Copyright 2014-2015 ForgeRock AS
026 */
027package org.opends.server.extensions;
028
029import java.util.ArrayList;
030import java.util.Collection;
031import java.util.Iterator;
032import java.util.LinkedHashSet;
033import java.util.LinkedList;
034import java.util.List;
035import java.util.Set;
036
037import org.forgerock.i18n.LocalizableMessage;
038import org.forgerock.opendj.config.server.ConfigChangeResult;
039import org.forgerock.opendj.config.server.ConfigException;
040import org.forgerock.opendj.ldap.ByteString;
041import org.forgerock.opendj.ldap.ResultCode;
042import org.forgerock.opendj.ldap.SearchScope;
043import org.opends.server.admin.server.ConfigurationChangeListener;
044import org.opends.server.admin.std.server.ExactMatchIdentityMapperCfg;
045import org.opends.server.admin.std.server.IdentityMapperCfg;
046import org.opends.server.api.Backend;
047import org.opends.server.api.IdentityMapper;
048import org.opends.server.core.DirectoryServer;
049import org.opends.server.protocols.internal.InternalClientConnection;
050import org.opends.server.protocols.internal.InternalSearchOperation;
051import org.opends.server.protocols.internal.SearchRequest;
052import static org.opends.server.protocols.internal.Requests.*;
053import org.opends.server.types.*;
054
055import static org.opends.messages.ExtensionMessages.*;
056import static org.opends.server.protocols.internal.InternalClientConnection.*;
057import static org.opends.server.util.CollectionUtils.*;
058
059/**
060 * This class provides an implementation of a Directory Server identity mapper
061 * that looks for the exact value provided as the ID string to appear in an
062 * attribute of a user's entry.  This mapper may be configured to look in one or
063 * more attributes using zero or more search bases.  In order for the mapping to
064 * be established properly, exactly one entry must have an attribute that
065 * exactly matches (according to the equality matching rule associated with that
066 * attribute) the ID value.
067 */
068public class ExactMatchIdentityMapper
069       extends IdentityMapper<ExactMatchIdentityMapperCfg>
070       implements ConfigurationChangeListener<
071                       ExactMatchIdentityMapperCfg>
072{
073  /** The set of attribute types to use when performing lookups. */
074  private AttributeType[] attributeTypes;
075
076  /** The DN of the configuration entry for this identity mapper. */
077  private DN configEntryDN;
078
079  /** The current configuration for this identity mapper. */
080  private ExactMatchIdentityMapperCfg currentConfig;
081
082  /** The set of attributes to return in search result entries. */
083  private LinkedHashSet<String> requestedAttributes;
084
085
086
087  /**
088   * Creates a new instance of this exact match identity mapper.  All
089   * initialization should be performed in the {@code initializeIdentityMapper}
090   * method.
091   */
092  public ExactMatchIdentityMapper()
093  {
094    super();
095
096    // Don't do any initialization here.
097  }
098
099
100
101  /** {@inheritDoc} */
102  @Override
103  public void initializeIdentityMapper(
104                   ExactMatchIdentityMapperCfg configuration)
105         throws ConfigException, InitializationException
106  {
107    configuration.addExactMatchChangeListener(this);
108
109    currentConfig = configuration;
110    configEntryDN = currentConfig.dn();
111
112
113    // Get the attribute types to use for the searches.  Ensure that they are
114    // all indexed for equality.
115    attributeTypes =
116         currentConfig.getMatchAttribute().toArray(new AttributeType[0]);
117
118    Set<DN> cfgBaseDNs = configuration.getMatchBaseDN();
119    if (cfgBaseDNs == null || cfgBaseDNs.isEmpty())
120    {
121      cfgBaseDNs = DirectoryServer.getPublicNamingContexts().keySet();
122    }
123
124    for (AttributeType t : attributeTypes)
125    {
126      for (DN baseDN : cfgBaseDNs)
127      {
128        Backend b = DirectoryServer.getBackend(baseDN);
129        if (b != null && ! b.isIndexed(t, IndexType.EQUALITY))
130        {
131          throw new ConfigException(ERR_EXACTMAP_ATTR_UNINDEXED.get(
132              configuration.dn(), t.getNameOrOID(), b.getBackendID()));
133        }
134      }
135    }
136
137
138    // Create the attribute list to include in search requests.  We want to
139    // include all user and operational attributes.
140    requestedAttributes = newLinkedHashSet("*", "+");
141  }
142
143
144
145  /**
146   * Performs any finalization that may be necessary for this identity mapper.
147   */
148  @Override
149  public void finalizeIdentityMapper()
150  {
151    currentConfig.removeExactMatchChangeListener(this);
152  }
153
154
155
156  /**
157   * Retrieves the user entry that was mapped to the provided identification
158   * string.
159   *
160   * @param  id  The identification string that is to be mapped to a user.
161   *
162   * @return  The user entry that was mapped to the provided identification, or
163   *          <CODE>null</CODE> if no users were found that could be mapped to
164   *          the provided ID.
165   *
166   * @throws  DirectoryException  If a problem occurs while attempting to map
167   *                              the given ID to a user entry, or if there are
168   *                              multiple user entries that could map to the
169   *                              provided ID.
170   */
171  @Override
172  public Entry getEntryForID(String id)
173         throws DirectoryException
174  {
175    ExactMatchIdentityMapperCfg config = currentConfig;
176    AttributeType[] attributeTypes = this.attributeTypes;
177
178
179    // Construct the search filter to use to make the determination.
180    SearchFilter filter;
181    if (attributeTypes.length == 1)
182    {
183      ByteString value = ByteString.valueOfUtf8(id);
184      filter = SearchFilter.createEqualityFilter(attributeTypes[0], value);
185    }
186    else
187    {
188      ArrayList<SearchFilter> filterComps = new ArrayList<>(attributeTypes.length);
189      for (AttributeType t : attributeTypes)
190      {
191        ByteString value = ByteString.valueOfUtf8(id);
192        filterComps.add(SearchFilter.createEqualityFilter(t, value));
193      }
194
195      filter = SearchFilter.createORFilter(filterComps);
196    }
197
198
199    // Iterate through the set of search bases and process an internal search
200    // to find any matching entries.  Since we'll only allow a single match,
201    // then use size and time limits to constrain costly searches resulting from
202    // non-unique or inefficient criteria.
203    Collection<DN> baseDNs = config.getMatchBaseDN();
204    if (baseDNs == null || baseDNs.isEmpty())
205    {
206      baseDNs = DirectoryServer.getPublicNamingContexts().keySet();
207    }
208
209    SearchResultEntry matchingEntry = null;
210    InternalClientConnection conn = getRootConnection();
211    for (DN baseDN : baseDNs)
212    {
213      final SearchRequest request = newSearchRequest(baseDN, SearchScope.WHOLE_SUBTREE, filter)
214          .setSizeLimit(1)
215          .setTimeLimit(10)
216          .addAttribute(requestedAttributes);
217      InternalSearchOperation internalSearch = conn.processSearch(request);
218
219      switch (internalSearch.getResultCode().asEnum())
220      {
221        case SUCCESS:
222          // This is fine.  No action needed.
223          break;
224
225        case NO_SUCH_OBJECT:
226          // The search base doesn't exist.  Not an ideal situation, but we'll
227          // ignore it.
228          break;
229
230        case SIZE_LIMIT_EXCEEDED:
231          // Multiple entries matched the filter.  This is not acceptable.
232          LocalizableMessage message = ERR_EXACTMAP_MULTIPLE_MATCHING_ENTRIES.get(id);
233          throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message);
234
235        case TIME_LIMIT_EXCEEDED:
236        case ADMIN_LIMIT_EXCEEDED:
237          // The search criteria was too inefficient.
238          message = ERR_EXACTMAP_INEFFICIENT_SEARCH.
239              get(id, internalSearch.getErrorMessage());
240          throw new DirectoryException(internalSearch.getResultCode(), message);
241
242        default:
243          // Just pass on the failure that was returned for this search.
244          message = ERR_EXACTMAP_SEARCH_FAILED.
245              get(id, internalSearch.getErrorMessage());
246          throw new DirectoryException(internalSearch.getResultCode(), message);
247      }
248
249      LinkedList<SearchResultEntry> searchEntries = internalSearch.getSearchEntries();
250      if (searchEntries != null && ! searchEntries.isEmpty())
251      {
252        if (matchingEntry == null)
253        {
254          Iterator<SearchResultEntry> iterator = searchEntries.iterator();
255          matchingEntry = iterator.next();
256          if (iterator.hasNext())
257          {
258            LocalizableMessage message = ERR_EXACTMAP_MULTIPLE_MATCHING_ENTRIES.get(id);
259            throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message);
260          }
261        }
262        else
263        {
264          LocalizableMessage message = ERR_EXACTMAP_MULTIPLE_MATCHING_ENTRIES.get(id);
265          throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message);
266        }
267      }
268    }
269
270    return matchingEntry;
271  }
272
273
274
275  /** {@inheritDoc} */
276  @Override
277  public boolean isConfigurationAcceptable(IdentityMapperCfg configuration,
278                                           List<LocalizableMessage> unacceptableReasons)
279  {
280    ExactMatchIdentityMapperCfg config =
281         (ExactMatchIdentityMapperCfg) configuration;
282    return isConfigurationChangeAcceptable(config, unacceptableReasons);
283  }
284
285
286
287  /** {@inheritDoc} */
288  @Override
289  public boolean isConfigurationChangeAcceptable(
290                      ExactMatchIdentityMapperCfg configuration,
291                      List<LocalizableMessage> unacceptableReasons)
292  {
293    boolean configAcceptable = true;
294
295    // Make sure that all of the configured attributes are indexed for equality
296    // in all appropriate backends.
297    Set<DN> cfgBaseDNs = configuration.getMatchBaseDN();
298    if (cfgBaseDNs == null || cfgBaseDNs.isEmpty())
299    {
300      cfgBaseDNs = DirectoryServer.getPublicNamingContexts().keySet();
301    }
302
303    for (AttributeType t : configuration.getMatchAttribute())
304    {
305      for (DN baseDN : cfgBaseDNs)
306      {
307        Backend b = DirectoryServer.getBackend(baseDN);
308        if (b != null && ! b.isIndexed(t, IndexType.EQUALITY))
309        {
310          unacceptableReasons.add(ERR_EXACTMAP_ATTR_UNINDEXED.get(
311              configuration.dn(), t.getNameOrOID(), b.getBackendID()));
312          configAcceptable = false;
313        }
314      }
315    }
316
317    return configAcceptable;
318  }
319
320
321
322  /** {@inheritDoc} */
323  @Override
324  public ConfigChangeResult applyConfigurationChange(
325              ExactMatchIdentityMapperCfg configuration)
326  {
327    final ConfigChangeResult ccr = new ConfigChangeResult();
328
329    attributeTypes =
330         configuration.getMatchAttribute().toArray(new AttributeType[0]);
331    currentConfig = configuration;
332
333   return ccr;
334  }
335}