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 */
016package org.forgerock.opendj.rest2ldap;
017
018import static org.forgerock.http.routing.RoutingMode.EQUALS;
019import static org.forgerock.http.routing.RoutingMode.STARTS_WITH;
020import static org.forgerock.json.resource.RouteMatchers.requestUriMatcher;
021import static org.forgerock.opendj.ldap.Filter.objectClassPresent;
022import static org.forgerock.opendj.ldap.SearchScope.BASE_OBJECT;
023import static org.forgerock.opendj.ldap.SearchScope.SINGLE_LEVEL;
024import static org.forgerock.opendj.ldap.requests.Requests.newSearchRequest;
025import static org.forgerock.opendj.rest2ldap.Rest2Ldap.asResourceException;
026import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.*;
027import static org.forgerock.opendj.rest2ldap.RoutingContext.newRoutingContext;
028import static org.forgerock.opendj.rest2ldap.Utils.newBadRequestException;
029import static org.forgerock.util.promise.Promises.newResultPromise;
030
031import org.forgerock.api.models.ApiDescription;
032import org.forgerock.http.ApiProducer;
033import org.forgerock.http.routing.UriRouterContext;
034import org.forgerock.i18n.LocalizedIllegalArgumentException;
035import org.forgerock.json.resource.ActionRequest;
036import org.forgerock.json.resource.ActionResponse;
037import org.forgerock.json.resource.BadRequestException;
038import org.forgerock.json.resource.CreateRequest;
039import org.forgerock.json.resource.DeleteRequest;
040import org.forgerock.json.resource.NotSupportedException;
041import org.forgerock.json.resource.PatchRequest;
042import org.forgerock.json.resource.QueryRequest;
043import org.forgerock.json.resource.QueryResourceHandler;
044import org.forgerock.json.resource.QueryResponse;
045import org.forgerock.json.resource.ReadRequest;
046import org.forgerock.json.resource.Request;
047import org.forgerock.json.resource.ResourceException;
048import org.forgerock.json.resource.ResourceResponse;
049import org.forgerock.json.resource.Router;
050import org.forgerock.json.resource.UpdateRequest;
051import org.forgerock.opendj.ldap.Attribute;
052import org.forgerock.opendj.ldap.AttributeDescription;
053import org.forgerock.opendj.ldap.ByteString;
054import org.forgerock.opendj.ldap.Connection;
055import org.forgerock.opendj.ldap.DN;
056import org.forgerock.opendj.ldap.Entry;
057import org.forgerock.opendj.ldap.Filter;
058import org.forgerock.opendj.ldap.LdapException;
059import org.forgerock.opendj.ldap.LinkedAttribute;
060import org.forgerock.opendj.ldap.RDN;
061import org.forgerock.opendj.ldap.requests.SearchRequest;
062import org.forgerock.opendj.ldap.responses.SearchResultEntry;
063import org.forgerock.services.context.Context;
064import org.forgerock.util.AsyncFunction;
065import org.forgerock.util.Function;
066import org.forgerock.util.promise.Promise;
067
068/**
069 * Defines a one-to-many relationship between a parent resource and its children. Removal of the parent resource
070 * implies that the children (the sub-resources) are also removed. Collections support all request types.
071 */
072public final class SubResourceCollection extends SubResource {
073    /** The LDAP object classes associated with the glue entries forming the DN template. */
074    private final Attribute glueObjectClasses = new LinkedAttribute("objectClass");
075
076    private NamingStrategy namingStrategy;
077
078    SubResourceCollection(final String resourceId) {
079        super(resourceId);
080        useClientDnNaming("uid");
081    }
082
083    /**
084     * Indicates that the JSON resource ID must be provided by the user, and will be used for naming the associated LDAP
085     * entry. More specifically, LDAP entry names will be derived by appending a single RDN to the collection's base DN
086     * composed of the specified attribute type and LDAP value taken from the LDAP entry once attribute mapping has been
087     * performed.
088     * <p>
089     * Note that this naming policy requires that the user provides the resource name when creating new resources, which
090     * means it must be included in the resource content when not specified explicitly in the create request.
091     *
092     * @param dnAttribute
093     *         The LDAP attribute which will be used for naming.
094     * @return A reference to this object.
095     */
096    public SubResourceCollection useClientDnNaming(final String dnAttribute) {
097        this.namingStrategy = new DnNamingStrategy(dnAttribute);
098        return this;
099    }
100
101    /**
102     * Indicates that the JSON resource ID must be provided by the user, but will not be used for naming the
103     * associated LDAP entry. Instead the JSON resource ID will be taken from the {@code idAttribute} in the LDAP
104     * entry, and the LDAP entry name will be derived by appending a single RDN to the collection's base DN composed
105     * of the {@code dnAttribute} taken from the LDAP entry once attribute mapping has been performed.
106     * <p>
107     * Note that this naming policy requires that the user provides the resource name when creating new resources, which
108     * means it must be included in the resource content when not specified explicitly in the create request.
109     *
110     * @param dnAttribute
111     *         The attribute which will be used for naming LDAP entries.
112     * @param idAttribute
113     *         The attribute which will be used for JSON resource IDs.
114     * @return A reference to this object.
115     */
116    public SubResourceCollection useClientNaming(final String dnAttribute, final String idAttribute) {
117        this.namingStrategy = new AttributeNamingStrategy(dnAttribute, idAttribute, false);
118        return this;
119    }
120
121    /**
122     * Indicates that the JSON resource ID will be derived from the server provided "entryUUID" LDAP attribute. The
123     * LDAP entry name will be derived by appending a single RDN to the collection's base DN composed of the {@code
124     * dnAttribute} taken from the LDAP entry once attribute mapping has been performed.
125     * <p>
126     * Note that this naming policy requires that the server provides the resource name when creating new resources,
127     * which means it must not be specified in the create request, nor included in the resource content.
128     *
129     * @param dnAttribute
130     *         The attribute which will be used for naming LDAP entries.
131     * @return A reference to this object.
132     */
133    public SubResourceCollection useServerEntryUuidNaming(final String dnAttribute) {
134        return useServerNaming(dnAttribute, "entryUUID");
135    }
136
137    /**
138     * Indicates that the JSON resource ID must not be provided by the user, and will not be used for naming the
139     * associated LDAP entry. Instead the JSON resource ID will be taken from the {@code idAttribute} in the LDAP
140     * entry, and the LDAP entry name will be derived by appending a single RDN to the collection's base DN composed
141     * of the {@code dnAttribute} taken from the LDAP entry once attribute mapping has been performed.
142     * <p>
143     * Note that this naming policy requires that the server provides the resource name when creating new resources,
144     * which means it must not be specified in the create request, nor included in the resource content.
145     *
146     * @param dnAttribute
147     *         The attribute which will be used for naming LDAP entries.
148     * @param idAttribute
149     *         The attribute which will be used for JSON resource IDs.
150     * @return A reference to this object.
151     */
152    public SubResourceCollection useServerNaming(final String dnAttribute, final String idAttribute) {
153        this.namingStrategy = new AttributeNamingStrategy(dnAttribute, idAttribute, true);
154        return this;
155    }
156
157    /**
158     * Sets the relative URL template beneath which the sub-resources will be located. The template may be empty
159     * indicating that the sub-resources will be located directly beneath the parent resource. Any URL template
160     * variables will be substituted into the {@link #dnTemplate(String) DN template}.
161     *
162     * @param urlTemplate
163     *         The relative URL template.
164     * @return A reference to this object.
165     */
166    public SubResourceCollection urlTemplate(final String urlTemplate) {
167        this.urlTemplate = urlTemplate;
168        return this;
169    }
170
171    /**
172     * Sets the relative DN template beneath which the sub-resource LDAP entries will be located. The template may be
173     * empty indicating that the LDAP entries will be located directly beneath the parent LDAP entry. Any DN template
174     * variables will be substituted using values extracted from the {@link #urlTemplate(String) URL template}.
175     *
176     * @param dnTemplate
177     *         The relative DN template.
178     * @return A reference to this object.
179     */
180    public SubResourceCollection dnTemplate(final String dnTemplate) {
181        this.dnTemplateString = dnTemplate;
182        return this;
183    }
184
185    /**
186     * Specifies an LDAP object class which is to be associated with any intermediate "glue" entries forming the DN
187     * template. Multiple object classes may be specified.
188     *
189     * @param objectClass
190     *         An LDAP object class which is to be associated with any intermediate "glue" entries forming the DN
191     *         template.
192     * @return A reference to this object.
193     */
194    public SubResourceCollection glueObjectClass(final String objectClass) {
195        this.glueObjectClasses.add(objectClass);
196        return this;
197    }
198
199    /**
200     * Specifies one or more LDAP object classes which is to be associated with any intermediate "glue" entries
201     * forming the DN template. Multiple object classes may be specified.
202     *
203     * @param objectClasses
204     *         The LDAP object classes which is to be associated with any intermediate "glue" entries forming the DN
205     *         template.
206     * @return A reference to this object.
207     */
208    public SubResourceCollection glueObjectClasses(final String... objectClasses) {
209        this.glueObjectClasses.add((Object[]) objectClasses);
210        return this;
211    }
212
213    /**
214     * Indicates whether this sub-resource collection only supports read and query operations.
215     *
216     * @param readOnly
217     *         {@code true} if this sub-resource collection is read-only.
218     * @return A reference to this object.
219     */
220    public SubResourceCollection isReadOnly(final boolean readOnly) {
221        isReadOnly = readOnly;
222        return this;
223    }
224
225    @Override
226    Router addRoutes(final Router router) {
227        router.addRoute(requestUriMatcher(EQUALS, urlTemplate), readOnly(new CollectionHandler()));
228        router.addRoute(requestUriMatcher(EQUALS, urlTemplate + "/{id}"), readOnly(new InstanceHandler()));
229        router.addRoute(requestUriMatcher(STARTS_WITH, urlTemplate + "/{id}"), readOnly(new SubResourceHandler()));
230        return router;
231    }
232
233    Promise<RoutingContext, ResourceException> route(final Context context) {
234        final Connection conn = context.asContext(AuthenticatedConnectionContext.class).getConnection();
235        final SearchRequest searchRequest = namingStrategy.createSearchRequest(dnFrom(context), idFrom(context));
236        if (searchRequest.getScope().equals(BASE_OBJECT) && !resource.hasSubTypesWithSubResources()) {
237            // There's no point in doing a search because we already know the DN and sub-resources.
238            return newResultPromise(newRoutingContext(context, searchRequest.getName(), resource));
239        }
240        searchRequest.addAttribute("objectClass");
241        return conn.searchSingleEntryAsync(searchRequest)
242                         .thenAsync(new AsyncFunction<SearchResultEntry, RoutingContext, ResourceException>() {
243                             @Override
244                             public Promise<RoutingContext, ResourceException> apply(SearchResultEntry entry)
245                                     throws ResourceException {
246                                 final Resource subType = resource.resolveSubTypeFromObjectClasses(entry);
247                                 return newResultPromise(newRoutingContext(context, entry.getName(), subType));
248                             }
249                         }, new AsyncFunction<LdapException, RoutingContext, ResourceException>() {
250                             @Override
251                             public Promise<RoutingContext, ResourceException> apply(LdapException e)
252                                     throws ResourceException {
253                                 return asResourceException(e).asPromise();
254                             }
255                         });
256    }
257
258    private SubResourceImpl collection(final Context context) {
259        return new SubResourceImpl(rest2Ldap,
260                                   dnFrom(context),
261                                   dnTemplateString.isEmpty() ? null : glueObjectClasses,
262                                   namingStrategy,
263                                   resource);
264    }
265
266    private String idFrom(final Context context) {
267        return context.asContext(UriRouterContext.class).getUriTemplateVariables().get("id");
268    }
269
270    private static final class AttributeNamingStrategy implements NamingStrategy {
271        private final AttributeDescription dnAttribute;
272        private final AttributeDescription idAttribute;
273        private final boolean isServerProvided;
274
275        private AttributeNamingStrategy(final String dnAttribute, final String idAttribute,
276                                        final boolean isServerProvided) {
277            this.dnAttribute = AttributeDescription.valueOf(dnAttribute);
278            this.idAttribute = AttributeDescription.valueOf(idAttribute);
279            if (this.dnAttribute.equals(this.idAttribute)) {
280                throw new LocalizedIllegalArgumentException(ERR_CONFIG_NAMING_STRATEGY_DN_AND_ID_NOT_DIFFERENT.get());
281            }
282            this.isServerProvided = isServerProvided;
283        }
284
285        @Override
286        public SearchRequest createSearchRequest(final DN baseDn, final String resourceId) {
287            return newSearchRequest(baseDn, SINGLE_LEVEL, Filter.equality(idAttribute.toString(), resourceId));
288        }
289
290        @Override
291        public String getResourceIdLdapAttribute() {
292            return idAttribute.toString();
293        }
294
295        @Override
296        public String decodeResourceId(final Entry entry) {
297            return entry.parseAttribute(idAttribute).asString();
298        }
299
300        @Override
301        public void encodeResourceId(final DN baseDn, final String resourceId, final Entry entry)
302                throws ResourceException {
303            if (isServerProvided) {
304                if (resourceId != null) {
305                    throw newBadRequestException(ERR_SERVER_PROVIDED_RESOURCE_ID_UNEXPECTED.get());
306                }
307            } else {
308                entry.addAttribute(new LinkedAttribute(idAttribute, ByteString.valueOfUtf8(resourceId)));
309            }
310            final String rdnValue = entry.parseAttribute(dnAttribute).asString();
311            final RDN rdn = new RDN(dnAttribute.getAttributeType(), rdnValue);
312            entry.setName(baseDn.child(rdn));
313        }
314    }
315
316    private static final class DnNamingStrategy implements NamingStrategy {
317        private final AttributeDescription attribute;
318
319        private DnNamingStrategy(final String attribute) {
320            this.attribute = AttributeDescription.valueOf(attribute);
321        }
322
323        @Override
324        public SearchRequest createSearchRequest(final DN baseDn, final String resourceId) {
325            return newSearchRequest(baseDn.child(rdn(resourceId)), BASE_OBJECT, objectClassPresent());
326        }
327
328        @Override
329        public String getResourceIdLdapAttribute() {
330            return attribute.toString();
331        }
332
333        @Override
334        public String decodeResourceId(final Entry entry) {
335            return entry.parseAttribute(attribute).asString();
336        }
337
338        @Override
339        public void encodeResourceId(final DN baseDn, final String resourceId, final Entry entry)
340                throws ResourceException {
341            if (resourceId != null) {
342                entry.setName(baseDn.child(rdn(resourceId)));
343                entry.addAttribute(new LinkedAttribute(attribute, ByteString.valueOfUtf8(resourceId)));
344            } else if (entry.getAttribute(attribute) != null) {
345                entry.setName(baseDn.child(rdn(entry.parseAttribute(attribute).asString())));
346            } else {
347                throw newBadRequestException(ERR_CLIENT_PROVIDED_RESOURCE_ID_MISSING.get());
348            }
349        }
350
351        private RDN rdn(final String resourceId) {
352            return new RDN(attribute.getAttributeType(), resourceId);
353        }
354    }
355
356    /**
357     * Responsible for routing collection requests (CQ) to this collection. More specifically, given the
358     * URL template /collection/{id} then this handler processes requests against /collection.
359     */
360    private final class CollectionHandler extends AbstractRequestHandler {
361        @Override
362        public Promise<ActionResponse, ResourceException> handleAction(final Context context,
363                                                                       final ActionRequest request) {
364            return new NotSupportedException(ERR_COLLECTION_ACTIONS_NOT_SUPPORTED.get().toString()).asPromise();
365        }
366
367        @Override
368        public Promise<ResourceResponse, ResourceException> handleCreate(final Context context,
369                                                                         final CreateRequest request) {
370            return collection(context).create(context, request);
371        }
372
373        @Override
374        public Promise<QueryResponse, ResourceException> handleQuery(final Context context, final QueryRequest request,
375                                                                     final QueryResourceHandler handler) {
376            return collection(context).query(context, request, handler);
377        }
378
379        @Override
380        protected <V> Promise<V, ResourceException> handleRequest(final Context context, final Request request) {
381            return new BadRequestException(ERR_UNSUPPORTED_REQUEST_AGAINST_COLLECTION.get().toString()).asPromise();
382        }
383
384        @Override
385        public ApiDescription api(ApiProducer<ApiDescription> producer) {
386            return resource.collectionApi(isReadOnly);
387        }
388    }
389
390    /**
391     * Responsible for processing instance requests (RUDPA) against this collection and collection requests (CQ) to
392     * any collections sharing the same base URL as an instance within this collection. More specifically, given the
393     * URL template /collection/{parent}/{child} then this handler processes requests against {parent} since it is
394     * both an instance within /collection and also a collection of {child}.
395     */
396    private final class InstanceHandler extends AbstractRequestHandler {
397        @Override
398        public Promise<ActionResponse, ResourceException> handleAction(final Context context,
399                                                                       final ActionRequest request) {
400            return collection(context).action(context, idFrom(context), request);
401        }
402
403        @Override
404        public Promise<ResourceResponse, ResourceException> handleCreate(final Context context,
405                                                                         final CreateRequest request) {
406            return route(context)
407                    .thenAsync(new AsyncFunction<RoutingContext, ResourceResponse, ResourceException>() {
408                        @Override
409                        public Promise<ResourceResponse, ResourceException> apply(final RoutingContext context) {
410                            return subResourceRouterFrom(context).handleCreate(context, request);
411                        }
412                    }).thenCatch(this.<ResourceResponse>convert404To400());
413        }
414
415        @Override
416        public Promise<ResourceResponse, ResourceException> handleDelete(final Context context,
417                                                                         final DeleteRequest request) {
418            return collection(context).delete(context, idFrom(context), request);
419        }
420
421        @Override
422        public Promise<ResourceResponse, ResourceException> handlePatch(final Context context,
423                                                                        final PatchRequest request) {
424            return collection(context).patch(context, idFrom(context), request);
425        }
426
427        @Override
428        public Promise<QueryResponse, ResourceException> handleQuery(final Context context, final QueryRequest request,
429                                                                     final QueryResourceHandler handler) {
430            return route(context)
431                    .thenAsync(new AsyncFunction<RoutingContext, QueryResponse, ResourceException>() {
432                        @Override
433                        public Promise<QueryResponse, ResourceException> apply(final RoutingContext context) {
434                            return subResourceRouterFrom(context).handleQuery(context, request, handler);
435                        }
436                    }).thenCatch(this.<QueryResponse>convert404To400());
437        }
438
439        @Override
440        public Promise<ResourceResponse, ResourceException> handleRead(final Context context,
441                                                                       final ReadRequest request) {
442            return collection(context).read(context, idFrom(context), request);
443        }
444
445        @Override
446        public Promise<ResourceResponse, ResourceException> handleUpdate(final Context context,
447                                                                         final UpdateRequest request) {
448            return collection(context).update(context, idFrom(context), request);
449        }
450
451        private <T> Function<ResourceException, T, ResourceException> convert404To400() {
452            return SubResource.convert404To400(ERR_UNSUPPORTED_REQUEST_AGAINST_INSTANCE.get());
453        }
454
455        /**
456         * Returns {@code null} because the corresponding {@link ApiDescription}
457         * is returned by the {@link CollectionHandler#api(ApiProducer)} method.
458         * <p>
459         * This avoids problems when trying to {@link ApiProducer#merge(java.util.List) merge}
460         * {@link ApiDescription}s with the same path.
461         */
462        @Override
463        public ApiDescription api(ApiProducer<ApiDescription> producer) {
464            return null;
465        }
466    }
467}