001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2016 ForgeRock AS.
015 *
016 */
017package org.forgerock.opendj.rest2ldap;
018
019import static org.forgerock.opendj.ldap.ResultCode.ADMIN_LIMIT_EXCEEDED;
020import static org.forgerock.opendj.ldap.ResultCode.ENTRY_ALREADY_EXISTS;
021import static org.forgerock.opendj.ldap.ResultCode.SIZE_LIMIT_EXCEEDED;
022import static org.forgerock.opendj.rest2ldap.ReadOnUpdatePolicy.CONTROLS;
023
024import java.util.Arrays;
025import java.util.Collection;
026import java.util.LinkedHashMap;
027import java.util.Map;
028
029import org.forgerock.json.resource.BadRequestException;
030import org.forgerock.json.resource.ForbiddenException;
031import org.forgerock.json.resource.InternalServerErrorException;
032import org.forgerock.json.resource.NotFoundException;
033import org.forgerock.json.resource.PermanentException;
034import org.forgerock.json.resource.PreconditionFailedException;
035import org.forgerock.json.resource.RequestHandler;
036import org.forgerock.json.resource.ResourceException;
037import org.forgerock.json.resource.RetryableException;
038import org.forgerock.json.resource.Router;
039import org.forgerock.json.resource.ServiceUnavailableException;
040import org.forgerock.opendj.ldap.AssertionFailureException;
041import org.forgerock.opendj.ldap.AttributeDescription;
042import org.forgerock.opendj.ldap.AuthenticationException;
043import org.forgerock.opendj.ldap.AuthorizationException;
044import org.forgerock.opendj.ldap.ConnectionException;
045import org.forgerock.opendj.ldap.ConstraintViolationException;
046import org.forgerock.opendj.ldap.DN;
047import org.forgerock.opendj.ldap.DecodeOptions;
048import org.forgerock.opendj.ldap.EntryNotFoundException;
049import org.forgerock.opendj.ldap.LdapException;
050import org.forgerock.opendj.ldap.MultipleEntriesFoundException;
051import org.forgerock.opendj.ldap.ResultCode;
052import org.forgerock.opendj.ldap.TimeoutResultException;
053import org.forgerock.opendj.ldap.schema.Schema;
054import org.forgerock.util.Option;
055import org.forgerock.util.Options;
056import org.forgerock.util.Reject;
057
058/**
059 * Provides methods for constructing Rest2Ldap protocol gateways. Applications construct a new Rest2Ldap
060 * instance by calling {@link #rest2Ldap} passing in a list of {@link Resource resources} which together define
061 * the data model being exposed by the gateway. Call {@link #newRequestHandlerFor(String)} in order to obtain
062 * a request handler for a specific resource. The methods in this class can be categorized as follows:
063 * <p/>
064 * Creating Rest2Ldap gateways:
065 * <ul>
066 * <li>{@link #rest2Ldap} - creates a gateway for a given set of resources</li>
067 * <li>{@link #newRequestHandlerFor} - obtains a request handler for the specified endpoint resource.</li>
068 * </ul>
069 * <p/>
070 * Defining resource types, e.g. users, groups, devices, etc:
071 * <ul>
072 * <li>{@link #resource} - creates a resource having a fluent API for defining additional characteristics
073 * such as the resource's inheritance, sub-resources, and properties</li>
074 * </ul>
075 * <p/>
076 * Defining a resource's sub-resources. A sub-resource is a resource which is subordinate to another resource. Or, to
077 * put it another way, sub-resources define parent child relationships where the life-cycle of a child resource is
078 * constrained by the life-cycle of the parent: deleting the parent implies that all children are deleted as well. An
079 * example of a sub-resource is a subscriber having one or more devices:
080 * <ul>
081 * <li>{@link #collectionOf} - creates a one-to-many relationship. Collections support creation, deletion,
082 * and querying of child resources</li>
083 * <li>{@link #singletonOf} - creates a one-to-one relationship. Singletons cannot be created or destroyed,
084 * although they may be modified if they have properties which are modifiable. Singletons are usually only used as
085 * top-level entry points into REST APIs.
086 * </li>
087 * </ul>
088 * <p/>
089 * Defining a resource's properties:
090 * <ul>
091 * <li>{@link #resourceType} - defines a property whose JSON value will be the name of the resource, e.g. "user"</li>
092 * <li>{@link #simple} - defines a property which maps a JSON value to a single LDAP attribute</li>
093 * <li>{@link #object} - defines a property which is a JSON object having zero or more nested properties</li>
094 * <li>{@link #reference} - defines a property whose JSON value is a reference to another resource. Use these for
095 * mapping LDAP attributes which contain the DN of another LDAP entry exposed by Rest2Ldap. For example, a user's
096 * "manager" attribute or the members of a group.</li>
097 * </ul>
098 */
099public final class Rest2Ldap {
100    /**
101     * Specifies the LDAP decoding options which should be used when decoding LDAP DNs, attribute types, and controls.
102     * By default Rest2Ldap will use a set of options of will always use the default schema.
103     */
104    public static final Option<DecodeOptions> DECODE_OPTIONS = Option.withDefault(new DecodeOptions());
105    /**
106     * Specifies whether Rest2Ldap should support multi-version concurrency control (MVCC) through the use of an MVCC
107     * LDAP {@link #MVCC_ATTRIBUTE attribute} such as "etag". By default Rest2Ldap will use MVCC.
108     */
109    public static final Option<Boolean> USE_MVCC = Option.withDefault(true);
110    /**
111     * Specifies the name of the LDAP attribute which should be used for multi-version concurrency control (MVCC) if
112     * {@link #USE_MVCC enabled}. By default Rest2Ldap will use the "etag" operational attribute.
113     */
114    public static final Option<String> MVCC_ATTRIBUTE = Option.withDefault("etag");
115    /**
116     * Specifies the policy which should be used in order to read an entry before it is deleted, or after it is added or
117     * modified. By default Rest2Ldap will use the {@link ReadOnUpdatePolicy#CONTROLS controls} read on update policy.
118     */
119    public static final Option<ReadOnUpdatePolicy> READ_ON_UPDATE_POLICY = Option.withDefault(CONTROLS);
120    /**
121     * Specifies whether Rest2Ldap should perform LDAP modify operations using the LDAP permissive modify
122     * control. By default Rest2Ldap will use the permissive modify control and use of the control is strongly
123     * recommended.
124     */
125    public static final Option<Boolean> USE_PERMISSIVE_MODIFY = Option.withDefault(true);
126    /**
127     * Specifies whether Rest2Ldap should perform LDAP delete operations using the LDAP subtree delete control. By
128     * default Rest2Ldap will use the subtree delete control and use of the control is strongly recommended.
129     */
130    public static final Option<Boolean> USE_SUBTREE_DELETE = Option.withDefault(true);
131
132    /**
133     * Creates a new {@link Rest2Ldap} instance using the provided options and {@link Resource resources}.
134     * Applications should call {@link #newRequestHandlerFor(String)} to obtain a request handler for a specific
135     * resource.
136     * <p>
137     * The supported options are defined in this class.
138     *
139     * @param options The configuration options for interactions with the backend LDAP server. The set of available
140     *                options are provided in this class.
141     * @param resources The list of resources.
142     * @return A new Rest2Ldap instance from which REST request handlers can be obtained.
143     */
144    public static Rest2Ldap rest2Ldap(final Options options, final Collection<Resource> resources) {
145        return new Rest2Ldap(options, resources);
146    }
147
148    /**
149     * Creates a new {@link Rest2Ldap} instance using the provided options and {@link Resource resources}.
150     * Applications should call {@link #newRequestHandlerFor(String)} to obtain a request handler for a specific
151     * resource.
152     * <p>
153     * The supported options are defined in this class.
154     *
155     * @param options The configuration options for interactions with the backend LDAP server. The set of available
156     *                options are provided in this class.
157     * @param resources The list of resources.
158     * @return A new Rest2Ldap instance from which REST request handlers can be obtained.
159     */
160    public static Rest2Ldap rest2Ldap(final Options options, final Resource... resources) {
161        return rest2Ldap(options, Arrays.asList(resources));
162    }
163
164    /**
165     * Creates a new {@link Resource resource} definition with the provided resource ID.
166     *
167     * @param resourceId
168     *         The resource ID.
169     * @return A new resource definition with the provided resource ID.
170     */
171    public static Resource resource(final String resourceId) {
172        return new Resource(resourceId);
173    }
174
175    /**
176     * Creates a new {@link SubResourceCollection collection} sub-resource definition whose members will be resources
177     * having the provided resource ID or its sub-types.
178     *
179     * @param resourceId
180     *         The type of resource contained in the sub-resource collection.
181     * @return A new sub-resource definition with the provided resource ID.
182     */
183    public static SubResourceCollection collectionOf(final String resourceId) {
184        return new SubResourceCollection(resourceId);
185    }
186
187    /**
188     * Creates a new {@link SubResourceSingleton singleton} sub-resource definition which will reference a single
189     * resource having the specified resource ID.
190     *
191     * @param resourceId
192     *         The type of resource referenced by the sub-resource singleton.
193     * @return A new sub-resource definition with the provided resource ID.
194     */
195    public static SubResourceSingleton singletonOf(final String resourceId) {
196        return new SubResourceSingleton(resourceId);
197    }
198
199    /**
200     * Returns a property mapper which maps a JSON property containing the resource type to its associated LDAP
201     * object classes.
202     *
203     * @return The property mapper.
204     */
205    public static PropertyMapper resourceType() {
206        return ResourceTypePropertyMapper.INSTANCE;
207    }
208
209    /**
210     * Returns a property mapper which maps a single JSON attribute to a JSON constant.
211     *
212     * @param value
213     *         The constant JSON value (a Boolean, Number, String, Map, or List).
214     * @return The property mapper.
215     */
216    public static PropertyMapper constant(final Object value) {
217        return new JsonConstantPropertyMapper(value);
218    }
219
220    /**
221     * Returns a property mapper which maps JSON objects to LDAP attributes.
222     *
223     * @return The property mapper.
224     */
225    public static ObjectPropertyMapper object() {
226        return new ObjectPropertyMapper();
227    }
228
229    /**
230     * Returns a property mapper which provides a mapping from a JSON value to a single DN valued LDAP attribute.
231     *
232     * @param attribute
233     *         The DN valued LDAP attribute to be mapped.
234     * @param baseDN
235     *         The search base DN for performing reverse lookups.
236     * @param primaryKey
237     *         The search primary key LDAP attribute to use for performing reverse lookups.
238     * @param mapper
239     *         An property mapper which will be used to map LDAP attributes in the referenced entry.
240     * @return The property mapper.
241     */
242    public static ReferencePropertyMapper reference(final AttributeDescription attribute, final DN baseDN,
243                                                    final AttributeDescription primaryKey,
244                                                    final PropertyMapper mapper) {
245        return new ReferencePropertyMapper(Schema.getDefaultSchema(), attribute, baseDN, primaryKey, mapper);
246    }
247
248    /**
249     * Returns a property mapper which provides a mapping from a JSON value to a single DN valued LDAP attribute.
250     *
251     * @param attribute
252     *         The DN valued LDAP attribute to be mapped.
253     * @param baseDN
254     *         The search base DN for performing reverse lookups.
255     * @param primaryKey
256     *         The search primary key LDAP attribute to use for performing reverse lookups.
257     * @param mapper
258     *         An property mapper which will be used to map LDAP attributes in the referenced entry.
259     * @return The property mapper.
260     */
261    public static ReferencePropertyMapper reference(final String attribute, final String baseDN,
262                                                    final String primaryKey, final PropertyMapper mapper) {
263        return reference(AttributeDescription.valueOf(attribute),
264                         DN.valueOf(baseDN),
265                         AttributeDescription.valueOf(primaryKey),
266                         mapper);
267    }
268
269    /**
270     * Returns a property mapper which provides a simple mapping from a JSON value to a single LDAP attribute.
271     *
272     * @param attribute
273     *         The LDAP attribute to be mapped.
274     * @return The property mapper.
275     */
276    public static SimplePropertyMapper simple(final AttributeDescription attribute) {
277        return new SimplePropertyMapper(attribute);
278    }
279
280    /**
281     * Returns a property mapper which provides a simple mapping from a JSON value to a single LDAP attribute.
282     *
283     * @param attribute
284     *         The LDAP attribute to be mapped.
285     * @return The property mapper.
286     */
287    public static SimplePropertyMapper simple(final String attribute) {
288        return simple(AttributeDescription.valueOf(attribute));
289    }
290
291    /**
292     * Adapts a {@code Throwable} to a {@code ResourceException}. If the {@code Throwable} is an LDAP
293     * {@link LdapException} then an appropriate {@code ResourceException} is returned, otherwise an {@code
294     * InternalServerErrorException} is returned.
295     * @param t
296     *         The {@code Throwable} to be converted.
297     * @return The equivalent resource exception.
298     */
299    public static ResourceException asResourceException(final Throwable t) {
300        try {
301            throw t;
302        } catch (final ResourceException e) {
303            return e;
304        } catch (final AssertionFailureException e) {
305            return new PreconditionFailedException(e);
306        } catch (final ConstraintViolationException e) {
307            final ResultCode rc = e.getResult().getResultCode();
308            if (rc.equals(ENTRY_ALREADY_EXISTS)) {
309                return new PreconditionFailedException(e); // Consistent with MVCC.
310            } else {
311                return new BadRequestException(e); // Schema violation, etc.
312            }
313        } catch (final AuthenticationException e) {
314            return new PermanentException(401, null, e); // Unauthorized
315        } catch (final AuthorizationException e) {
316            return new ForbiddenException(e);
317        } catch (final ConnectionException e) {
318            return new ServiceUnavailableException(e);
319        } catch (final EntryNotFoundException e) {
320            return new NotFoundException(e);
321        } catch (final MultipleEntriesFoundException e) {
322            return new InternalServerErrorException(e);
323        } catch (final TimeoutResultException e) {
324            return new RetryableException(408, null, e); // Request Timeout
325        } catch (final LdapException e) {
326            final ResultCode rc = e.getResult().getResultCode();
327            if (rc.equals(ADMIN_LIMIT_EXCEEDED) || rc.equals(SIZE_LIMIT_EXCEEDED)) {
328                return new PermanentException(413, null, e); // Payload Too Large (Request Entity Too Large)
329            } else {
330                return new InternalServerErrorException(e);
331            }
332        } catch (final Throwable tmp) {
333            return new InternalServerErrorException(t);
334        }
335    }
336
337    private final Map<String, Resource> resources = new LinkedHashMap<>();
338    private final Options options;
339
340    private Rest2Ldap(final Options options, final Collection<Resource> resources) {
341        this.options = options;
342        for (final Resource resource : resources) {
343            this.resources.put(resource.getResourceId(), resource);
344        }
345        // Now build the model.
346        for (final Resource resource : resources) {
347            resource.build(this);
348        }
349    }
350
351    /**
352     * Returns a {@link RequestHandler} which will handle requests to the named resource and any of its sub-resources.
353     *
354     * @param resourceId
355     *         The resource ID.
356     * @return A {@link RequestHandler} which will handle requests to the named resource.
357     */
358    public RequestHandler newRequestHandlerFor(final String resourceId) {
359        Reject.ifTrue(!resources.containsKey(resourceId), "unrecognized resource '" + resourceId + "'");
360        final SubResourceSingleton root = singletonOf(resourceId);
361        root.build(this, null);
362        return root.addRoutes(new Router());
363    }
364
365    Options getOptions() {
366        return options;
367    }
368
369    Resource getResource(final String resourceId) {
370        return resources.get(resourceId);
371    }
372}