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 2013-2015 ForgeRock AS. 015 */ 016package org.forgerock.opendj.rest2ldap; 017 018import static java.util.Arrays.asList; 019import static org.forgerock.json.resource.ResourceException.newResourceException; 020import static org.forgerock.opendj.ldap.Connections.newCachedConnectionPool; 021import static org.forgerock.opendj.ldap.Connections.newFailoverLoadBalancer; 022import static org.forgerock.opendj.ldap.Connections.newRoundRobinLoadBalancer; 023import static org.forgerock.opendj.ldap.LDAPConnectionFactory.*; 024import static org.forgerock.opendj.ldap.LoadBalancingAlgorithm.LOAD_BALANCER_MONITORING_INTERVAL; 025import static org.forgerock.opendj.ldap.requests.Requests.newSearchRequest; 026import static org.forgerock.opendj.ldap.schema.CoreSchema.getEntryUUIDAttributeType; 027import static org.forgerock.opendj.rest2ldap.ReadOnUpdatePolicy.CONTROLS; 028import static org.forgerock.opendj.rest2ldap.Utils.ensureNotNull; 029 030import java.io.IOException; 031import java.security.GeneralSecurityException; 032import java.util.ArrayList; 033import java.util.LinkedHashMap; 034import java.util.LinkedList; 035import java.util.List; 036import java.util.Map; 037import java.util.Set; 038import java.util.concurrent.TimeUnit; 039 040import org.forgerock.json.JsonValue; 041import org.forgerock.json.JsonValueException; 042import org.forgerock.json.resource.BadRequestException; 043import org.forgerock.json.resource.CollectionResourceProvider; 044import org.forgerock.json.resource.ResourceException; 045import org.forgerock.opendj.ldap.AssertionFailureException; 046import org.forgerock.opendj.ldap.Attribute; 047import org.forgerock.opendj.ldap.AttributeDescription; 048import org.forgerock.opendj.ldap.AuthenticationException; 049import org.forgerock.opendj.ldap.AuthorizationException; 050import org.forgerock.opendj.ldap.ByteString; 051import org.forgerock.opendj.ldap.ConnectionException; 052import org.forgerock.opendj.ldap.ConnectionFactory; 053import org.forgerock.opendj.ldap.ConstraintViolationException; 054import org.forgerock.opendj.ldap.DN; 055import org.forgerock.opendj.ldap.Entry; 056import org.forgerock.opendj.ldap.EntryNotFoundException; 057import org.forgerock.opendj.ldap.Filter; 058import org.forgerock.opendj.ldap.LDAPConnectionFactory; 059import org.forgerock.opendj.ldap.LdapException; 060import org.forgerock.opendj.ldap.LinkedAttribute; 061import org.forgerock.opendj.ldap.MultipleEntriesFoundException; 062import org.forgerock.opendj.ldap.RDN; 063import org.forgerock.opendj.ldap.ResultCode; 064import org.forgerock.opendj.ldap.SSLContextBuilder; 065import org.forgerock.opendj.ldap.SearchScope; 066import org.forgerock.opendj.ldap.TimeoutResultException; 067import org.forgerock.opendj.ldap.TrustManagers; 068import org.forgerock.opendj.ldap.requests.BindRequest; 069import org.forgerock.opendj.ldap.requests.Requests; 070import org.forgerock.opendj.ldap.requests.SearchRequest; 071import org.forgerock.opendj.ldap.schema.AttributeType; 072import org.forgerock.opendj.ldap.schema.Schema; 073import org.forgerock.util.Options; 074import org.forgerock.util.time.Duration; 075 076/** Provides core factory methods and builders for constructing LDAP resource collections. */ 077public final class Rest2LDAP { 078 /** Indicates whether or not LDAP client connections should use SSL or StartTLS. */ 079 private enum ConnectionSecurity { 080 NONE, SSL, STARTTLS 081 } 082 083 /** 084 * Specifies the mechanism which should be used for trusting certificates 085 * presented by the LDAP server. 086 */ 087 private enum TrustManagerType { 088 TRUSTALL, JVM, FILE 089 } 090 091 /** A builder for incrementally constructing LDAP resource collections. */ 092 public static final class Builder { 093 private final List<Attribute> additionalLDAPAttributes = new LinkedList<>(); 094 private AuthorizationPolicy authzPolicy = AuthorizationPolicy.NONE; 095 private DN baseDN; // TODO: support template variables. 096 private AttributeDescription etagAttribute; 097 private ConnectionFactory factory; 098 private NameStrategy nameStrategy; 099 private AuthzIdTemplate proxiedAuthzTemplate; 100 private ReadOnUpdatePolicy readOnUpdatePolicy = CONTROLS; 101 private AttributeMapper rootMapper; 102 private Schema schema = Schema.getDefaultSchema(); 103 private boolean usePermissiveModify; 104 private boolean useSubtreeDelete; 105 106 private Builder() { 107 useEtagAttribute(); 108 useClientDNNaming("uid"); 109 } 110 111 /** 112 * Specifies an additional LDAP attribute which should be included with 113 * new LDAP entries when they are created. Use this method to specify 114 * the LDAP objectClass attribute. 115 * 116 * @param attribute 117 * The additional LDAP attribute to be included with new LDAP 118 * entries. 119 * @return A reference to this LDAP resource collection builder. 120 */ 121 public Builder additionalLDAPAttribute(final Attribute attribute) { 122 additionalLDAPAttributes.add(attribute); 123 return this; 124 } 125 126 /** 127 * Specifies an additional LDAP attribute which should be included with 128 * new LDAP entries when they are created. Use this method to specify 129 * the LDAP objectClass attribute. 130 * 131 * @param attribute 132 * The name of the additional LDAP attribute to be included 133 * with new LDAP entries. 134 * @param values 135 * The value(s) of the additional LDAP attribute. 136 * @return A reference to this LDAP resource collection builder. 137 */ 138 public Builder additionalLDAPAttribute(final String attribute, final Object... values) { 139 return additionalLDAPAttribute(new LinkedAttribute(ad(attribute), values)); 140 } 141 142 /** 143 * Sets the policy which should be for performing authorization. 144 * 145 * @param policy 146 * The policy which should be for performing authorization. 147 * @return A reference to this LDAP resource collection builder. 148 */ 149 public Builder authorizationPolicy(final AuthorizationPolicy policy) { 150 this.authzPolicy = ensureNotNull(policy); 151 return this; 152 } 153 154 /** 155 * Sets the base DN beneath which LDAP entries (resources) are to be found. 156 * 157 * @param dn 158 * The base DN. 159 * @return A reference to this LDAP resource collection builder. 160 */ 161 public Builder baseDN(final DN dn) { 162 ensureNotNull(dn); 163 this.baseDN = dn; 164 return this; 165 } 166 167 /** 168 * Sets the base DN beneath which LDAP entries (resources) are to be found. 169 * 170 * @param dn 171 * The base DN. 172 * @return A reference to this LDAP resource collection builder. 173 */ 174 public Builder baseDN(final String dn) { 175 return baseDN(DN.valueOf(dn, schema)); 176 } 177 178 /** 179 * Creates a new LDAP resource collection configured using this builder. 180 * 181 * @return The new LDAP resource collection. 182 */ 183 public CollectionResourceProvider build() { 184 ensureNotNull(baseDN); 185 if (rootMapper == null) { 186 throw new IllegalStateException("No mappings provided"); 187 } 188 switch (authzPolicy) { 189 case NONE: 190 if (factory == null) { 191 throw new IllegalStateException( 192 "A connection factory must be specified when the authorization policy is 'none'"); 193 } 194 break; 195 case PROXY: 196 if (proxiedAuthzTemplate == null) { 197 throw new IllegalStateException( 198 "Proxied authorization enabled but no template defined"); 199 } 200 if (factory == null) { 201 throw new IllegalStateException( 202 "A connection factory must be specified when using proxied authorization"); 203 } 204 break; 205 case REUSE: 206 // This is always ok. 207 break; 208 } 209 return new LDAPCollectionResourceProvider(baseDN, rootMapper, nameStrategy, 210 etagAttribute, new Config(factory, readOnUpdatePolicy, authzPolicy, 211 proxiedAuthzTemplate, useSubtreeDelete, usePermissiveModify, schema), 212 additionalLDAPAttributes); 213 } 214 215 /** 216 * Configures the JSON to LDAP mapping using the provided JSON 217 * configuration. The caller is still required to set the connection 218 * factory. See the sample configuration file for a detailed description 219 * of its content. 220 * 221 * @param configuration 222 * The JSON configuration. 223 * @return A reference to this LDAP resource collection builder. 224 * @throws IllegalArgumentException 225 * If the configuration is invalid. 226 */ 227 public Builder configureMapping(final JsonValue configuration) { 228 baseDN(configuration.get("baseDN").required().asString()); 229 230 final JsonValue readOnUpdatePolicy = configuration.get("readOnUpdatePolicy"); 231 if (!readOnUpdatePolicy.isNull()) { 232 readOnUpdatePolicy(readOnUpdatePolicy.asEnum(ReadOnUpdatePolicy.class)); 233 } 234 235 for (final JsonValue v : configuration.get("additionalLDAPAttributes")) { 236 final String type = v.get("type").required().asString(); 237 final List<Object> values = v.get("values").required().asList(); 238 additionalLDAPAttribute(new LinkedAttribute(type, values)); 239 } 240 241 final JsonValue namingStrategy = configuration.get("namingStrategy"); 242 if (!namingStrategy.isNull()) { 243 final String name = namingStrategy.get("strategy").required().asString(); 244 if (name.equalsIgnoreCase("clientDNNaming")) { 245 useClientDNNaming(namingStrategy.get("dnAttribute").required().asString()); 246 } else if (name.equalsIgnoreCase("clientNaming")) { 247 useClientNaming(namingStrategy.get("dnAttribute").required().asString(), 248 namingStrategy.get("idAttribute").required().asString()); 249 } else if (name.equalsIgnoreCase("serverNaming")) { 250 useServerNaming(namingStrategy.get("dnAttribute").required().asString(), 251 namingStrategy.get("idAttribute").required().asString()); 252 } else { 253 throw new IllegalArgumentException( 254 "Illegal naming strategy. Must be one of: clientDNNaming, clientNaming, or serverNaming"); 255 } 256 } 257 258 final JsonValue etagAttribute = configuration.get("etagAttribute"); 259 if (!etagAttribute.isNull()) { 260 useEtagAttribute(etagAttribute.asString()); 261 } 262 263 /* 264 * Default to false, even though it is supported by OpenDJ, because 265 * it requires additional permissions. 266 */ 267 if (configuration.get("useSubtreeDelete").defaultTo(false).asBoolean()) { 268 useSubtreeDelete(); 269 } 270 271 /* 272 * Default to true because it is supported by OpenDJ and does not 273 * require additional permissions. 274 */ 275 if (configuration.get("usePermissiveModify").defaultTo(true).asBoolean()) { 276 usePermissiveModify(); 277 } 278 279 mapper(configureObjectMapper(configuration.get("attributes").required())); 280 281 return this; 282 } 283 284 /** 285 * Sets the LDAP connection factory to be used for accessing the LDAP 286 * directory. Each HTTP request will obtain a single connection from the 287 * factory and then close it once the HTTP response has been sent. It is 288 * recommended that the provided connection factory supports connection 289 * pooling. 290 * 291 * @param factory 292 * The LDAP connection factory to be used for accessing the 293 * LDAP directory. 294 * @return A reference to this LDAP resource collection builder. 295 */ 296 public Builder ldapConnectionFactory(final ConnectionFactory factory) { 297 this.factory = factory; 298 return this; 299 } 300 301 /** 302 * Sets the attribute mapper which should be used for mapping JSON 303 * resources to and from LDAP entries. 304 * 305 * @param mapper 306 * The attribute mapper. 307 * @return A reference to this LDAP resource collection builder. 308 */ 309 public Builder mapper(final AttributeMapper mapper) { 310 this.rootMapper = mapper; 311 return this; 312 } 313 314 /** 315 * Sets the authorization ID template which will be used for proxied 316 * authorization. Template parameters are specified by including the 317 * parameter name surrounded by curly braces. The template should 318 * contain fields which are expected to be found in the security context 319 * create during authentication, e.g. "dn:{dn}" or "u:{id}". 320 * 321 * @param template 322 * The authorization ID template which will be used for 323 * proxied authorization. 324 * @return A reference to this LDAP resource collection builder. 325 */ 326 public Builder proxyAuthzIdTemplate(final String template) { 327 this.proxiedAuthzTemplate = template != null ? new AuthzIdTemplate(template) : null; 328 return this; 329 } 330 331 /** 332 * Sets the policy which should be used in order to read an entry before 333 * it is deleted, or after it is added or modified. The default read on 334 * update policy is to use {@link ReadOnUpdatePolicy#CONTROLS controls}. 335 * 336 * @param policy 337 * The policy which should be used in order to read an entry 338 * before it is deleted, or after it is added or modified. 339 * @return A reference to this LDAP resource collection builder. 340 */ 341 public Builder readOnUpdatePolicy(final ReadOnUpdatePolicy policy) { 342 this.readOnUpdatePolicy = ensureNotNull(policy); 343 return this; 344 } 345 346 /** 347 * Sets the schema which should be used when attribute types and 348 * controls. 349 * 350 * @param schema 351 * The schema which should be used when attribute types and 352 * controls. 353 * @return A reference to this LDAP resource collection builder. 354 */ 355 public Builder schema(final Schema schema) { 356 this.schema = ensureNotNull(schema); 357 return this; 358 } 359 360 /** 361 * Indicates that the JSON resource ID must be provided by the user, and 362 * will be used for naming the associated LDAP entry. More specifically, 363 * LDAP entry names will be derived by appending a single RDN to the 364 * {@link #baseDN(String) base DN} composed of the specified attribute 365 * type and LDAP value taken from the LDAP entry once attribute mapping 366 * has been performed. 367 * <p> 368 * Note that this naming policy requires that the user provides the 369 * resource name when creating new resources, which means it must be 370 * included in the resource content when not specified explicitly in the 371 * create request. 372 * 373 * @param attribute 374 * The LDAP attribute which will be used for naming. 375 * @return A reference to this LDAP resource collection builder. 376 */ 377 public Builder useClientDNNaming(final AttributeType attribute) { 378 this.nameStrategy = new DNNameStrategy(attribute); 379 return this; 380 } 381 382 /** 383 * Indicates that the JSON resource ID must be provided by the user, and 384 * will be used for naming the associated LDAP entry. More specifically, 385 * LDAP entry names will be derived by appending a single RDN to the 386 * {@link #baseDN(String) base DN} composed of the specified attribute 387 * type and LDAP value taken from the LDAP entry once attribute mapping 388 * has been performed. 389 * <p> 390 * Note that this naming policy requires that the user provides the 391 * resource name when creating new resources, which means it must be 392 * included in the resource content when not specified explicitly in the 393 * create request. 394 * 395 * @param attribute 396 * The LDAP attribute which will be used for naming. 397 * @return A reference to this LDAP resource collection builder. 398 */ 399 public Builder useClientDNNaming(final String attribute) { 400 return useClientDNNaming(at(attribute)); 401 } 402 403 /** 404 * Indicates that the JSON resource ID must be provided by the user, but 405 * will not be used for naming the associated LDAP entry. Instead the 406 * JSON resource ID will be taken from the {@code idAttribute} in the 407 * LDAP entry, and the LDAP entry name will be derived by appending a 408 * single RDN to the {@link #baseDN(String) base DN} composed of the 409 * {@code dnAttribute} taken from the LDAP entry once attribute mapping 410 * has been performed. 411 * <p> 412 * Note that this naming policy requires that the user provides the 413 * resource name when creating new resources, which means it must be 414 * included in the resource content when not specified explicitly in the 415 * create request. 416 * 417 * @param dnAttribute 418 * The attribute which will be used for naming LDAP entries. 419 * @param idAttribute 420 * The attribute which will be used for JSON resource IDs. 421 * @return A reference to this LDAP resource collection builder. 422 */ 423 public Builder useClientNaming(final AttributeType dnAttribute, 424 final AttributeDescription idAttribute) { 425 this.nameStrategy = new AttributeNameStrategy(dnAttribute, idAttribute, false); 426 return this; 427 } 428 429 /** 430 * Indicates that the JSON resource ID must be provided by the user, but 431 * will not be used for naming the associated LDAP entry. Instead the 432 * JSON resource ID will be taken from the {@code idAttribute} in the 433 * LDAP entry, and the LDAP entry name will be derived by appending a 434 * single RDN to the {@link #baseDN(String) base DN} composed of the 435 * {@code dnAttribute} taken from the LDAP entry once attribute mapping 436 * has been performed. 437 * <p> 438 * Note that this naming policy requires that the user provides the 439 * resource name when creating new resources, which means it must be 440 * included in the resource content when not specified explicitly in the 441 * create request. 442 * 443 * @param dnAttribute 444 * The attribute which will be used for naming LDAP entries. 445 * @param idAttribute 446 * The attribute which will be used for JSON resource IDs. 447 * @return A reference to this LDAP resource collection builder. 448 */ 449 public Builder useClientNaming(final String dnAttribute, final String idAttribute) { 450 return useClientNaming(at(dnAttribute), ad(idAttribute)); 451 } 452 453 /** 454 * Indicates that the "etag" LDAP attribute should be used for resource 455 * versioning. This is the default behavior. 456 * 457 * @return A reference to this LDAP resource collection builder. 458 */ 459 public Builder useEtagAttribute() { 460 return useEtagAttribute("etag"); 461 } 462 463 /** 464 * Indicates that the provided LDAP attribute should be used for 465 * resource versioning. The "etag" attribute will be used by default. 466 * 467 * @param attribute 468 * The name of the attribute to use for versioning, or 469 * {@code null} if resource versioning will not supported. 470 * @return A reference to this LDAP resource collection builder. 471 */ 472 public Builder useEtagAttribute(final AttributeDescription attribute) { 473 this.etagAttribute = attribute; 474 return this; 475 } 476 477 /** 478 * Indicates that the provided LDAP attribute should be used for 479 * resource versioning. The "etag" attribute will be used by default. 480 * 481 * @param attribute 482 * The name of the attribute to use for versioning, or 483 * {@code null} if resource versioning will not supported. 484 * @return A reference to this LDAP resource collection builder. 485 */ 486 public Builder useEtagAttribute(final String attribute) { 487 return useEtagAttribute(attribute != null ? ad(attribute) : null); 488 } 489 490 /** 491 * Indicates that all LDAP modify operations should be performed using 492 * the LDAP permissive modify control. The default behavior is to not 493 * use the permissive modify control. Use of the control is strongly 494 * recommended. 495 * 496 * @return A reference to this LDAP resource collection builder. 497 */ 498 public Builder usePermissiveModify() { 499 this.usePermissiveModify = true; 500 return this; 501 } 502 503 /** 504 * Indicates that the JSON resource ID will be derived from the server 505 * provided "entryUUID" LDAP attribute. The LDAP entry name will be 506 * derived by appending a single RDN to the {@link #baseDN(String) base 507 * DN} composed of the {@code dnAttribute} taken from the LDAP entry 508 * once attribute mapping has been performed. 509 * <p> 510 * Note that this naming policy requires that the server provides the 511 * resource name when creating new resources, which means it must not be 512 * specified in the create request, nor included in the resource 513 * content. 514 * 515 * @param dnAttribute 516 * The attribute which will be used for naming LDAP entries. 517 * @return A reference to this LDAP resource collection builder. 518 */ 519 public Builder useServerEntryUUIDNaming(final AttributeType dnAttribute) { 520 return useServerNaming(dnAttribute, AttributeDescription 521 .create(getEntryUUIDAttributeType())); 522 } 523 524 /** 525 * Indicates that the JSON resource ID will be derived from the server 526 * provided "entryUUID" LDAP attribute. The LDAP entry name will be 527 * derived by appending a single RDN to the {@link #baseDN(String) base 528 * DN} composed of the {@code dnAttribute} taken from the LDAP entry 529 * once attribute mapping has been performed. 530 * <p> 531 * Note that this naming policy requires that the server provides the 532 * resource name when creating new resources, which means it must not be 533 * specified in the create request, nor included in the resource 534 * content. 535 * 536 * @param dnAttribute 537 * The attribute which will be used for naming LDAP entries. 538 * @return A reference to this LDAP resource collection builder. 539 */ 540 public Builder useServerEntryUUIDNaming(final String dnAttribute) { 541 return useServerEntryUUIDNaming(at(dnAttribute)); 542 } 543 544 /** 545 * Indicates that the JSON resource ID must not be provided by the user, 546 * and will not be used for naming the associated LDAP entry. Instead 547 * the JSON resource ID will be taken from the {@code idAttribute} in 548 * the LDAP entry, and the LDAP entry name will be derived by appending 549 * a single RDN to the {@link #baseDN(String) base DN} composed of the 550 * {@code dnAttribute} taken from the LDAP entry once attribute mapping 551 * has been performed. 552 * <p> 553 * Note that this naming policy requires that the server provides the 554 * resource name when creating new resources, which means it must not be 555 * specified in the create request, nor included in the resource 556 * content. 557 * 558 * @param dnAttribute 559 * The attribute which will be used for naming LDAP entries. 560 * @param idAttribute 561 * The attribute which will be used for JSON resource IDs. 562 * @return A reference to this LDAP resource collection builder. 563 */ 564 public Builder useServerNaming(final AttributeType dnAttribute, 565 final AttributeDescription idAttribute) { 566 this.nameStrategy = new AttributeNameStrategy(dnAttribute, idAttribute, true); 567 return this; 568 } 569 570 /** 571 * Indicates that the JSON resource ID must not be provided by the user, 572 * and will not be used for naming the associated LDAP entry. Instead 573 * the JSON resource ID will be taken from the {@code idAttribute} in 574 * the LDAP entry, and the LDAP entry name will be derived by appending 575 * a single RDN to the {@link #baseDN(String) base DN} composed of the 576 * {@code dnAttribute} taken from the LDAP entry once attribute mapping 577 * has been performed. 578 * <p> 579 * Note that this naming policy requires that the server provides the 580 * resource name when creating new resources, which means it must not be 581 * specified in the create request, nor included in the resource 582 * content. 583 * 584 * @param dnAttribute 585 * The attribute which will be used for naming LDAP entries. 586 * @param idAttribute 587 * The attribute which will be used for JSON resource IDs. 588 * @return A reference to this LDAP resource collection builder. 589 */ 590 public Builder useServerNaming(final String dnAttribute, final String idAttribute) { 591 return useServerNaming(at(dnAttribute), ad(idAttribute)); 592 } 593 594 /** 595 * Indicates that all LDAP delete operations should be performed using 596 * the LDAP subtree delete control. The default behavior is to not use 597 * the subtree delete control. 598 * 599 * @return A reference to this LDAP resource collection builder. 600 */ 601 public Builder useSubtreeDelete() { 602 this.useSubtreeDelete = true; 603 return this; 604 } 605 606 private AttributeDescription ad(final String attribute) { 607 return AttributeDescription.valueOf(attribute, schema); 608 } 609 610 private AttributeType at(final String attribute) { 611 return schema.getAttributeType(attribute); 612 } 613 614 private AttributeMapper configureMapper(final JsonValue mapper) { 615 if (mapper.isDefined("constant")) { 616 return constant(mapper.get("constant").getObject()); 617 } else if (mapper.isDefined("simple")) { 618 final JsonValue config = mapper.get("simple"); 619 final SimpleAttributeMapper s = 620 simple(ad(config.get("ldapAttribute").required().asString())); 621 if (config.isDefined("defaultJSONValue")) { 622 s.defaultJSONValue(config.get("defaultJSONValue").getObject()); 623 } 624 if (config.get("isBinary").defaultTo(false).asBoolean()) { 625 s.isBinary(); 626 } 627 if (config.get("isRequired").defaultTo(false).asBoolean()) { 628 s.isRequired(); 629 } 630 if (config.get("isSingleValued").defaultTo(false).asBoolean()) { 631 s.isSingleValued(); 632 } 633 s.writability(parseWritability(mapper, config)); 634 return s; 635 } else if (mapper.isDefined("reference")) { 636 final JsonValue config = mapper.get("reference"); 637 final AttributeDescription ldapAttribute = 638 ad(config.get("ldapAttribute").required().asString()); 639 final DN baseDN = DN.valueOf(config.get("baseDN").required().asString(), schema); 640 final AttributeDescription primaryKey = 641 ad(config.get("primaryKey").required().asString()); 642 final AttributeMapper m = configureMapper(config.get("mapper").required()); 643 final ReferenceAttributeMapper r = reference(ldapAttribute, baseDN, primaryKey, m); 644 if (config.get("isRequired").defaultTo(false).asBoolean()) { 645 r.isRequired(); 646 } 647 if (config.get("isSingleValued").defaultTo(false).asBoolean()) { 648 r.isSingleValued(); 649 } 650 if (config.isDefined("searchFilter")) { 651 r.searchFilter(config.get("searchFilter").asString()); 652 } 653 r.writability(parseWritability(mapper, config)); 654 return r; 655 } else if (mapper.isDefined("object")) { 656 return configureObjectMapper(mapper.get("object")); 657 } else { 658 throw new JsonValueException(mapper, 659 "Illegal mapping: must contain constant, simple, or object"); 660 } 661 } 662 663 private ObjectAttributeMapper configureObjectMapper(final JsonValue mapper) { 664 final ObjectAttributeMapper object = object(); 665 for (final String attribute : mapper.keys()) { 666 object.attribute(attribute, configureMapper(mapper.get(attribute))); 667 } 668 return object; 669 } 670 671 private WritabilityPolicy parseWritability(final JsonValue mapper, final JsonValue config) { 672 if (config.isDefined("writability")) { 673 final String writability = config.get("writability").asString(); 674 if (writability.equalsIgnoreCase("readOnly")) { 675 return WritabilityPolicy.READ_ONLY; 676 } else if (writability.equalsIgnoreCase("readOnlyDiscardWrites")) { 677 return WritabilityPolicy.READ_ONLY_DISCARD_WRITES; 678 } else if (writability.equalsIgnoreCase("createOnly")) { 679 return WritabilityPolicy.CREATE_ONLY; 680 } else if (writability.equalsIgnoreCase("createOnlyDiscardWrites")) { 681 return WritabilityPolicy.CREATE_ONLY_DISCARD_WRITES; 682 } else if (writability.equalsIgnoreCase("readWrite")) { 683 return WritabilityPolicy.READ_WRITE; 684 } else { 685 throw new JsonValueException(mapper, 686 "Illegal writability: must be one of readOnly, readOnlyDiscardWrites, " 687 + "createOnly, createOnlyDiscardWrites, or readWrite"); 688 } 689 } else { 690 return WritabilityPolicy.READ_WRITE; 691 } 692 } 693 } 694 695 private static final class AttributeNameStrategy extends NameStrategy { 696 private final AttributeDescription dnAttribute; 697 private final AttributeDescription idAttribute; 698 private final boolean isServerProvided; 699 700 private AttributeNameStrategy(final AttributeType dnAttribute, 701 final AttributeDescription idAttribute, final boolean isServerProvided) { 702 this.dnAttribute = AttributeDescription.create(dnAttribute); 703 if (this.dnAttribute.equals(idAttribute)) { 704 throw new IllegalArgumentException("DN and ID attributes must be different"); 705 } 706 this.idAttribute = ensureNotNull(idAttribute); 707 this.isServerProvided = isServerProvided; 708 } 709 710 @Override 711 SearchRequest createSearchRequest(final RequestState requestState, final DN baseDN, final String resourceId) { 712 return newSearchRequest(baseDN, SearchScope.SINGLE_LEVEL, Filter.equality(idAttribute 713 .toString(), resourceId)); 714 } 715 716 @Override 717 void getLDAPAttributes(final RequestState requestState, final Set<String> ldapAttributes) { 718 ldapAttributes.add(idAttribute.toString()); 719 } 720 721 @Override 722 String getResourceId(final RequestState requestState, final Entry entry) { 723 return entry.parseAttribute(idAttribute).asString(); 724 } 725 726 @Override 727 void setResourceId(final RequestState requestState, final DN baseDN, final String resourceId, 728 final Entry entry) throws ResourceException { 729 if (isServerProvided) { 730 if (resourceId != null) { 731 throw new BadRequestException("Resources cannot be created with a " 732 + "client provided resource ID"); 733 } 734 } else { 735 entry.addAttribute(new LinkedAttribute(idAttribute, ByteString.valueOfUtf8(resourceId))); 736 } 737 final String rdnValue = entry.parseAttribute(dnAttribute).asString(); 738 final RDN rdn = new RDN(dnAttribute.getAttributeType(), rdnValue); 739 entry.setName(baseDN.child(rdn)); 740 } 741 } 742 743 private static final class DNNameStrategy extends NameStrategy { 744 private final AttributeDescription attribute; 745 746 private DNNameStrategy(final AttributeType attribute) { 747 this.attribute = AttributeDescription.create(attribute); 748 } 749 750 @Override 751 SearchRequest createSearchRequest(final RequestState requestState, final DN baseDN, final String resourceId) { 752 return newSearchRequest(baseDN.child(rdn(resourceId)), SearchScope.BASE_OBJECT, Filter 753 .objectClassPresent()); 754 } 755 756 @Override 757 void getLDAPAttributes(final RequestState requestState, final Set<String> ldapAttributes) { 758 ldapAttributes.add(attribute.toString()); 759 } 760 761 @Override 762 String getResourceId(final RequestState requestState, final Entry entry) { 763 return entry.parseAttribute(attribute).asString(); 764 } 765 766 @Override 767 void setResourceId(final RequestState requestState, final DN baseDN, final String resourceId, 768 final Entry entry) throws ResourceException { 769 if (resourceId != null) { 770 entry.setName(baseDN.child(rdn(resourceId))); 771 entry.addAttribute(new LinkedAttribute(attribute, ByteString.valueOfUtf8(resourceId))); 772 } else if (entry.getAttribute(attribute) != null) { 773 entry.setName(baseDN.child(rdn(entry.parseAttribute(attribute).asString()))); 774 } else { 775 throw new BadRequestException("Resources cannot be created without a " 776 + "client provided resource ID"); 777 } 778 } 779 780 private RDN rdn(final String resourceId) { 781 return new RDN(attribute.getAttributeType(), resourceId); 782 } 783 } 784 785 /** 786 * Adapts a {@code Throwable} to a {@code ResourceException}. If the 787 * {@code Throwable} is an LDAP {@link LdapException} then an 788 * appropriate {@code ResourceException} is returned, otherwise an 789 * {@code InternalServerErrorException} is returned. 790 * 791 * @param t 792 * The {@code Throwable} to be converted. 793 * @return The equivalent resource exception. 794 */ 795 public static ResourceException asResourceException(final Throwable t) { 796 int resourceResultCode; 797 try { 798 throw t; 799 } catch (final ResourceException e) { 800 return e; 801 } catch (final AssertionFailureException e) { 802 resourceResultCode = ResourceException.VERSION_MISMATCH; 803 } catch (final ConstraintViolationException e) { 804 final ResultCode rc = e.getResult().getResultCode(); 805 if (rc.equals(ResultCode.ENTRY_ALREADY_EXISTS)) { 806 resourceResultCode = ResourceException.VERSION_MISMATCH; // Consistent with MVCC. 807 } else { 808 // Schema violation, etc. 809 resourceResultCode = ResourceException.BAD_REQUEST; 810 } 811 } catch (final AuthenticationException e) { 812 resourceResultCode = 401; 813 } catch (final AuthorizationException e) { 814 resourceResultCode = ResourceException.FORBIDDEN; 815 } catch (final ConnectionException e) { 816 resourceResultCode = ResourceException.UNAVAILABLE; 817 } catch (final EntryNotFoundException e) { 818 resourceResultCode = ResourceException.NOT_FOUND; 819 } catch (final MultipleEntriesFoundException e) { 820 resourceResultCode = ResourceException.INTERNAL_ERROR; 821 } catch (final TimeoutResultException e) { 822 resourceResultCode = 408; 823 } catch (final LdapException e) { 824 final ResultCode rc = e.getResult().getResultCode(); 825 if (rc.equals(ResultCode.ADMIN_LIMIT_EXCEEDED)) { 826 resourceResultCode = 413; // Request Entity Too Large 827 } else if (rc.equals(ResultCode.SIZE_LIMIT_EXCEEDED)) { 828 resourceResultCode = 413; // Request Entity Too Large 829 } else { 830 resourceResultCode = ResourceException.INTERNAL_ERROR; 831 } 832 } catch (final Throwable tmp) { 833 resourceResultCode = ResourceException.INTERNAL_ERROR; 834 } 835 return newResourceException(resourceResultCode, t.getMessage(), t); 836 } 837 838 /** 839 * Returns a builder for incrementally constructing LDAP resource 840 * collections. 841 * 842 * @return An LDAP resource collection builder. 843 */ 844 public static Builder builder() { 845 return new Builder(); 846 } 847 848 /** 849 * Creates a new connection factory using the named configuration in the 850 * provided JSON list of factory configurations. See the sample 851 * configuration file for a detailed description of its content. 852 * 853 * @param configuration 854 * The JSON configuration. 855 * @param name 856 * The name of the connection factory configuration to be parsed. 857 * @param providerClassLoader 858 * The {@link ClassLoader} used to fetch the 859 * {@link org.forgerock.opendj.ldap.spi.TransportProvider}. 860 * This can be useful in OSGI environments. 861 * @return A new connection factory using the provided JSON configuration. 862 * @throws IllegalArgumentException 863 * If the configuration is invalid. 864 */ 865 public static ConnectionFactory configureConnectionFactory(final JsonValue configuration, 866 final String name, 867 final ClassLoader providerClassLoader) { 868 final JsonValue normalizedConfiguration = 869 normalizeConnectionFactory(configuration, name, 0); 870 return configureConnectionFactory(normalizedConfiguration, providerClassLoader); 871 } 872 873 /** 874 * Creates a new connection factory using the named configuration in the 875 * provided JSON list of factory configurations. See the sample 876 * configuration file for a detailed description of its content. 877 * 878 * @param configuration 879 * The JSON configuration. 880 * @param name 881 * The name of the connection factory configuration to be parsed. 882 * @return A new connection factory using the provided JSON configuration. 883 * @throws IllegalArgumentException 884 * If the configuration is invalid. 885 */ 886 public static ConnectionFactory configureConnectionFactory(final JsonValue configuration, 887 final String name) { 888 return configureConnectionFactory(configuration, name, null); 889 } 890 891 /** 892 * Returns an attribute mapper which maps a single JSON attribute to a JSON 893 * constant. 894 * 895 * @param value 896 * The constant JSON value (a Boolean, Number, String, Map, or 897 * List). 898 * @return The attribute mapper. 899 */ 900 public static AttributeMapper constant(final Object value) { 901 return new JSONConstantAttributeMapper(value); 902 } 903 904 /** 905 * Returns an attribute mapper which maps JSON objects to LDAP attributes. 906 * 907 * @return The attribute mapper. 908 */ 909 public static ObjectAttributeMapper object() { 910 return new ObjectAttributeMapper(); 911 } 912 913 /** 914 * Returns an attribute mapper which provides a mapping from a JSON value to 915 * a single DN valued LDAP attribute. 916 * 917 * @param attribute 918 * The DN valued LDAP attribute to be mapped. 919 * @param baseDN 920 * The search base DN for performing reverse lookups. 921 * @param primaryKey 922 * The search primary key LDAP attribute to use for performing 923 * reverse lookups. 924 * @param mapper 925 * An attribute mapper which will be used to map LDAP attributes 926 * in the referenced entry. 927 * @return The attribute mapper. 928 */ 929 public static ReferenceAttributeMapper reference(final AttributeDescription attribute, 930 final DN baseDN, final AttributeDescription primaryKey, final AttributeMapper mapper) { 931 return new ReferenceAttributeMapper(attribute, baseDN, primaryKey, mapper); 932 } 933 934 /** 935 * Returns an attribute mapper which provides a mapping from a JSON value to 936 * a single DN valued LDAP attribute. 937 * 938 * @param attribute 939 * The DN valued LDAP attribute to be mapped. 940 * @param baseDN 941 * The search base DN for performing reverse lookups. 942 * @param primaryKey 943 * The search primary key LDAP attribute to use for performing 944 * reverse lookups. 945 * @param mapper 946 * An attribute mapper which will be used to map LDAP attributes 947 * in the referenced entry. 948 * @return The attribute mapper. 949 */ 950 public static ReferenceAttributeMapper reference(final String attribute, final String baseDN, 951 final String primaryKey, final AttributeMapper mapper) { 952 return reference(AttributeDescription.valueOf(attribute), DN.valueOf(baseDN), 953 AttributeDescription.valueOf(primaryKey), mapper); 954 } 955 956 /** 957 * Returns an attribute mapper which provides a simple mapping from a JSON 958 * value to a single LDAP attribute. 959 * 960 * @param attribute 961 * The LDAP attribute to be mapped. 962 * @return The attribute mapper. 963 */ 964 public static SimpleAttributeMapper simple(final AttributeDescription attribute) { 965 return new SimpleAttributeMapper(attribute); 966 } 967 968 /** 969 * Returns an attribute mapper which provides a simple mapping from a JSON 970 * value to a single LDAP attribute. 971 * 972 * @param attribute 973 * The LDAP attribute to be mapped. 974 * @return The attribute mapper. 975 */ 976 public static SimpleAttributeMapper simple(final String attribute) { 977 return simple(AttributeDescription.valueOf(attribute)); 978 } 979 980 private static ConnectionFactory configureConnectionFactory(final JsonValue configuration, 981 final ClassLoader providerClassLoader) { 982 final long heartBeatIntervalSeconds = configuration.get("heartBeatIntervalSeconds").defaultTo(30L).asLong(); 983 final Duration heartBeatInterval = new Duration(Math.max(heartBeatIntervalSeconds, 1L), TimeUnit.SECONDS); 984 985 final long heartBeatTimeoutMillis = configuration.get("heartBeatTimeoutMilliSeconds").defaultTo(500L).asLong(); 986 final Duration heartBeatTimeout = new Duration(Math.max(heartBeatTimeoutMillis, 100L), TimeUnit.MILLISECONDS); 987 988 final Options options = Options.defaultOptions() 989 .set(TRANSPORT_PROVIDER_CLASS_LOADER, providerClassLoader) 990 .set(HEARTBEAT_ENABLED, true) 991 .set(HEARTBEAT_INTERVAL, heartBeatInterval) 992 .set(HEARTBEAT_TIMEOUT, heartBeatTimeout) 993 .set(LOAD_BALANCER_MONITORING_INTERVAL, heartBeatInterval); 994 995 // Parse pool parameters, 996 final int connectionPoolSize = 997 Math.max(configuration.get("connectionPoolSize").defaultTo(10).asInteger(), 1); 998 999 // Parse authentication parameters. 1000 if (configuration.isDefined("authentication")) { 1001 final JsonValue authn = configuration.get("authentication"); 1002 if (authn.isDefined("simple")) { 1003 final JsonValue simple = authn.get("simple"); 1004 final BindRequest bindRequest = 1005 Requests.newSimpleBindRequest(simple.get("bindDN").required().asString(), 1006 simple.get("bindPassword").required().asString().toCharArray()); 1007 options.set(AUTHN_BIND_REQUEST, bindRequest); 1008 } else { 1009 throw new IllegalArgumentException("Only simple authentication is supported"); 1010 } 1011 } 1012 1013 // Parse SSL/StartTLS parameters. 1014 final ConnectionSecurity connectionSecurity = 1015 configuration.get("connectionSecurity").defaultTo(ConnectionSecurity.NONE).asEnum( 1016 ConnectionSecurity.class); 1017 if (connectionSecurity != ConnectionSecurity.NONE) { 1018 try { 1019 // Configure SSL. 1020 final SSLContextBuilder builder = new SSLContextBuilder(); 1021 1022 // Parse trust store configuration. 1023 final TrustManagerType trustManagerType = 1024 configuration.get("trustManager").defaultTo(TrustManagerType.TRUSTALL) 1025 .asEnum(TrustManagerType.class); 1026 switch (trustManagerType) { 1027 case TRUSTALL: 1028 builder.setTrustManager(TrustManagers.trustAll()); 1029 break; 1030 case JVM: 1031 // Do nothing: JVM trust manager is the default. 1032 break; 1033 case FILE: 1034 final String fileName = 1035 configuration.get("fileBasedTrustManagerFile").required().asString(); 1036 final String password = 1037 configuration.get("fileBasedTrustManagerPassword").asString(); 1038 final String type = configuration.get("fileBasedTrustManagerType").asString(); 1039 builder.setTrustManager(TrustManagers.checkUsingTrustStore(fileName, 1040 password != null ? password.toCharArray() : null, type)); 1041 break; 1042 } 1043 options.set(SSL_CONTEXT, builder.getSSLContext()); 1044 options.set(SSL_USE_STARTTLS, 1045 connectionSecurity == ConnectionSecurity.STARTTLS); 1046 } catch (GeneralSecurityException | IOException e) { 1047 // Rethrow as unchecked exception. 1048 throw new IllegalArgumentException(e); 1049 } 1050 } 1051 1052 // Parse primary data center. 1053 final JsonValue primaryLDAPServers = configuration.get("primaryLDAPServers"); 1054 if (!primaryLDAPServers.isList() || primaryLDAPServers.size() == 0) { 1055 throw new IllegalArgumentException("No primaryLDAPServers"); 1056 } 1057 final ConnectionFactory primary = parseLDAPServers(primaryLDAPServers, connectionPoolSize, options); 1058 1059 // Parse secondary data center(s). 1060 final JsonValue secondaryLDAPServers = configuration.get("secondaryLDAPServers"); 1061 ConnectionFactory secondary = null; 1062 if (secondaryLDAPServers.isList()) { 1063 if (secondaryLDAPServers.size() > 0) { 1064 secondary = parseLDAPServers(secondaryLDAPServers, connectionPoolSize, options); 1065 } 1066 } else if (!secondaryLDAPServers.isNull()) { 1067 throw new IllegalArgumentException("Invalid secondaryLDAPServers configuration"); 1068 } 1069 1070 // Create fail-over. 1071 if (secondary != null) { 1072 return newFailoverLoadBalancer(asList(primary, secondary), options); 1073 } else { 1074 return primary; 1075 } 1076 } 1077 1078 private static JsonValue normalizeConnectionFactory(final JsonValue configuration, 1079 final String name, final int depth) { 1080 // Protect against infinite recursion in the configuration. 1081 if (depth > 100) { 1082 throw new IllegalArgumentException( 1083 "The LDAP server configuration '" 1084 + name 1085 + "' could not be parsed because of potential circular inheritance dependencies"); 1086 } 1087 1088 final JsonValue current = configuration.get(name).required(); 1089 if (current.isDefined("inheritFrom")) { 1090 // Inherit missing fields from inherited configuration. 1091 final JsonValue parent = 1092 normalizeConnectionFactory(configuration, 1093 current.get("inheritFrom").asString(), depth + 1); 1094 final Map<String, Object> normalized = new LinkedHashMap<>(parent.asMap()); 1095 normalized.putAll(current.asMap()); 1096 normalized.remove("inheritFrom"); 1097 return new JsonValue(normalized); 1098 } else { 1099 // No normalization required. 1100 return current; 1101 } 1102 } 1103 1104 private static ConnectionFactory parseLDAPServers(JsonValue config, int poolSize, Options options) { 1105 final List<ConnectionFactory> servers = new ArrayList<>(config.size()); 1106 for (final JsonValue server : config) { 1107 final String host = server.get("hostname").required().asString(); 1108 final int port = server.get("port").required().asInteger(); 1109 final ConnectionFactory factory = new LDAPConnectionFactory(host, port, options); 1110 if (poolSize > 1) { 1111 servers.add(newCachedConnectionPool(factory, 0, poolSize, 60L, TimeUnit.SECONDS)); 1112 } else { 1113 servers.add(factory); 1114 } 1115 } 1116 if (servers.size() > 1) { 1117 return newRoundRobinLoadBalancer(servers, options); 1118 } else { 1119 return servers.get(0); 1120 } 1121 } 1122 1123 private Rest2LDAP() { 1124 // Prevent instantiation. 1125 } 1126}