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