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