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.forgerock.opendj.config.server.ConfigurationChangeListener;
035import org.forgerock.opendj.server.config.server.ExactMatchIdentityMapperCfg;
036import org.forgerock.opendj.server.config.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   * Creates a new instance of this exact match identity mapper.  All
079   * initialization should be performed in the {@code initializeIdentityMapper}
080   * method.
081   */
082  public ExactMatchIdentityMapper()
083  {
084    super();
085
086    // Don't do any initialization here.
087  }
088
089  @Override
090  public void initializeIdentityMapper(
091                   ExactMatchIdentityMapperCfg configuration)
092         throws ConfigException, InitializationException
093  {
094    configuration.addExactMatchChangeListener(this);
095
096    currentConfig = configuration;
097    configEntryDN = currentConfig.dn();
098
099    // Get the attribute types to use for the searches.  Ensure that they are
100    // all indexed for equality.
101    attributeTypes =
102         currentConfig.getMatchAttribute().toArray(new AttributeType[0]);
103
104    Set<DN> cfgBaseDNs = configuration.getMatchBaseDN();
105    if (cfgBaseDNs == null || cfgBaseDNs.isEmpty())
106    {
107      cfgBaseDNs = DirectoryServer.getPublicNamingContexts().keySet();
108    }
109
110    for (AttributeType t : attributeTypes)
111    {
112      for (DN baseDN : cfgBaseDNs)
113      {
114        Backend b = DirectoryServer.getBackend(baseDN);
115        if (b != null && ! b.isIndexed(t, IndexType.EQUALITY))
116        {
117          throw new ConfigException(ERR_EXACTMAP_ATTR_UNINDEXED.get(
118              configuration.dn(), t.getNameOrOID(), b.getBackendID()));
119        }
120      }
121    }
122
123    // Create the attribute list to include in search requests.  We want to
124    // include all user and operational attributes.
125    requestedAttributes = newLinkedHashSet("*", "+");
126  }
127
128  @Override
129  public void finalizeIdentityMapper()
130  {
131    currentConfig.removeExactMatchChangeListener(this);
132  }
133
134  /**
135   * Retrieves the user entry that was mapped to the provided identification
136   * string.
137   *
138   * @param  id  The identification string that is to be mapped to a user.
139   *
140   * @return  The user entry that was mapped to the provided identification, or
141   *          <CODE>null</CODE> if no users were found that could be mapped to
142   *          the provided ID.
143   *
144   * @throws  DirectoryException  If a problem occurs while attempting to map
145   *                              the given ID to a user entry, or if there are
146   *                              multiple user entries that could map to the
147   *                              provided ID.
148   */
149  @Override
150  public Entry getEntryForID(String id)
151         throws DirectoryException
152  {
153    ExactMatchIdentityMapperCfg config = currentConfig;
154    AttributeType[] attributeTypes = this.attributeTypes;
155
156    // Construct the search filter to use to make the determination.
157    SearchFilter filter;
158    if (attributeTypes.length == 1)
159    {
160      ByteString value = ByteString.valueOfUtf8(id);
161      filter = SearchFilter.createEqualityFilter(attributeTypes[0], value);
162    }
163    else
164    {
165      ArrayList<SearchFilter> filterComps = new ArrayList<>(attributeTypes.length);
166      for (AttributeType t : attributeTypes)
167      {
168        ByteString value = ByteString.valueOfUtf8(id);
169        filterComps.add(SearchFilter.createEqualityFilter(t, value));
170      }
171
172      filter = SearchFilter.createORFilter(filterComps);
173    }
174
175    // Iterate through the set of search bases and process an internal search
176    // to find any matching entries.  Since we'll only allow a single match,
177    // then use size and time limits to constrain costly searches resulting from
178    // non-unique or inefficient criteria.
179    Collection<DN> baseDNs = config.getMatchBaseDN();
180    if (baseDNs == null || baseDNs.isEmpty())
181    {
182      baseDNs = DirectoryServer.getPublicNamingContexts().keySet();
183    }
184
185    SearchResultEntry matchingEntry = null;
186    InternalClientConnection conn = getRootConnection();
187    for (DN baseDN : baseDNs)
188    {
189      final SearchRequest request = newSearchRequest(baseDN, SearchScope.WHOLE_SUBTREE, filter)
190          .setSizeLimit(1)
191          .setTimeLimit(10)
192          .addAttribute(requestedAttributes);
193      InternalSearchOperation internalSearch = conn.processSearch(request);
194
195      switch (internalSearch.getResultCode().asEnum())
196      {
197        case SUCCESS:
198          // This is fine.  No action needed.
199          break;
200
201        case NO_SUCH_OBJECT:
202          // The search base doesn't exist.  Not an ideal situation, but we'll
203          // ignore it.
204          break;
205
206        case SIZE_LIMIT_EXCEEDED:
207          // Multiple entries matched the filter.  This is not acceptable.
208          LocalizableMessage message = ERR_EXACTMAP_MULTIPLE_MATCHING_ENTRIES.get(id);
209          throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message);
210
211        case TIME_LIMIT_EXCEEDED:
212        case ADMIN_LIMIT_EXCEEDED:
213          // The search criteria was too inefficient.
214          message = ERR_EXACTMAP_INEFFICIENT_SEARCH.
215              get(id, internalSearch.getErrorMessage());
216          throw new DirectoryException(internalSearch.getResultCode(), message);
217
218        default:
219          // Just pass on the failure that was returned for this search.
220          message = ERR_EXACTMAP_SEARCH_FAILED.
221              get(id, internalSearch.getErrorMessage());
222          throw new DirectoryException(internalSearch.getResultCode(), message);
223      }
224
225      LinkedList<SearchResultEntry> searchEntries = internalSearch.getSearchEntries();
226      if (searchEntries != null && ! searchEntries.isEmpty())
227      {
228        if (matchingEntry == null)
229        {
230          Iterator<SearchResultEntry> iterator = searchEntries.iterator();
231          matchingEntry = iterator.next();
232          if (iterator.hasNext())
233          {
234            LocalizableMessage message = ERR_EXACTMAP_MULTIPLE_MATCHING_ENTRIES.get(id);
235            throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message);
236          }
237        }
238        else
239        {
240          LocalizableMessage message = ERR_EXACTMAP_MULTIPLE_MATCHING_ENTRIES.get(id);
241          throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message);
242        }
243      }
244    }
245
246    return matchingEntry;
247  }
248
249  @Override
250  public boolean isConfigurationAcceptable(IdentityMapperCfg configuration,
251                                           List<LocalizableMessage> unacceptableReasons)
252  {
253    ExactMatchIdentityMapperCfg config =
254         (ExactMatchIdentityMapperCfg) configuration;
255    return isConfigurationChangeAcceptable(config, unacceptableReasons);
256  }
257
258  @Override
259  public boolean isConfigurationChangeAcceptable(
260                      ExactMatchIdentityMapperCfg configuration,
261                      List<LocalizableMessage> unacceptableReasons)
262  {
263    boolean configAcceptable = true;
264
265    // Make sure that all of the configured attributes are indexed for equality
266    // in all appropriate backends.
267    Set<DN> cfgBaseDNs = configuration.getMatchBaseDN();
268    if (cfgBaseDNs == null || cfgBaseDNs.isEmpty())
269    {
270      cfgBaseDNs = DirectoryServer.getPublicNamingContexts().keySet();
271    }
272
273    for (AttributeType t : configuration.getMatchAttribute())
274    {
275      for (DN baseDN : cfgBaseDNs)
276      {
277        Backend b = DirectoryServer.getBackend(baseDN);
278        if (b != null && ! b.isIndexed(t, IndexType.EQUALITY))
279        {
280          unacceptableReasons.add(ERR_EXACTMAP_ATTR_UNINDEXED.get(
281              configuration.dn(), t.getNameOrOID(), b.getBackendID()));
282          configAcceptable = false;
283        }
284      }
285    }
286
287    return configAcceptable;
288  }
289
290  @Override
291  public ConfigChangeResult applyConfigurationChange(
292              ExactMatchIdentityMapperCfg configuration)
293  {
294    final ConfigChangeResult ccr = new ConfigChangeResult();
295
296    attributeTypes =
297         configuration.getMatchAttribute().toArray(new AttributeType[0]);
298    currentConfig = configuration;
299
300   return ccr;
301  }
302}