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.forgerock.opendj.config.server.ConfigurationChangeListener; 043import org.forgerock.opendj.server.config.server.IdentityMapperCfg; 044import org.forgerock.opendj.server.config.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 * Creates a new instance of this regular expression identity mapper. All 089 * initialization should be performed in the {@code initializeIdentityMapper} 090 * method. 091 */ 092 public RegularExpressionIdentityMapper() 093 { 094 super(); 095 096 // Don't do any initialization here. 097 } 098 099 @Override 100 public void initializeIdentityMapper( 101 RegularExpressionIdentityMapperCfg configuration) 102 throws ConfigException, InitializationException 103 { 104 configuration.addRegularExpressionChangeListener(this); 105 106 currentConfig = configuration; 107 configEntryDN = currentConfig.dn(); 108 109 try 110 { 111 matchPattern = Pattern.compile(currentConfig.getMatchPattern()); 112 } 113 catch (PatternSyntaxException pse) { 114 LocalizableMessage message = ERR_REGEXMAP_INVALID_MATCH_PATTERN.get( 115 currentConfig.getMatchPattern(), 116 pse.getMessage()); 117 throw new ConfigException(message, pse); 118 } 119 120 replacePattern = currentConfig.getReplacePattern(); 121 if (replacePattern == null) 122 { 123 replacePattern = ""; 124 } 125 126 // Get the attribute types to use for the searches. Ensure that they are 127 // all indexed for equality. 128 attributeTypes = 129 currentConfig.getMatchAttribute().toArray(new AttributeType[0]); 130 131 Set<DN> cfgBaseDNs = configuration.getMatchBaseDN(); 132 if (cfgBaseDNs == null || cfgBaseDNs.isEmpty()) 133 { 134 cfgBaseDNs = DirectoryServer.getPublicNamingContexts().keySet(); 135 } 136 137 for (AttributeType t : attributeTypes) 138 { 139 for (DN baseDN : cfgBaseDNs) 140 { 141 Backend b = DirectoryServer.getBackend(baseDN); 142 if (b != null && ! b.isIndexed(t, IndexType.EQUALITY)) 143 { 144 throw new ConfigException(ERR_REGEXMAP_ATTR_UNINDEXED.get( 145 configuration.dn(), t.getNameOrOID(), b.getBackendID())); 146 } 147 } 148 } 149 150 // Create the attribute list to include in search requests. We want to 151 // include all user and operational attributes. 152 requestedAttributes = newLinkedHashSet("*", "+"); 153 } 154 155 @Override 156 public void finalizeIdentityMapper() 157 { 158 currentConfig.removeRegularExpressionChangeListener(this); 159 } 160 161 @Override 162 public Entry getEntryForID(String id) 163 throws DirectoryException 164 { 165 RegularExpressionIdentityMapperCfg config = currentConfig; 166 AttributeType[] attributeTypes = this.attributeTypes; 167 168 // Run the provided identifier string through the regular expression pattern 169 // matcher and make the appropriate replacement. 170 Matcher matcher = matchPattern.matcher(id); 171 String processedID = matcher.replaceAll(replacePattern); 172 173 // Construct the search filter to use to make the determination. 174 SearchFilter filter; 175 if (attributeTypes.length == 1) 176 { 177 ByteString value = ByteString.valueOfUtf8(processedID); 178 filter = SearchFilter.createEqualityFilter(attributeTypes[0], value); 179 } 180 else 181 { 182 ArrayList<SearchFilter> filterComps = new ArrayList<>(attributeTypes.length); 183 for (AttributeType t : attributeTypes) 184 { 185 ByteString value = ByteString.valueOfUtf8(processedID); 186 filterComps.add(SearchFilter.createEqualityFilter(t, value)); 187 } 188 189 filter = SearchFilter.createORFilter(filterComps); 190 } 191 192 // Iterate through the set of search bases and process an internal search 193 // to find any matching entries. Since we'll only allow a single match, 194 // then use size and time limits to constrain costly searches resulting from 195 // non-unique or inefficient criteria. 196 Collection<DN> baseDNs = config.getMatchBaseDN(); 197 if (baseDNs == null || baseDNs.isEmpty()) 198 { 199 baseDNs = DirectoryServer.getPublicNamingContexts().keySet(); 200 } 201 202 SearchResultEntry matchingEntry = null; 203 InternalClientConnection conn = getRootConnection(); 204 for (DN baseDN : baseDNs) 205 { 206 final SearchRequest request = newSearchRequest(baseDN, SearchScope.WHOLE_SUBTREE, filter) 207 .setSizeLimit(1) 208 .setTimeLimit(10) 209 .addAttribute(requestedAttributes); 210 InternalSearchOperation internalSearch = conn.processSearch(request); 211 212 switch (internalSearch.getResultCode().asEnum()) 213 { 214 case SUCCESS: 215 // This is fine. No action needed. 216 break; 217 218 case NO_SUCH_OBJECT: 219 // The search base doesn't exist. Not an ideal situation, but we'll 220 // ignore it. 221 break; 222 223 case SIZE_LIMIT_EXCEEDED: 224 // Multiple entries matched the filter. This is not acceptable. 225 LocalizableMessage message = ERR_REGEXMAP_MULTIPLE_MATCHING_ENTRIES.get(processedID); 226 throw new DirectoryException( 227 ResultCode.CONSTRAINT_VIOLATION, message); 228 229 case TIME_LIMIT_EXCEEDED: 230 case ADMIN_LIMIT_EXCEEDED: 231 // The search criteria was too inefficient. 232 message = ERR_REGEXMAP_INEFFICIENT_SEARCH.get(processedID, internalSearch.getErrorMessage()); 233 throw new DirectoryException(internalSearch.getResultCode(), message); 234 235 default: 236 // Just pass on the failure that was returned for this search. 237 message = ERR_REGEXMAP_SEARCH_FAILED.get(processedID, internalSearch.getErrorMessage()); 238 throw new DirectoryException(internalSearch.getResultCode(), message); 239 } 240 241 LinkedList<SearchResultEntry> searchEntries = 242 internalSearch.getSearchEntries(); 243 if (searchEntries != null && ! searchEntries.isEmpty()) 244 { 245 if (matchingEntry == null) 246 { 247 Iterator<SearchResultEntry> iterator = searchEntries.iterator(); 248 matchingEntry = iterator.next(); 249 if (iterator.hasNext()) 250 { 251 LocalizableMessage message = ERR_REGEXMAP_MULTIPLE_MATCHING_ENTRIES.get(processedID); 252 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message); 253 } 254 } 255 else 256 { 257 LocalizableMessage message = ERR_REGEXMAP_MULTIPLE_MATCHING_ENTRIES.get(processedID); 258 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message); 259 } 260 } 261 } 262 263 return matchingEntry; 264 } 265 266 @Override 267 public boolean isConfigurationAcceptable(IdentityMapperCfg configuration, 268 List<LocalizableMessage> unacceptableReasons) 269 { 270 RegularExpressionIdentityMapperCfg config = 271 (RegularExpressionIdentityMapperCfg) configuration; 272 return isConfigurationChangeAcceptable(config, unacceptableReasons); 273 } 274 275 @Override 276 public boolean isConfigurationChangeAcceptable( 277 RegularExpressionIdentityMapperCfg configuration, 278 List<LocalizableMessage> unacceptableReasons) 279 { 280 boolean configAcceptable = true; 281 282 // Make sure that all of the configured attributes are indexed for equality 283 // in all appropriate backends. 284 Set<DN> cfgBaseDNs = configuration.getMatchBaseDN(); 285 if (cfgBaseDNs == null || cfgBaseDNs.isEmpty()) 286 { 287 cfgBaseDNs = DirectoryServer.getPublicNamingContexts().keySet(); 288 } 289 290 for (AttributeType t : configuration.getMatchAttribute()) 291 { 292 for (DN baseDN : cfgBaseDNs) 293 { 294 Backend b = DirectoryServer.getBackend(baseDN); 295 if (b != null && ! b.isIndexed(t, IndexType.EQUALITY)) 296 { 297 unacceptableReasons.add(ERR_REGEXMAP_ATTR_UNINDEXED.get( 298 configuration.dn(), t.getNameOrOID(), b.getBackendID())); 299 configAcceptable = false; 300 } 301 } 302 } 303 304 // Make sure that we can parse the match pattern. 305 try 306 { 307 Pattern.compile(configuration.getMatchPattern()); 308 } 309 catch (PatternSyntaxException pse) 310 { 311 unacceptableReasons.add(ERR_REGEXMAP_INVALID_MATCH_PATTERN.get( 312 configuration.getMatchPattern(), pse.getMessage())); 313 configAcceptable = false; 314 } 315 316 return configAcceptable; 317 } 318 319 @Override 320 public ConfigChangeResult applyConfigurationChange( 321 RegularExpressionIdentityMapperCfg configuration) 322 { 323 final ConfigChangeResult ccr = new ConfigChangeResult(); 324 325 Pattern newMatchPattern = null; 326 try 327 { 328 newMatchPattern = Pattern.compile(configuration.getMatchPattern()); 329 } 330 catch (PatternSyntaxException pse) 331 { 332 ccr.addMessage(ERR_REGEXMAP_INVALID_MATCH_PATTERN.get(configuration.getMatchPattern(), pse.getMessage())); 333 ccr.setResultCode(ResultCode.CONSTRAINT_VIOLATION); 334 } 335 336 String newReplacePattern = configuration.getReplacePattern(); 337 if (newReplacePattern == null) 338 { 339 newReplacePattern = ""; 340 } 341 342 AttributeType[] newAttributeTypes = 343 configuration.getMatchAttribute().toArray(new AttributeType[0]); 344 345 if (ccr.getResultCode() == ResultCode.SUCCESS) 346 { 347 attributeTypes = newAttributeTypes; 348 currentConfig = configuration; 349 matchPattern = newMatchPattern; 350 replacePattern = newReplacePattern; 351 } 352 353 return ccr; 354 } 355}