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