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}