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}