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}