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