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