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