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