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 2016 ForgeRock AS. 015 */ 016package org.forgerock.opendj.security; 017 018import static java.util.Collections.singletonList; 019import static java.util.Collections.synchronizedMap; 020import static org.forgerock.opendj.ldap.Connections.newCachedConnectionPool; 021import static org.forgerock.opendj.ldap.Connections.newInternalConnectionFactory; 022import static org.forgerock.opendj.ldap.LDAPConnectionFactory.AUTHN_BIND_REQUEST; 023import static org.forgerock.opendj.ldap.LdapException.newLdapException; 024import static org.forgerock.opendj.ldap.requests.Requests.newSimpleBindRequest; 025import static org.forgerock.opendj.ldif.LDIF.copyTo; 026import static org.forgerock.opendj.ldif.LDIF.newEntryIteratorReader; 027import static org.forgerock.opendj.security.KeyStoreParameters.GLOBAL_PASSWORD; 028import static org.forgerock.opendj.security.KeyStoreParameters.newKeyStoreParameters; 029import static org.forgerock.opendj.security.OpenDJProviderSchema.SCHEMA; 030import static org.forgerock.util.Options.defaultOptions; 031 032import java.io.BufferedWriter; 033import java.io.File; 034import java.io.FileReader; 035import java.io.FileWriter; 036import java.io.IOException; 037import java.io.InputStreamReader; 038import java.io.Reader; 039import java.net.URI; 040import java.security.AccessController; 041import java.security.GeneralSecurityException; 042import java.security.KeyStore; 043import java.security.PrivilegedAction; 044import java.security.Provider; 045import java.security.ProviderException; 046import java.util.LinkedHashMap; 047import java.util.Map; 048import java.util.Properties; 049 050import org.forgerock.opendj.ldap.ConnectionFactory; 051import org.forgerock.opendj.ldap.DN; 052import org.forgerock.opendj.ldap.IntermediateResponseHandler; 053import org.forgerock.opendj.ldap.LDAPConnectionFactory; 054import org.forgerock.opendj.ldap.LdapException; 055import org.forgerock.opendj.ldap.LdapResultHandler; 056import org.forgerock.opendj.ldap.MemoryBackend; 057import org.forgerock.opendj.ldap.RequestContext; 058import org.forgerock.opendj.ldap.RequestHandler; 059import org.forgerock.opendj.ldap.ResultCode; 060import org.forgerock.opendj.ldap.SearchResultHandler; 061import org.forgerock.opendj.ldap.requests.AddRequest; 062import org.forgerock.opendj.ldap.requests.BindRequest; 063import org.forgerock.opendj.ldap.requests.CompareRequest; 064import org.forgerock.opendj.ldap.requests.DeleteRequest; 065import org.forgerock.opendj.ldap.requests.ExtendedRequest; 066import org.forgerock.opendj.ldap.requests.ModifyDNRequest; 067import org.forgerock.opendj.ldap.requests.ModifyRequest; 068import org.forgerock.opendj.ldap.requests.SearchRequest; 069import org.forgerock.opendj.ldap.responses.BindResult; 070import org.forgerock.opendj.ldap.responses.CompareResult; 071import org.forgerock.opendj.ldap.responses.ExtendedResult; 072import org.forgerock.opendj.ldap.responses.Result; 073import org.forgerock.opendj.ldif.LDIFEntryReader; 074import org.forgerock.opendj.ldif.LDIFEntryWriter; 075import org.forgerock.util.Factory; 076import org.forgerock.util.Options; 077 078/** 079 * The OpenDJ LDAP security provider which exposes an LDAP/LDIF based {@link java.security.KeyStore KeyStore} 080 * service, as well as providing utility methods facilitating construction of LDAP/LDIF based key stores. See the 081 * package documentation for more information. 082 */ 083public final class OpenDJProvider extends Provider { 084 private static final long serialVersionUID = -1; 085 // Security provider configuration property names. 086 private static final String PREFIX = "org.forgerock.opendj.security."; 087 private static final String LDIF_PROPERTY = PREFIX + "ldif"; 088 private static final String HOST_PROPERTY = PREFIX + "host"; 089 private static final String PORT_PROPERTY = PREFIX + "port"; 090 private static final String BIND_DN_PROPERTY = PREFIX + "bindDN"; 091 private static final String BIND_PASSWORD_PROPERTY = PREFIX + "bindPassword"; 092 private static final String KEYSTORE_BASE_DN_PROPERTY = PREFIX + "keyStoreBaseDN"; 093 private static final String KEYSTORE_PASSWORD_PROPERTY = PREFIX + "keyStorePassword"; 094 // Default key store configuration or null if key stores need explicit configuration. 095 private final KeyStoreParameters defaultConfig; 096 097 /** Creates a default LDAP security provider with no default key store configuration. */ 098 public OpenDJProvider() { 099 this((KeyStoreParameters) null); 100 } 101 102 /** 103 * Creates a LDAP security provider with provided default key store configuration. 104 * 105 * @param configFile 106 * The configuration file, which may be {@code null} indicating that key stores will be configured when they 107 * are instantiated. 108 */ 109 public OpenDJProvider(final String configFile) { 110 this(new File(configFile).toURI()); 111 } 112 113 /** 114 * Creates a LDAP security provider with provided default key store configuration. 115 * 116 * @param configFile 117 * The configuration file, which may be {@code null} indicating that key stores will be configured when they 118 * are instantiated. 119 */ 120 public OpenDJProvider(final URI configFile) { 121 this(configFile != null ? parseConfig(configFile) : null); 122 } 123 124 OpenDJProvider(final KeyStoreParameters defaultConfig) { 125 super("OpenDJ", 1.0D, "OpenDJ LDAP security provider"); 126 this.defaultConfig = defaultConfig; 127 AccessController.doPrivileged(new PrivilegedAction<Void>() { 128 public Void run() { 129 putService(new KeyStoreService()); 130 return null; 131 } 132 }); 133 } 134 135 /** 136 * Creates a new LDAP key store with default options. The returned key store will already have been 137 * {@link KeyStore#load(KeyStore.LoadStoreParameter) loaded}. 138 * 139 * @param factory 140 * The LDAP connection factory. 141 * @param baseDN 142 * The DN of the subtree containing the LDAP key store. 143 * @return The LDAP key store. 144 */ 145 public static KeyStore newLDAPKeyStore(final ConnectionFactory factory, final DN baseDN) { 146 return newLDAPKeyStore(factory, baseDN, defaultOptions()); 147 } 148 149 /** 150 * Creates a new LDAP key store with custom options. The returned key store will already have been 151 * {@link KeyStore#load(KeyStore.LoadStoreParameter) loaded}. 152 * 153 * @param factory 154 * The LDAP connection factory. 155 * @param baseDN 156 * The DN of the subtree containing the LDAP key store. 157 * @param options 158 * The optional key store parameters, including the cache configuration, key store password, and crypto 159 * parameters. 160 * @return The LDAP key store. 161 * @see KeyStoreParameters For the list of available key store options. 162 */ 163 public static KeyStore newLDAPKeyStore(final ConnectionFactory factory, final DN baseDN, final Options options) { 164 try { 165 final KeyStore keyStore = KeyStore.getInstance("LDAP", new OpenDJProvider()); 166 keyStore.load(newKeyStoreParameters(factory, baseDN, options)); 167 return keyStore; 168 } catch (GeneralSecurityException | IOException e) { 169 // Should not happen. 170 throw new RuntimeException(e); 171 } 172 } 173 174 /** 175 * Creates a new LDIF based key store which will read and write key store objects to the provided key store file. 176 * The LDIF file will be read during construction and re-written after each update. The returned key store will 177 * already have been {@link KeyStore#load(KeyStore.LoadStoreParameter) loaded}. 178 * 179 * @param ldifFile 180 * The name of the LDIF file containing the key store objects. 181 * @param baseDN 182 * The DN of the subtree containing the LDAP key store. 183 * @return The LDIF key store. 184 * @throws IOException 185 * If an error occurred while reading the LDIF file. 186 */ 187 public static KeyStore newLDIFKeyStore(final File ldifFile, final DN baseDN) throws IOException { 188 return newLDIFKeyStore(ldifFile, baseDN, defaultOptions()); 189 } 190 191 /** 192 * Creates a new LDIF based key store which will read and write key store objects to the provided key store file. 193 * The LDIF file will be read during construction and re-written after each update. The returned key store will 194 * already have been {@link KeyStore#load(KeyStore.LoadStoreParameter) loaded}. 195 * 196 * @param ldifFile 197 * The name of the LDIF file containing the key store objects. 198 * @param baseDN 199 * The DN of the subtree containing the LDAP key store. 200 * @param options 201 * The optional key store parameters, including the cache configuration, key store password, and crypto 202 * parameters. 203 * @return The LDIF key store. 204 * @throws IOException 205 * If an error occurred while reading the LDIF file. 206 */ 207 public static KeyStore newLDIFKeyStore(final File ldifFile, final DN baseDN, final Options options) 208 throws IOException { 209 return newLDAPKeyStore(newLDIFConnectionFactory(ldifFile), baseDN, options); 210 } 211 212 private static ConnectionFactory newLDIFConnectionFactory(final File ldifFile) throws IOException { 213 try (LDIFEntryReader reader = new LDIFEntryReader(new FileReader(ldifFile)).setSchema(SCHEMA)) { 214 final MemoryBackend backend = new MemoryBackend(SCHEMA, reader).enableVirtualAttributes(true); 215 return newInternalConnectionFactory(new WriteLDIFOnUpdateRequestHandler(backend, ldifFile)); 216 } 217 } 218 219 /** 220 * Creates a new key store object cache which will delegate to the provided {@link Map}. It is the responsibility 221 * of the map implementation to perform cache eviction if needed. The provided map MUST be thread-safe. 222 * 223 * @param map 224 * The thread-safe {@link Map} implementation in which key store objects will be stored. 225 * @return The new key store object cache. 226 */ 227 public static KeyStoreObjectCache newKeyStoreObjectCacheFromMap(final Map<String, KeyStoreObject> map) { 228 return new KeyStoreObjectCache() { 229 @Override 230 public void put(final KeyStoreObject keyStoreObject) { 231 map.put(keyStoreObject.getAlias(), keyStoreObject); 232 } 233 234 @Override 235 public KeyStoreObject get(final String alias) { 236 return map.get(alias); 237 } 238 }; 239 } 240 241 /** 242 * Creates a new fixed capacity key store object cache which will evict objects once it reaches the 243 * provided capacity. This implementation is only intended for simple use cases and is not particularly scalable. 244 * 245 * @param capacity 246 * The maximum number of key store objects that will be cached before eviction occurs. 247 * @return The new key store object cache. 248 */ 249 public static KeyStoreObjectCache newCapacityBasedKeyStoreObjectCache(final int capacity) { 250 return newKeyStoreObjectCacheFromMap(synchronizedMap(new LinkedHashMap<String, KeyStoreObject>() { 251 private static final long serialVersionUID = -1; 252 253 @Override 254 protected boolean removeEldestEntry(final Map.Entry<String, KeyStoreObject> eldest) { 255 return size() > capacity; 256 } 257 })); 258 } 259 260 /** 261 * Returns a password factory which will return a copy of the provided password for each invocation of 262 * {@link Factory#newInstance()}, and which does not provide any protection of the in memory representation of 263 * the password. 264 * 265 * @param password 266 * The password or {@code null} if no password should ever be returned. 267 * @return A password factory which will return a copy of the provided password. 268 */ 269 public static Factory<char[]> newClearTextPasswordFactory(final char[] password) { 270 return new Factory<char[]>() { 271 private final char[] clonedPassword = password != null ? password.clone() : null; 272 273 @Override 274 public char[] newInstance() { 275 return clonedPassword != null ? clonedPassword.clone() : null; 276 } 277 }; 278 } 279 280 KeyStoreParameters getDefaultConfig() { 281 return defaultConfig; 282 } 283 284 private static KeyStoreParameters parseConfig(final URI configFile) { 285 try (final Reader configFileReader = new InputStreamReader(configFile.toURL().openStream())) { 286 final Properties properties = new Properties(); 287 properties.load(configFileReader); 288 289 final String keyStoreBaseDNProperty = properties.getProperty(KEYSTORE_BASE_DN_PROPERTY); 290 if (keyStoreBaseDNProperty == null) { 291 throw new IllegalArgumentException("missing key store base DN"); 292 } 293 final DN keyStoreBaseDN = DN.valueOf(keyStoreBaseDNProperty); 294 295 final Options keystoreOptions = defaultOptions(); 296 final String keystorePassword = properties.getProperty(KEYSTORE_PASSWORD_PROPERTY); 297 if (keystorePassword != null) { 298 keystoreOptions.set(GLOBAL_PASSWORD, newClearTextPasswordFactory(keystorePassword.toCharArray())); 299 } 300 301 final ConnectionFactory factory; 302 final String ldif = properties.getProperty(LDIF_PROPERTY); 303 if (ldif != null) { 304 factory = newLDIFConnectionFactory(new File(ldif)); 305 } else { 306 final String host = properties.getProperty(HOST_PROPERTY, "localhost"); 307 final int port = Integer.parseInt(properties.getProperty(PORT_PROPERTY, "389")); 308 final DN bindDN = DN.valueOf(properties.getProperty(BIND_DN_PROPERTY, "")); 309 final String bindPassword = properties.getProperty(BIND_PASSWORD_PROPERTY); 310 311 final Options factoryOptions = defaultOptions(); 312 if (!bindDN.isRootDN()) { 313 factoryOptions.set(AUTHN_BIND_REQUEST, 314 newSimpleBindRequest(bindDN.toString(), bindPassword.toCharArray())); 315 } 316 factory = newCachedConnectionPool(new LDAPConnectionFactory(host, port, factoryOptions)); 317 } 318 319 return newKeyStoreParameters(factory, keyStoreBaseDN, keystoreOptions); 320 } catch (Exception e) { 321 throw new ProviderException("Error parsing configuration in file '" + configFile + "'", e); 322 } 323 } 324 325 private static final class WriteLDIFOnUpdateRequestHandler implements RequestHandler<RequestContext> { 326 private final MemoryBackend backend; 327 private final File ldifFile; 328 329 private WriteLDIFOnUpdateRequestHandler(final MemoryBackend backend, final File ldifFile) { 330 this.backend = backend; 331 this.ldifFile = ldifFile; 332 } 333 334 @Override 335 public void handleAdd(final RequestContext requestContext, final AddRequest request, 336 final IntermediateResponseHandler intermediateResponseHandler, 337 final LdapResultHandler<Result> resultHandler) { 338 backend.handleAdd(requestContext, request, intermediateResponseHandler, saveAndForwardTo(resultHandler)); 339 } 340 341 @Override 342 public void handleBind(final RequestContext requestContext, final int version, final BindRequest request, 343 final IntermediateResponseHandler intermediateResponseHandler, 344 final LdapResultHandler<BindResult> resultHandler) { 345 backend.handleBind(requestContext, version, request, intermediateResponseHandler, resultHandler); 346 } 347 348 @Override 349 public void handleCompare(final RequestContext requestContext, final CompareRequest request, 350 final IntermediateResponseHandler intermediateResponseHandler, 351 final LdapResultHandler<CompareResult> resultHandler) { 352 backend.handleCompare(requestContext, request, intermediateResponseHandler, resultHandler); 353 } 354 355 @Override 356 public void handleDelete(final RequestContext requestContext, final DeleteRequest request, 357 final IntermediateResponseHandler intermediateResponseHandler, 358 final LdapResultHandler<Result> resultHandler) { 359 backend.handleDelete(requestContext, request, intermediateResponseHandler, saveAndForwardTo(resultHandler)); 360 } 361 362 @Override 363 public <R extends ExtendedResult> void handleExtendedRequest(final RequestContext requestContext, 364 final ExtendedRequest<R> request, 365 final IntermediateResponseHandler 366 intermediateResponseHandler, 367 final LdapResultHandler<R> resultHandler) { 368 backend.handleExtendedRequest(requestContext, request, intermediateResponseHandler, resultHandler); 369 } 370 371 @Override 372 public void handleModify(final RequestContext requestContext, final ModifyRequest request, 373 final IntermediateResponseHandler intermediateResponseHandler, 374 final LdapResultHandler<Result> resultHandler) { 375 backend.handleModify(requestContext, request, intermediateResponseHandler, saveAndForwardTo(resultHandler)); 376 } 377 378 @Override 379 public void handleModifyDN(final RequestContext requestContext, final ModifyDNRequest request, 380 final IntermediateResponseHandler intermediateResponseHandler, 381 final LdapResultHandler<Result> resultHandler) { 382 backend.handleModifyDN(requestContext, 383 request, 384 intermediateResponseHandler, 385 saveAndForwardTo(resultHandler)); 386 } 387 388 @Override 389 public void handleSearch(final RequestContext requestContext, final SearchRequest request, 390 final IntermediateResponseHandler intermediateResponseHandler, 391 final SearchResultHandler entryHandler, 392 final LdapResultHandler<Result> resultHandler) { 393 backend.handleSearch(requestContext, request, intermediateResponseHandler, entryHandler, resultHandler); 394 } 395 396 private LdapResultHandler<Result> saveAndForwardTo(final LdapResultHandler<Result> resultHandler) { 397 return new LdapResultHandler<Result>() { 398 @Override 399 public void handleException(final LdapException exception) { 400 resultHandler.handleException(exception); 401 } 402 403 @Override 404 public void handleResult(final Result result) { 405 try { 406 writeLDIF(backend, ldifFile); 407 resultHandler.handleResult(result); 408 } catch (IOException e) { 409 final LdapException ldapException = 410 newLdapException(ResultCode.OTHER, "Unable to write LDIF file " + ldifFile, e); 411 resultHandler.handleException(ldapException); 412 } 413 } 414 }; 415 } 416 417 private static void writeLDIF(final MemoryBackend backend, final File ldifFile) throws IOException { 418 try (final FileWriter fileWriter = new FileWriter(ldifFile); 419 final BufferedWriter bufferedWriter = new BufferedWriter(fileWriter); 420 final LDIFEntryWriter entryWriter = new LDIFEntryWriter(bufferedWriter)) { 421 copyTo(newEntryIteratorReader(backend.getAll().iterator()), entryWriter); 422 } 423 } 424 } 425 426 private final class KeyStoreService extends Service { 427 private KeyStoreService() { 428 super(OpenDJProvider.this, "KeyStore", "LDAP", KeyStoreImpl.class.getName(), singletonList("OpenDJ"), null); 429 } 430 431 // Override the default constructor so that we can pass in this provider and any file based configuration. 432 @Override 433 public Object newInstance(final Object constructorParameter) { 434 return new KeyStoreImpl(OpenDJProvider.this); 435 } 436 } 437}