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 * 016 */ 017package org.forgerock.opendj.rest2ldap; 018 019import static java.util.Arrays.asList; 020import static java.util.Collections.emptyList; 021import static org.forgerock.http.routing.RouteMatchers.newResourceApiVersionBehaviourManager; 022import static org.forgerock.http.routing.RoutingMode.STARTS_WITH; 023import static org.forgerock.http.routing.Version.version; 024import static org.forgerock.http.util.Json.readJsonLenient; 025import static org.forgerock.json.JsonValueFunctions.enumConstant; 026import static org.forgerock.json.JsonValueFunctions.pointer; 027import static org.forgerock.json.JsonValueFunctions.setOf; 028import static org.forgerock.json.resource.RouteMatchers.requestUriMatcher; 029import static org.forgerock.json.resource.RouteMatchers.resourceApiVersionContextFilter; 030import static org.forgerock.opendj.ldap.Connections.LOAD_BALANCER_MONITORING_INTERVAL; 031import static org.forgerock.opendj.ldap.Connections.newCachedConnectionPool; 032import static org.forgerock.opendj.ldap.Connections.newFailoverLoadBalancer; 033import static org.forgerock.opendj.ldap.Connections.newRoundRobinLoadBalancer; 034import static org.forgerock.opendj.ldap.KeyManagers.useJvmDefaultKeyStore; 035import static org.forgerock.opendj.ldap.KeyManagers.useKeyStoreFile; 036import static org.forgerock.opendj.ldap.KeyManagers.usePKCS11Token; 037import static org.forgerock.opendj.ldap.KeyManagers.useSingleCertificate; 038import static org.forgerock.opendj.ldap.LDAPConnectionFactory.*; 039import static org.forgerock.opendj.ldap.TrustManagers.checkUsingTrustStore; 040import static org.forgerock.opendj.ldap.TrustManagers.trustAll; 041import static org.forgerock.opendj.rest2ldap.ReadOnUpdatePolicy.CONTROLS; 042import static org.forgerock.opendj.rest2ldap.Rest2Ldap.*; 043import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.*; 044import static org.forgerock.opendj.rest2ldap.Utils.newJsonValueException; 045import static org.forgerock.util.Utils.joinAsString; 046import static org.forgerock.util.time.Duration.duration; 047 048import java.io.BufferedReader; 049import java.io.File; 050import java.io.FileFilter; 051import java.io.FileInputStream; 052import java.io.FileReader; 053import java.io.IOException; 054import java.io.InputStream; 055import java.security.GeneralSecurityException; 056import java.util.ArrayList; 057import java.util.Collections; 058import java.util.LinkedHashMap; 059import java.util.LinkedList; 060import java.util.List; 061import java.util.Map; 062import java.util.concurrent.TimeUnit; 063 064import javax.net.ssl.TrustManager; 065import javax.net.ssl.X509KeyManager; 066 067import org.forgerock.http.routing.ResourceApiVersionBehaviourManager; 068import org.forgerock.i18n.LocalizedIllegalArgumentException; 069import org.forgerock.i18n.slf4j.LocalizedLogger; 070import org.forgerock.json.JsonValue; 071import org.forgerock.json.resource.BadRequestException; 072import org.forgerock.json.resource.FilterChain; 073import org.forgerock.json.resource.Request; 074import org.forgerock.json.resource.RequestHandler; 075import org.forgerock.json.resource.ResourceException; 076import org.forgerock.json.resource.Router; 077import org.forgerock.opendj.ldap.ConnectionFactory; 078import org.forgerock.opendj.ldap.LDAPConnectionFactory; 079import org.forgerock.opendj.ldap.SSLContextBuilder; 080import org.forgerock.opendj.ldap.requests.BindRequest; 081import org.forgerock.opendj.ldap.requests.Requests; 082import org.forgerock.services.context.Context; 083import org.forgerock.util.Options; 084import org.forgerock.util.promise.Promise; 085import org.forgerock.util.time.Duration; 086 087/** Provides core factory methods and builders for constructing Rest2Ldap endpoints from JSON configuration. */ 088public final class Rest2LdapJsonConfigurator { 089 private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); 090 091 /** 092 * Parses Rest2Ldap configuration options. The JSON configuration must have the following format: 093 * <p> 094 * <pre> 095 * { 096 * "readOnUpdatePolicy": "controls", 097 * "useSubtreeDelete": true, 098 * "usePermissiveModify": true, 099 * "useMvcc": true 100 * "mvccAttribute": "etag" 101 * } 102 * </pre> 103 * <p> 104 * See the sample configuration file for a detailed description of its content. 105 * 106 * @param config 107 * The JSON configuration. 108 * @return The parsed Rest2Ldap configuration options. 109 * @throws IllegalArgumentException 110 * If the configuration is invalid. 111 */ 112 public static Options configureOptions(final JsonValue config) { 113 final Options options = Options.defaultOptions(); 114 115 options.set(READ_ON_UPDATE_POLICY, 116 config.get("readOnUpdatePolicy").defaultTo(CONTROLS).as(enumConstant(ReadOnUpdatePolicy.class))); 117 118 // Default to false, even though it is supported by OpenDJ, because it requires additional permissions. 119 options.set(USE_SUBTREE_DELETE, config.get("useSubtreeDelete").defaultTo(false).asBoolean()); 120 121 // Default to true because it is supported by OpenDJ and does not require additional permissions. 122 options.set(USE_PERMISSIVE_MODIFY, config.get("usePermissiveModify").defaultTo(false).asBoolean()); 123 124 options.set(USE_MVCC, config.get("useMvcc").defaultTo(true).asBoolean()); 125 options.set(MVCC_ATTRIBUTE, config.get("mvccAttribute").defaultTo("etag").asString()); 126 127 return options; 128 } 129 130 /** 131 * Parses a list of Rest2Ldap resource definitions. The JSON configuration must have the following format: 132 * <p> 133 * <pre> 134 * "top": { 135 * "isAbstract": true, 136 * "properties": { 137 * "_rev": { 138 * "type": "simple" 139 * "ldapAttribute": "etag", 140 * "writability": "readOnly" 141 * }, 142 * ... 143 * }, 144 * ... 145 * }, 146 * ... 147 * </pre> 148 * <p> 149 * See the sample configuration file for a detailed description of its content. 150 * 151 * @param config 152 * The JSON configuration. 153 * @return The parsed list of Rest2Ldap resource definitions. 154 * @throws IllegalArgumentException 155 * If the configuration is invalid. 156 */ 157 public static List<Resource> configureResources(final JsonValue config) { 158 final JsonValue resourcesConfig = config.required().expect(Map.class); 159 final List<Resource> resources = new LinkedList<>(); 160 for (final String resourceId : resourcesConfig.keys()) { 161 resources.add(configureResource(resourceId, resourcesConfig.get(resourceId))); 162 } 163 return resources; 164 } 165 166 /** 167 * Creates a new CREST {@link Router} using the provided endpoints configuration directory and Rest2Ldap options. 168 * The Rest2Ldap configuration typically has the following structure on disk: 169 * <ul> 170 * <li> config.json - contains the configuration for the LDAP connection factories and authorization 171 * <li> rest2ldap/rest2ldap.json - defines Rest2Ldap configuration options 172 * <li> rest2ldap/endpoints/{api} - a directory containing the endpoint's resource definitions for endpoint {api} 173 * <li> rest2ldap/endpoints/{api}/{resource-id}.json - the resource definitions for a specific version of API {api}. 174 * The name of the file, {resource-id}, determines which resource type definition in the mapping file will be 175 * used as the root resource. 176 * </ul> 177 * 178 * @param endpointsDirectory The directory representing the Rest2Ldap "endpoints" directory. 179 * @param options The Rest2Ldap configuration options. 180 * @return A new CREST {@link Router} configured using the provided options and endpoints. 181 * @throws IOException If the endpoints configuration cannot be read. 182 * @throws IllegalArgumentException 183 * If the configuration is invalid. 184 */ 185 public static Router configureEndpoints(final File endpointsDirectory, final Options options) throws IOException { 186 final Router pathRouter = new Router(); 187 188 final File[] endpoints = endpointsDirectory.listFiles(new FileFilter() { 189 @Override 190 public boolean accept(final File pathname) { 191 return pathname.isDirectory() && pathname.canRead(); 192 } 193 }); 194 195 if (endpoints == null) { 196 throw new LocalizedIllegalArgumentException(ERR_INVALID_ENDPOINTS_DIRECTORY.get(endpointsDirectory)); 197 } 198 199 for (final File endpoint : endpoints) { 200 final RequestHandler endpointHandler = configureEndpoint(endpoint, options); 201 pathRouter.addRoute(requestUriMatcher(STARTS_WITH, endpoint.getName()), endpointHandler); 202 } 203 return pathRouter; 204 } 205 206 /** 207 * Creates a new CREST {@link RequestHandler} representing a single endpoint whose configuration is defined in the 208 * provided {@code endpointDirectory} parameter. The directory should contain a separate file for each supported 209 * version of the REST endpoint. The name of the file, excluding the suffix, identifies the resource definition 210 * which acts as the entry point into the endpoint. 211 * 212 * @param endpointDirectory The directory containing the endpoint's resource definitions, e.g. 213 * rest2ldap/routes/api would contain definitions for the "api" endpoint. 214 * @param options The Rest2Ldap configuration options. 215 * @return A new CREST {@link RequestHandler} configured using the provided options and endpoint mappings. 216 * @throws IOException If the endpoint configuration cannot be read. 217 * @throws IllegalArgumentException If the configuration is invalid. 218 */ 219 public static RequestHandler configureEndpoint(File endpointDirectory, Options options) throws IOException { 220 final Router versionRouter = new Router(); 221 final File[] endpointVersions = endpointDirectory.listFiles(new FileFilter() { 222 @Override 223 public boolean accept(final File pathname) { 224 return pathname.isFile() && pathname.canRead() && pathname.getName().endsWith(".json"); 225 } 226 }); 227 228 if (endpointVersions == null) { 229 throw new LocalizedIllegalArgumentException(ERR_INVALID_ENDPOINT_DIRECTORY.get(endpointDirectory)); 230 } 231 232 final List<String> supportedVersions = new ArrayList<>(); 233 boolean hasWildCardVersion = false; 234 for (final File endpointVersion : endpointVersions) { 235 final JsonValue mappingConfig = readJson(endpointVersion); 236 final String version = mappingConfig.get("version").defaultTo("*").asString(); 237 final List<Resource> resourceTypes = configureResources(mappingConfig.get("resourceTypes")); 238 final Rest2Ldap rest2Ldap = rest2Ldap(options, resourceTypes); 239 240 final String endpointVersionFileName = endpointVersion.getName(); 241 final int endIndex = endpointVersionFileName.lastIndexOf('.'); 242 final String rootResourceType = endpointVersionFileName.substring(0, endIndex); 243 final RequestHandler handler = rest2Ldap.newRequestHandlerFor(rootResourceType); 244 245 if (version.equals("*")) { 246 versionRouter.setDefaultRoute(handler); 247 hasWildCardVersion = true; 248 } else { 249 versionRouter.addRoute(version(version), handler); 250 supportedVersions.add(version); 251 } 252 logger.debug(INFO_REST2LDAP_CREATING_ENDPOINT.get(endpointDirectory.getName(), version)); 253 } 254 if (!hasWildCardVersion) { 255 versionRouter.setDefaultRoute(new AbstractRequestHandler() { 256 @Override 257 protected <V> Promise<V, ResourceException> handleRequest(Context context, Request request) { 258 final String message = ERR_BAD_API_RESOURCE_VERSION.get(request.getResourceVersion(), 259 joinAsString(", ", supportedVersions)) 260 .toString(); 261 return new BadRequestException(message).asPromise(); 262 } 263 }); 264 } 265 266 // FIXME: Disable the warning header for now due to CREST-389 / CREST-390. 267 final ResourceApiVersionBehaviourManager behaviourManager = newResourceApiVersionBehaviourManager(); 268 behaviourManager.setWarningEnabled(false); 269 return new FilterChain(versionRouter, resourceApiVersionContextFilter(behaviourManager)); 270 } 271 272 static JsonValue readJson(final File resource) throws IOException { 273 try (InputStream in = new FileInputStream(resource)) { 274 return new JsonValue(readJsonLenient(in)); 275 } 276 } 277 278 private static Resource configureResource(final String resourceId, final JsonValue config) { 279 final Resource resource = resource(resourceId) 280 .isAbstract(config.get("isAbstract").defaultTo(false).asBoolean()) 281 .superType(config.get("superType").asString()) 282 .objectClasses(config.get("objectClasses") 283 .defaultTo(emptyList()).asList(String.class).toArray(new String[0])) 284 .supportedActions(config.get("supportedActions") 285 .defaultTo(emptyList()) 286 .as(setOf(enumConstant(Action.class))).toArray(new Action[0])) 287 .resourceTypeProperty(config.get("resourceTypeProperty").as(pointer())) 288 .includeAllUserAttributesByDefault(config.get("includeAllUserAttributesByDefault") 289 .defaultTo(false).asBoolean()) 290 .excludedDefaultUserAttributes(config.get("excludedDefaultUserAttributes") 291 .defaultTo(Collections.emptyList()).asList(String.class)); 292 293 final JsonValue properties = config.get("properties").expect(Map.class); 294 for (final String property : properties.keys()) { 295 resource.property(property, configurePropertyMapper(properties.get(property), property)); 296 } 297 298 final JsonValue subResources = config.get("subResources").expect(Map.class); 299 for (final String urlTemplate : subResources.keys()) { 300 resource.subResource(configureSubResource(urlTemplate, subResources.get(urlTemplate))); 301 } 302 303 return resource; 304 } 305 306 private enum NamingStrategyType { CLIENTDNNAMING, CLIENTNAMING, SERVERNAMING } 307 private enum SubResourceType { COLLECTION, SINGLETON } 308 309 private static SubResource configureSubResource(final String urlTemplate, final JsonValue config) { 310 final String dnTemplate = config.get("dnTemplate").defaultTo("").asString(); 311 final Boolean isReadOnly = config.get("isReadOnly").defaultTo(false).asBoolean(); 312 final String resourceId = config.get("resource").required().asString(); 313 314 if (config.get("type").required().as(enumConstant(SubResourceType.class)) == SubResourceType.COLLECTION) { 315 final String[] glueObjectClasses = config.get("glueObjectClasses") 316 .defaultTo(emptyList()) 317 .asList(String.class) 318 .toArray(new String[0]); 319 320 final SubResourceCollection collection = collectionOf(resourceId).urlTemplate(urlTemplate) 321 .dnTemplate(dnTemplate) 322 .isReadOnly(isReadOnly) 323 .glueObjectClasses(glueObjectClasses); 324 325 final JsonValue namingStrategy = config.get("namingStrategy").required(); 326 switch (namingStrategy.get("type").required().as(enumConstant(NamingStrategyType.class))) { 327 case CLIENTDNNAMING: 328 collection.useClientDnNaming(namingStrategy.get("dnAttribute").required().asString()); 329 break; 330 case CLIENTNAMING: 331 collection.useClientNaming(namingStrategy.get("dnAttribute").required().asString(), 332 namingStrategy.get("idAttribute").required().asString()); 333 break; 334 case SERVERNAMING: 335 collection.useServerNaming(namingStrategy.get("dnAttribute").required().asString(), 336 namingStrategy.get("idAttribute").required().asString()); 337 break; 338 } 339 340 return collection; 341 } else { 342 return singletonOf(resourceId).urlTemplate(urlTemplate).dnTemplate(dnTemplate).isReadOnly(isReadOnly); 343 } 344 } 345 346 private static PropertyMapper configurePropertyMapper(final JsonValue mapper, final String defaultLdapAttribute) { 347 switch (mapper.get("type").required().asString()) { 348 case "resourceType": 349 return resourceType(); 350 case "constant": 351 return constant(mapper.get("value").getObject()); 352 case "simple": 353 return simple(mapper.get("ldapAttribute").defaultTo(defaultLdapAttribute).required().asString()) 354 .defaultJsonValue(mapper.get("defaultJsonValue").getObject()) 355 .isBinary(mapper.get("isBinary").defaultTo(false).asBoolean()) 356 .isRequired(mapper.get("isRequired").defaultTo(false).asBoolean()) 357 .isMultiValued(mapper.get("isMultiValued").defaultTo(false).asBoolean()) 358 .writability(parseWritability(mapper)); 359 case "json": 360 return json(mapper.get("ldapAttribute").defaultTo(defaultLdapAttribute).required().asString()) 361 .defaultJsonValue(mapper.get("defaultJsonValue").getObject()) 362 .isRequired(mapper.get("isRequired").defaultTo(false).asBoolean()) 363 .isMultiValued(mapper.get("isMultiValued").defaultTo(false).asBoolean()) 364 .jsonSchema(mapper.isDefined("schema") ? mapper.get("schema") : null) 365 .writability(parseWritability(mapper)); 366 case "reference": 367 final String ldapAttribute = mapper.get("ldapAttribute") 368 .defaultTo(defaultLdapAttribute).required().asString(); 369 final String baseDN = mapper.get("baseDn").required().asString(); 370 final String primaryKey = mapper.get("primaryKey").required().asString(); 371 final PropertyMapper m = configurePropertyMapper(mapper.get("mapper").required(), primaryKey); 372 return reference(ldapAttribute, baseDN, primaryKey, m) 373 .isRequired(mapper.get("isRequired").defaultTo(false).asBoolean()) 374 .isMultiValued(mapper.get("isMultiValued").defaultTo(false).asBoolean()) 375 .searchFilter(mapper.get("searchFilter").defaultTo("(objectClass=*)").asString()) 376 .writability(parseWritability(mapper)); 377 case "object": 378 final JsonValue properties = mapper.get("properties"); 379 final ObjectPropertyMapper object = object(); 380 for (final String attribute : properties.keys()) { 381 object.property(attribute, configurePropertyMapper(properties.get(attribute), attribute)); 382 } 383 return object; 384 default: 385 throw newJsonValueException(mapper, ERR_CONFIG_NO_MAPPING_IN_CONFIGURATION.get( 386 "constant, simple, reference, object")); 387 } 388 } 389 390 private static WritabilityPolicy parseWritability(final JsonValue mapper) { 391 return mapper.get("writability").defaultTo("readWrite").as(enumConstant(WritabilityPolicy.class)); 392 } 393 394 /** Indicates whether LDAP client connections should use SSL or StartTLS. */ 395 private enum ConnectionSecurity { NONE, SSL, STARTTLS } 396 397 /** Specifies the mechanism which will be used for trusting certificates presented by the LDAP server. */ 398 private enum TrustManagerType { TRUSTALL, JVM, FILE } 399 400 /** Specifies the type of key-store to use when performing SSL client authentication. */ 401 private enum KeyManagerType { JVM, FILE, PKCS11 } 402 403 /** 404 * Configures a {@link X509KeyManager} using the provided JSON configuration. 405 * 406 * @param configuration 407 * The JSON object containing the key manager configuration. 408 * @return The configured key manager. 409 */ 410 public static X509KeyManager configureKeyManager(final JsonValue configuration) { 411 try { 412 return configureKeyManager(configuration, KeyManagerType.JVM); 413 } catch (GeneralSecurityException | IOException e) { 414 throw new IllegalArgumentException(ERR_CONFIG_INVALID_KEY_MANAGER.get( 415 configuration.getPointer(), e.getLocalizedMessage()).toString(), e); 416 } 417 } 418 419 private static X509KeyManager configureKeyManager(JsonValue config, KeyManagerType defaultIfMissing) 420 throws GeneralSecurityException, IOException { 421 final KeyManagerType keyManagerType = config.get("keyManager") 422 .defaultTo(defaultIfMissing) 423 .as(enumConstant(KeyManagerType.class)); 424 switch (keyManagerType) { 425 case JVM: 426 return useJvmDefaultKeyStore(); 427 case FILE: 428 final String fileName = config.get("fileBasedKeyManagerFile").required().asString(); 429 final String passwordFile = config.get("fileBasedKeyManagerPasswordFile").asString(); 430 final String password = passwordFile != null 431 ? readPasswordFromFile(passwordFile) : config.get("fileBasedKeyManagerPassword").asString(); 432 final String type = config.get("fileBasedKeyManagerType").asString(); 433 final String provider = config.get("fileBasedKeyManagerProvider").asString(); 434 return useKeyStoreFile(fileName, password != null ? password.toCharArray() : null, type, provider); 435 case PKCS11: 436 final String pkcs11PasswordFile = config.get("pkcs11KeyManagerPasswordFile").asString(); 437 return usePKCS11Token(pkcs11PasswordFile != null 438 ? readPasswordFromFile(pkcs11PasswordFile).toCharArray() : null); 439 default: 440 throw new IllegalArgumentException("Unsupported key-manager type: " + keyManagerType); 441 } 442 } 443 444 private static String readPasswordFromFile(String fileName) throws IOException { 445 try (final BufferedReader reader = new BufferedReader(new FileReader(new File(fileName)))) { 446 return reader.readLine(); 447 } 448 } 449 450 /** 451 * Configures a {@link TrustManager} using the provided JSON configuration. 452 * 453 * @param configuration 454 * The JSON object containing the trust manager configuration. 455 * @return The configured trust manager. 456 */ 457 public static TrustManager configureTrustManager(final JsonValue configuration) { 458 try { 459 return configureTrustManager(configuration, TrustManagerType.JVM); 460 } catch (GeneralSecurityException | IOException e) { 461 throw new IllegalArgumentException(ERR_CONFIG_INVALID_TRUST_MANAGER.get( 462 configuration.getPointer(), e.getLocalizedMessage()).toString(), e); 463 } 464 } 465 466 private static TrustManager configureTrustManager(JsonValue config, TrustManagerType defaultIfMissing) 467 throws GeneralSecurityException, IOException { 468 final TrustManagerType trustManagerType = config.get("trustManager") 469 .defaultTo(defaultIfMissing) 470 .as(enumConstant(TrustManagerType.class)); 471 switch (trustManagerType) { 472 case TRUSTALL: 473 return trustAll(); 474 case JVM: 475 return null; 476 case FILE: 477 final String fileName = config.get("fileBasedTrustManagerFile").required().asString(); 478 final String passwordFile = config.get("fileBasedTrustManagerPasswordFile").asString(); 479 final String password = passwordFile != null 480 ? readPasswordFromFile(passwordFile) : config.get("fileBasedTrustManagerPassword").asString(); 481 final String type = config.get("fileBasedTrustManagerType").asString(); 482 return checkUsingTrustStore(fileName, password != null ? password.toCharArray() : null, type); 483 default: 484 throw new IllegalArgumentException("Unsupported trust-manager type: " + trustManagerType); 485 } 486 } 487 488 /** 489 * Creates a new connection factory using the named configuration in the provided JSON list of factory 490 * configurations. See the sample configuration file for a detailed description of its content. 491 * 492 * @param configuration 493 * The JSON configuration. 494 * @param name 495 * The name of the connection factory configuration to be parsed. 496 * @param trustManager 497 * The trust manager to use for secure connection. Can be {@code null} 498 * to use the default JVM trust manager. 499 * @param keyManager 500 * The key manager to use for secure connection. Can be {@code null} 501 * to use the default JVM key manager. 502 * @param providerClassLoader 503 * The {@link ClassLoader} used to fetch the {@link org.forgerock.opendj.ldap.spi.TransportProvider}. This 504 * can be useful in OSGI environments. 505 * @return A new connection factory using the provided JSON configuration. 506 * @throws IllegalArgumentException 507 * If the configuration is invalid. 508 */ 509 public static ConnectionFactory configureConnectionFactory(final JsonValue configuration, 510 final String name, 511 final TrustManager trustManager, 512 final X509KeyManager keyManager, 513 final ClassLoader providerClassLoader) { 514 final JsonValue normalizedConfiguration = normalizeConnectionFactory(configuration, name, 0); 515 return configureConnectionFactory(normalizedConfiguration, trustManager, keyManager, providerClassLoader); 516 } 517 518 /** 519 * Creates a new connection factory using the named configuration in the provided JSON list of factory 520 * configurations. See the sample configuration file for a detailed description of its content. 521 * 522 * @param configuration 523 * The JSON configuration. 524 * @param name 525 * The name of the connection factory configuration to be parsed. 526 * @param trustManager 527 * The trust manager to use for secure connection. Can be {@code null} 528 * to use the default JVM trust manager. 529 * @param keyManager 530 * The key manager to use for secure connection. Can be {@code null} 531 * to use the default JVM key manager. 532 * @return A new connection factory using the provided JSON configuration. 533 * @throws IllegalArgumentException 534 * If the configuration is invalid. 535 */ 536 public static ConnectionFactory configureConnectionFactory(final JsonValue configuration, 537 final String name, 538 final TrustManager trustManager, 539 final X509KeyManager keyManager) { 540 return configureConnectionFactory(configuration, name, trustManager, keyManager, null); 541 } 542 543 private static ConnectionFactory configureConnectionFactory(final JsonValue configuration, 544 final TrustManager trustManager, 545 final X509KeyManager keyManager, 546 final ClassLoader providerClassLoader) { 547 final long heartBeatIntervalSeconds = configuration.get("heartBeatIntervalSeconds").defaultTo(30L).asLong(); 548 final Duration heartBeatInterval = duration(Math.max(heartBeatIntervalSeconds, 1L), TimeUnit.SECONDS); 549 550 final long heartBeatTimeoutMillis = configuration.get("heartBeatTimeoutMilliSeconds").defaultTo(500L).asLong(); 551 final Duration heartBeatTimeout = duration(Math.max(heartBeatTimeoutMillis, 100L), TimeUnit.MILLISECONDS); 552 553 final Options options = Options.defaultOptions() 554 .set(TRANSPORT_PROVIDER_CLASS_LOADER, providerClassLoader) 555 .set(HEARTBEAT_ENABLED, true) 556 .set(HEARTBEAT_INTERVAL, heartBeatInterval) 557 .set(HEARTBEAT_TIMEOUT, heartBeatTimeout) 558 .set(LOAD_BALANCER_MONITORING_INTERVAL, heartBeatInterval); 559 560 // Parse pool parameters, 561 final int connectionPoolSize = 562 Math.max(configuration.get("connectionPoolSize").defaultTo(10).asInteger(), 1); 563 564 // Parse authentication parameters. 565 if (configuration.isDefined("authentication")) { 566 final JsonValue authn = configuration.get("authentication"); 567 if (authn.isDefined("simple")) { 568 final JsonValue simple = authn.get("simple"); 569 final BindRequest bindRequest = 570 Requests.newSimpleBindRequest(simple.get("bindDn").required().asString(), 571 simple.get("bindPassword").required().asString().toCharArray()); 572 options.set(AUTHN_BIND_REQUEST, bindRequest); 573 } else { 574 throw new LocalizedIllegalArgumentException(ERR_CONFIG_INVALID_AUTHENTICATION.get()); 575 } 576 } 577 578 // Parse SSL/StartTLS parameters. 579 final ConnectionSecurity connectionSecurity = configuration.get("connectionSecurity") 580 .defaultTo(ConnectionSecurity.NONE) 581 .as(enumConstant(ConnectionSecurity.class)); 582 if (connectionSecurity != ConnectionSecurity.NONE) { 583 try { 584 // Configure SSL. 585 final SSLContextBuilder builder = new SSLContextBuilder(); 586 builder.setTrustManager(trustManager); 587 final String sslCertAlias = configuration.get("sslCertAlias").asString(); 588 builder.setKeyManager(sslCertAlias != null 589 ? useSingleCertificate(sslCertAlias, keyManager) 590 : keyManager); 591 options.set(SSL_CONTEXT, builder.getSSLContext()); 592 options.set(SSL_USE_STARTTLS, connectionSecurity == ConnectionSecurity.STARTTLS); 593 } catch (GeneralSecurityException e) { 594 // Rethrow as unchecked exception. 595 throw new IllegalArgumentException(e); 596 } 597 } 598 599 // Parse primary data center. 600 final JsonValue primaryLdapServers = configuration.get("primaryLdapServers"); 601 if (!primaryLdapServers.isList() || primaryLdapServers.size() == 0) { 602 throw new IllegalArgumentException("No primaryLdapServers"); 603 } 604 final ConnectionFactory primary = parseLdapServers(primaryLdapServers, connectionPoolSize, options); 605 606 // Parse secondary data center(s). 607 final JsonValue secondaryLdapServers = configuration.get("secondaryLdapServers"); 608 ConnectionFactory secondary = null; 609 if (secondaryLdapServers.isList()) { 610 if (secondaryLdapServers.size() > 0) { 611 secondary = parseLdapServers(secondaryLdapServers, connectionPoolSize, options); 612 } 613 } else if (!secondaryLdapServers.isNull()) { 614 throw new LocalizedIllegalArgumentException(ERR_CONFIG_INVALID_SECONDARY_LDAP_SERVER.get()); 615 } 616 617 // Create fail-over. 618 if (secondary != null) { 619 return newFailoverLoadBalancer(asList(primary, secondary), options); 620 } else { 621 return primary; 622 } 623 } 624 625 private static JsonValue normalizeConnectionFactory(final JsonValue configuration, 626 final String name, final int depth) { 627 // Protect against infinite recursion in the configuration. 628 if (depth > 100) { 629 throw new LocalizedIllegalArgumentException(ERR_CONFIG_SERVER_CIRCULAR_DEPENDENCIES.get(name)); 630 } 631 632 final JsonValue current = configuration.get(name).required(); 633 if (current.isDefined("inheritFrom")) { 634 // Inherit missing fields from inherited configuration. 635 final JsonValue parent = 636 normalizeConnectionFactory(configuration, 637 current.get("inheritFrom").asString(), depth + 1); 638 final Map<String, Object> normalized = new LinkedHashMap<>(parent.asMap()); 639 normalized.putAll(current.asMap()); 640 normalized.remove("inheritFrom"); 641 return new JsonValue(normalized); 642 } else { 643 // No normalization required. 644 return current; 645 } 646 } 647 648 private static ConnectionFactory parseLdapServers(JsonValue config, int poolSize, Options options) { 649 final List<ConnectionFactory> servers = new ArrayList<>(config.size()); 650 for (final JsonValue server : config) { 651 final String host = server.get("hostname").required().asString(); 652 final int port = server.get("port").required().asInteger(); 653 final ConnectionFactory factory = new LDAPConnectionFactory(host, port, options); 654 if (poolSize > 1) { 655 servers.add(newCachedConnectionPool(factory, 0, poolSize, 60L, TimeUnit.SECONDS)); 656 } else { 657 servers.add(factory); 658 } 659 } 660 if (servers.size() > 1) { 661 return newRoundRobinLoadBalancer(servers, options); 662 } else { 663 return servers.get(0); 664 } 665 } 666 667 private Rest2LdapJsonConfigurator() { 668 // Prevent instantiation. 669 } 670}