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}