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}