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 "reference": 360 final String ldapAttribute = mapper.get("ldapAttribute") 361 .defaultTo(defaultLdapAttribute).required().asString(); 362 final String baseDN = mapper.get("baseDn").required().asString(); 363 final String primaryKey = mapper.get("primaryKey").required().asString(); 364 final PropertyMapper m = configurePropertyMapper(mapper.get("mapper").required(), primaryKey); 365 return reference(ldapAttribute, baseDN, primaryKey, m) 366 .isRequired(mapper.get("isRequired").defaultTo(false).asBoolean()) 367 .isMultiValued(mapper.get("isMultiValued").defaultTo(false).asBoolean()) 368 .searchFilter(mapper.get("searchFilter").defaultTo("(objectClass=*)").asString()) 369 .writability(parseWritability(mapper)); 370 case "object": 371 final JsonValue properties = mapper.get("properties"); 372 final ObjectPropertyMapper object = object(); 373 for (final String attribute : properties.keys()) { 374 object.property(attribute, configurePropertyMapper(properties.get(attribute), attribute)); 375 } 376 return object; 377 default: 378 throw newJsonValueException(mapper, ERR_CONFIG_NO_MAPPING_IN_CONFIGURATION.get( 379 "constant, simple, reference, object")); 380 } 381 } 382 383 private static WritabilityPolicy parseWritability(final JsonValue mapper) { 384 return mapper.get("writability").defaultTo("readWrite").as(enumConstant(WritabilityPolicy.class)); 385 } 386 387 /** Indicates whether LDAP client connections should use SSL or StartTLS. */ 388 private enum ConnectionSecurity { NONE, SSL, STARTTLS } 389 390 /** Specifies the mechanism which will be used for trusting certificates presented by the LDAP server. */ 391 private enum TrustManagerType { TRUSTALL, JVM, FILE } 392 393 /** Specifies the type of key-store to use when performing SSL client authentication. */ 394 private enum KeyManagerType { JVM, FILE, PKCS11 } 395 396 /** 397 * Configures a {@link X509KeyManager} using the provided JSON configuration. 398 * 399 * @param configuration 400 * The JSON object containing the key manager configuration. 401 * @return The configured key manager. 402 */ 403 public static X509KeyManager configureKeyManager(final JsonValue configuration) { 404 try { 405 return configureKeyManager(configuration, KeyManagerType.JVM); 406 } catch (GeneralSecurityException | IOException e) { 407 throw new IllegalArgumentException(ERR_CONFIG_INVALID_KEY_MANAGER.get( 408 configuration.getPointer(), e.getLocalizedMessage()).toString(), e); 409 } 410 } 411 412 private static X509KeyManager configureKeyManager(JsonValue config, KeyManagerType defaultIfMissing) 413 throws GeneralSecurityException, IOException { 414 final KeyManagerType keyManagerType = config.get("keyManager") 415 .defaultTo(defaultIfMissing) 416 .as(enumConstant(KeyManagerType.class)); 417 switch (keyManagerType) { 418 case JVM: 419 return useJvmDefaultKeyStore(); 420 case FILE: 421 final String fileName = config.get("fileBasedKeyManagerFile").required().asString(); 422 final String passwordFile = config.get("fileBasedKeyManagerPasswordFile").asString(); 423 final String password = passwordFile != null 424 ? readPasswordFromFile(passwordFile) : config.get("fileBasedKeyManagerPassword").asString(); 425 final String type = config.get("fileBasedKeyManagerType").asString(); 426 final String provider = config.get("fileBasedKeyManagerProvider").asString(); 427 return useKeyStoreFile(fileName, password != null ? password.toCharArray() : null, type, provider); 428 case PKCS11: 429 final String pkcs11PasswordFile = config.get("pkcs11KeyManagerPasswordFile").asString(); 430 return usePKCS11Token(pkcs11PasswordFile != null 431 ? readPasswordFromFile(pkcs11PasswordFile).toCharArray() : null); 432 default: 433 throw new IllegalArgumentException("Unsupported key-manager type: " + keyManagerType); 434 } 435 } 436 437 private static String readPasswordFromFile(String fileName) throws IOException { 438 try (final BufferedReader reader = new BufferedReader(new FileReader(new File(fileName)))) { 439 return reader.readLine(); 440 } 441 } 442 443 /** 444 * Configures a {@link TrustManager} using the provided JSON configuration. 445 * 446 * @param configuration 447 * The JSON object containing the trust manager configuration. 448 * @return The configured trust manager. 449 */ 450 public static TrustManager configureTrustManager(final JsonValue configuration) { 451 try { 452 return configureTrustManager(configuration, TrustManagerType.JVM); 453 } catch (GeneralSecurityException | IOException e) { 454 throw new IllegalArgumentException(ERR_CONFIG_INVALID_TRUST_MANAGER.get( 455 configuration.getPointer(), e.getLocalizedMessage()).toString(), e); 456 } 457 } 458 459 private static TrustManager configureTrustManager(JsonValue config, TrustManagerType defaultIfMissing) 460 throws GeneralSecurityException, IOException { 461 final TrustManagerType trustManagerType = config.get("trustManager") 462 .defaultTo(defaultIfMissing) 463 .as(enumConstant(TrustManagerType.class)); 464 switch (trustManagerType) { 465 case TRUSTALL: 466 return trustAll(); 467 case JVM: 468 return null; 469 case FILE: 470 final String fileName = config.get("fileBasedTrustManagerFile").required().asString(); 471 final String passwordFile = config.get("fileBasedTrustManagerPasswordFile").asString(); 472 final String password = passwordFile != null 473 ? readPasswordFromFile(passwordFile) : config.get("fileBasedTrustManagerPassword").asString(); 474 final String type = config.get("fileBasedTrustManagerType").asString(); 475 return checkUsingTrustStore(fileName, password != null ? password.toCharArray() : null, type); 476 default: 477 throw new IllegalArgumentException("Unsupported trust-manager type: " + trustManagerType); 478 } 479 } 480 481 /** 482 * Creates a new connection factory using the named configuration in the provided JSON list of factory 483 * configurations. See the sample configuration file for a detailed description of its content. 484 * 485 * @param configuration 486 * The JSON configuration. 487 * @param name 488 * The name of the connection factory configuration to be parsed. 489 * @param trustManager 490 * The trust manager to use for secure connection. Can be {@code null} 491 * to use the default JVM trust manager. 492 * @param keyManager 493 * The key manager to use for secure connection. Can be {@code null} 494 * to use the default JVM key manager. 495 * @param providerClassLoader 496 * The {@link ClassLoader} used to fetch the {@link org.forgerock.opendj.ldap.spi.TransportProvider}. This 497 * can be useful in OSGI environments. 498 * @return A new connection factory using the provided JSON configuration. 499 * @throws IllegalArgumentException 500 * If the configuration is invalid. 501 */ 502 public static ConnectionFactory configureConnectionFactory(final JsonValue configuration, 503 final String name, 504 final TrustManager trustManager, 505 final X509KeyManager keyManager, 506 final ClassLoader providerClassLoader) { 507 final JsonValue normalizedConfiguration = normalizeConnectionFactory(configuration, name, 0); 508 return configureConnectionFactory(normalizedConfiguration, trustManager, keyManager, providerClassLoader); 509 } 510 511 /** 512 * Creates a new connection factory using the named configuration in the provided JSON list of factory 513 * configurations. See the sample configuration file for a detailed description of its content. 514 * 515 * @param configuration 516 * The JSON configuration. 517 * @param name 518 * The name of the connection factory configuration to be parsed. 519 * @param trustManager 520 * The trust manager to use for secure connection. Can be {@code null} 521 * to use the default JVM trust manager. 522 * @param keyManager 523 * The key manager to use for secure connection. Can be {@code null} 524 * to use the default JVM key manager. 525 * @return A new connection factory using the provided JSON configuration. 526 * @throws IllegalArgumentException 527 * If the configuration is invalid. 528 */ 529 public static ConnectionFactory configureConnectionFactory(final JsonValue configuration, 530 final String name, 531 final TrustManager trustManager, 532 final X509KeyManager keyManager) { 533 return configureConnectionFactory(configuration, name, trustManager, keyManager, null); 534 } 535 536 private static ConnectionFactory configureConnectionFactory(final JsonValue configuration, 537 final TrustManager trustManager, 538 final X509KeyManager keyManager, 539 final ClassLoader providerClassLoader) { 540 final long heartBeatIntervalSeconds = configuration.get("heartBeatIntervalSeconds").defaultTo(30L).asLong(); 541 final Duration heartBeatInterval = duration(Math.max(heartBeatIntervalSeconds, 1L), TimeUnit.SECONDS); 542 543 final long heartBeatTimeoutMillis = configuration.get("heartBeatTimeoutMilliSeconds").defaultTo(500L).asLong(); 544 final Duration heartBeatTimeout = duration(Math.max(heartBeatTimeoutMillis, 100L), TimeUnit.MILLISECONDS); 545 546 final Options options = Options.defaultOptions() 547 .set(TRANSPORT_PROVIDER_CLASS_LOADER, providerClassLoader) 548 .set(HEARTBEAT_ENABLED, true) 549 .set(HEARTBEAT_INTERVAL, heartBeatInterval) 550 .set(HEARTBEAT_TIMEOUT, heartBeatTimeout) 551 .set(LOAD_BALANCER_MONITORING_INTERVAL, heartBeatInterval); 552 553 // Parse pool parameters, 554 final int connectionPoolSize = 555 Math.max(configuration.get("connectionPoolSize").defaultTo(10).asInteger(), 1); 556 557 // Parse authentication parameters. 558 if (configuration.isDefined("authentication")) { 559 final JsonValue authn = configuration.get("authentication"); 560 if (authn.isDefined("simple")) { 561 final JsonValue simple = authn.get("simple"); 562 final BindRequest bindRequest = 563 Requests.newSimpleBindRequest(simple.get("bindDn").required().asString(), 564 simple.get("bindPassword").required().asString().toCharArray()); 565 options.set(AUTHN_BIND_REQUEST, bindRequest); 566 } else { 567 throw new LocalizedIllegalArgumentException(ERR_CONFIG_INVALID_AUTHENTICATION.get()); 568 } 569 } 570 571 // Parse SSL/StartTLS parameters. 572 final ConnectionSecurity connectionSecurity = configuration.get("connectionSecurity") 573 .defaultTo(ConnectionSecurity.NONE) 574 .as(enumConstant(ConnectionSecurity.class)); 575 if (connectionSecurity != ConnectionSecurity.NONE) { 576 try { 577 // Configure SSL. 578 final SSLContextBuilder builder = new SSLContextBuilder(); 579 builder.setTrustManager(trustManager); 580 final String sslCertAlias = configuration.get("sslCertAlias").asString(); 581 builder.setKeyManager(sslCertAlias != null 582 ? useSingleCertificate(sslCertAlias, keyManager) 583 : keyManager); 584 options.set(SSL_CONTEXT, builder.getSSLContext()); 585 options.set(SSL_USE_STARTTLS, connectionSecurity == ConnectionSecurity.STARTTLS); 586 } catch (GeneralSecurityException e) { 587 // Rethrow as unchecked exception. 588 throw new IllegalArgumentException(e); 589 } 590 } 591 592 // Parse primary data center. 593 final JsonValue primaryLdapServers = configuration.get("primaryLdapServers"); 594 if (!primaryLdapServers.isList() || primaryLdapServers.size() == 0) { 595 throw new IllegalArgumentException("No primaryLdapServers"); 596 } 597 final ConnectionFactory primary = parseLdapServers(primaryLdapServers, connectionPoolSize, options); 598 599 // Parse secondary data center(s). 600 final JsonValue secondaryLdapServers = configuration.get("secondaryLdapServers"); 601 ConnectionFactory secondary = null; 602 if (secondaryLdapServers.isList()) { 603 if (secondaryLdapServers.size() > 0) { 604 secondary = parseLdapServers(secondaryLdapServers, connectionPoolSize, options); 605 } 606 } else if (!secondaryLdapServers.isNull()) { 607 throw new LocalizedIllegalArgumentException(ERR_CONFIG_INVALID_SECONDARY_LDAP_SERVER.get()); 608 } 609 610 // Create fail-over. 611 if (secondary != null) { 612 return newFailoverLoadBalancer(asList(primary, secondary), options); 613 } else { 614 return primary; 615 } 616 } 617 618 private static JsonValue normalizeConnectionFactory(final JsonValue configuration, 619 final String name, final int depth) { 620 // Protect against infinite recursion in the configuration. 621 if (depth > 100) { 622 throw new LocalizedIllegalArgumentException(ERR_CONFIG_SERVER_CIRCULAR_DEPENDENCIES.get(name)); 623 } 624 625 final JsonValue current = configuration.get(name).required(); 626 if (current.isDefined("inheritFrom")) { 627 // Inherit missing fields from inherited configuration. 628 final JsonValue parent = 629 normalizeConnectionFactory(configuration, 630 current.get("inheritFrom").asString(), depth + 1); 631 final Map<String, Object> normalized = new LinkedHashMap<>(parent.asMap()); 632 normalized.putAll(current.asMap()); 633 normalized.remove("inheritFrom"); 634 return new JsonValue(normalized); 635 } else { 636 // No normalization required. 637 return current; 638 } 639 } 640 641 private static ConnectionFactory parseLdapServers(JsonValue config, int poolSize, Options options) { 642 final List<ConnectionFactory> servers = new ArrayList<>(config.size()); 643 for (final JsonValue server : config) { 644 final String host = server.get("hostname").required().asString(); 645 final int port = server.get("port").required().asInteger(); 646 final ConnectionFactory factory = new LDAPConnectionFactory(host, port, options); 647 if (poolSize > 1) { 648 servers.add(newCachedConnectionPool(factory, 0, poolSize, 60L, TimeUnit.SECONDS)); 649 } else { 650 servers.add(factory); 651 } 652 } 653 if (servers.size() > 1) { 654 return newRoundRobinLoadBalancer(servers, options); 655 } else { 656 return servers.get(0); 657 } 658 } 659 660 private Rest2LdapJsonConfigurator() { 661 // Prevent instantiation. 662 } 663}