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