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}