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 java.util.Arrays.asList; 019import static org.forgerock.api.enums.CountPolicy.*; 020import static org.forgerock.api.enums.PagingMode.*; 021import static org.forgerock.api.enums.ParameterSource.*; 022import static org.forgerock.api.enums.PatchOperation.*; 023import static org.forgerock.api.enums.Stability.*; 024import static org.forgerock.api.models.VersionedPath.*; 025import static org.forgerock.json.JsonValue.*; 026import static org.forgerock.json.resource.ResourceException.*; 027import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.ERR_ABSTRACT_TYPE_IN_CREATE; 028import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.ERR_MISSING_TYPE_PROPERTY_IN_CREATE; 029import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.ERR_UNRECOGNIZED_RESOURCE_SUPER_TYPE; 030import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.ERR_UNRECOGNIZED_TYPE_IN_CREATE; 031import static org.forgerock.opendj.rest2ldap.Utils.newBadRequestException; 032import static org.forgerock.util.Utils.joinAsString; 033 034import java.util.ArrayList; 035import java.util.Arrays; 036import java.util.Collection; 037import java.util.HashSet; 038import java.util.LinkedHashMap; 039import java.util.LinkedHashSet; 040import java.util.List; 041import java.util.Map; 042import java.util.Set; 043 044import org.forgerock.api.commons.CommonsApi; 045import org.forgerock.api.enums.CreateMode; 046import org.forgerock.api.enums.QueryType; 047import org.forgerock.api.models.ApiDescription; 048import org.forgerock.api.models.ApiError; 049import org.forgerock.api.models.Create; 050import org.forgerock.api.models.Definitions; 051import org.forgerock.api.models.Delete; 052import org.forgerock.api.models.Errors; 053import org.forgerock.api.models.Items; 054import org.forgerock.api.models.Parameter; 055import org.forgerock.api.models.Patch; 056import org.forgerock.api.models.Paths; 057import org.forgerock.api.models.Query; 058import org.forgerock.api.models.Read; 059import org.forgerock.api.models.Reference; 060import org.forgerock.api.models.Schema; 061import org.forgerock.api.models.Services; 062import org.forgerock.api.models.Update; 063import org.forgerock.http.ApiProducer; 064import org.forgerock.i18n.LocalizableMessage; 065import org.forgerock.i18n.LocalizedIllegalArgumentException; 066import org.forgerock.json.JsonPointer; 067import org.forgerock.json.JsonValue; 068import org.forgerock.json.resource.RequestHandler; 069import org.forgerock.json.resource.ResourceException; 070import org.forgerock.json.resource.Router; 071import org.forgerock.opendj.ldap.Attribute; 072import org.forgerock.opendj.ldap.Entry; 073import org.forgerock.opendj.ldap.LinkedAttribute; 074import org.forgerock.util.i18n.LocalizableString; 075 076/** 077 * Defines the characteristics of a resource, including its properties, inheritance, and sub-resources. 078 */ 079public final class Resource { 080 // errors 081 private static final String ERROR_ADMIN_LIMIT_EXCEEDED = "#/errors/adminLimitExceeded"; 082 private static final String ERROR_READ_FOUND_MULTIPLE_ENTRIES = "#/errors/readFoundMultipleEntries"; 083 private static final String ERROR_PASSWORD_MODIFY_REQUIRES_HTTPS = "#/errors/passwordModifyRequiresHttps"; 084 private static final String ERROR_PASSWORD_MODIFY_REQUIRES_AUTHENTICATION = "#/errors/passwordModifyRequiresAuthn"; 085 086 private static final String ERROR_BAD_REQUEST = CommonsApi.Errors.BAD_REQUEST.getReference(); 087 private static final String ERROR_FORBIDDEN = CommonsApi.Errors.FORBIDDEN.getReference(); 088 private static final String ERROR_INTERNAL_SERVER_ERROR = CommonsApi.Errors.INTERNAL_SERVER_ERROR.getReference(); 089 private static final String ERROR_NOT_FOUND = CommonsApi.Errors.NOT_FOUND.getReference(); 090 private static final String ERROR_REQUEST_ENTITY_TOO_LARGE = 091 CommonsApi.Errors.REQUEST_ENTITY_TOO_LARGE.getReference(); 092 private static final String ERROR_REQUEST_TIMEOUT = CommonsApi.Errors.REQUEST_TIMEOUT.getReference(); 093 private static final String ERROR_UNAUTHORIZED = CommonsApi.Errors.UNAUTHORIZED.getReference(); 094 private static final String ERROR_UNAVAILABLE = CommonsApi.Errors.UNAVAILABLE.getReference(); 095 private static final String ERROR_VERSION_MISMATCH = CommonsApi.Errors.VERSION_MISMATCH.getReference(); 096 097 /** All fields are queryable, but the directory server may reject some requests (unindexed?). */ 098 private static final String ALL_FIELDS = "*"; 099 100 101 /** The resource ID. */ 102 private final String id; 103 /** {@code true} if only sub-types of this resource can be created. */ 104 private boolean isAbstract; 105 /** The ID of the super-type of this resource, may be {@code null}. */ 106 private String superTypeId; 107 /** The LDAP object classes associated with this resource. */ 108 private final Attribute objectClasses = new LinkedAttribute("objectClass"); 109 /** The possibly empty set of sub-resources. */ 110 private final Set<SubResource> subResources = new LinkedHashSet<>(); 111 /** The set of property mappers associated with this resource, excluding inherited properties. */ 112 private final Map<String, PropertyMapper> declaredProperties = new LinkedHashMap<>(); 113 /** The set of property mappers associated with this resource, including inherited properties. */ 114 private final Map<String, PropertyMapper> allProperties = new LinkedHashMap<>(); 115 /** 116 * A JSON pointer to the primitive JSON property that will be used to convey type information. May be {@code 117 * null} if the type property is defined in a super type or if this resource does not have any sub-types. 118 */ 119 private JsonPointer resourceTypeProperty; 120 /** Set to {@code true} once this Resource has been built. */ 121 private boolean isBuilt = false; 122 /** The resolved super-type. */ 123 private Resource superType; 124 /** The resolved sub-resources (only immediate children). */ 125 private final Set<Resource> subTypes = new LinkedHashSet<>(); 126 /** The property mapper which will map all properties for this resource including inherited properties. */ 127 private final ObjectPropertyMapper propertyMapper = new ObjectPropertyMapper(); 128 /** Routes requests to sub-resources. */ 129 private final Router subResourceRouter = new Router(); 130 private volatile Boolean hasSubTypesWithSubResources = null; 131 /** The set of actions supported by this resource and its sub-types. */ 132 private final Set<Action> supportedActions = new HashSet<>(); 133 private LocalizableMessage description; 134 135 Resource(final String id) { 136 this.id = id; 137 } 138 139 /** 140 * Sets the description of this resource. 141 * 142 * @param description 143 * the description of this resource 144 */ 145 public void description(LocalizableMessage description) { 146 this.description = description; 147 } 148 149 /** 150 * Returns the resource ID of this resource. 151 * 152 * @return The resource ID of this resource. 153 */ 154 @Override 155 public String toString() { 156 return id; 157 } 158 159 /** 160 * Returns {@code true} if the provided parameter is a {@code Resource} having the same resource ID as this 161 * resource. 162 * 163 * @param o 164 * The object to compare. 165 * @return {@code true} if the provided parameter is a {@code Resource} having the same resource ID as this 166 * resource. 167 */ 168 @Override 169 public boolean equals(final Object o) { 170 return this == o || (o instanceof Resource && id.equals(((Resource) o).id)); 171 } 172 173 @Override 174 public int hashCode() { 175 return id.hashCode(); 176 } 177 178 /** 179 * Specifies the resource ID of the resource which is a super-type of this resource. This resource will inherit 180 * the properties and sub-resources of the super-type, and may optionally override them. 181 * 182 * @param resourceId 183 * The resource ID of the resource which is a super-type of this resource, or {@code null} if there is no 184 * super-type. 185 * @return A reference to this object. 186 */ 187 public Resource superType(final String resourceId) { 188 this.superTypeId = resourceId; 189 return this; 190 } 191 192 /** 193 * Specifies whether this resource is an abstract type and therefore cannot be created. Only non-abstract 194 * sub-types can be created. 195 * 196 * @param isAbstract 197 * {@code true} if this resource is abstract. 198 * @return A reference to this object. 199 */ 200 public Resource isAbstract(final boolean isAbstract) { 201 this.isAbstract = isAbstract; 202 return this; 203 } 204 205 /** 206 * Specifies a mapping for a property contained in this JSON resource. Properties are inherited and sub-types may 207 * override them. Properties are optional: a resource that does not have any properties cannot be created, read, 208 * or modified, and may only be used for accessing sub-resources. These resources usually represent API 209 * "endpoints". 210 * 211 * @param name 212 * The name of the JSON property to be mapped. 213 * @param mapper 214 * The property mapper responsible for mapping the JSON property to LDAP attribute(s). 215 * @return A reference to this object. 216 */ 217 public Resource property(final String name, final PropertyMapper mapper) { 218 declaredProperties.put(name, mapper); 219 return this; 220 } 221 222 /** 223 * Specifies whether all LDAP user attributes should be mapped by default using the default schema based mapping 224 * rules. Individual attributes can be excluded using {@link #excludedDefaultUserAttributes} in order to prevent 225 * attributes with explicit mappings being mapped twice. 226 * 227 * @param include {@code true} if all LDAP user attributes be mapped by default. 228 * @return A reference to this object. 229 */ 230 public Resource includeAllUserAttributesByDefault(final boolean include) { 231 propertyMapper.includeAllUserAttributesByDefault(include); 232 return this; 233 } 234 235 /** 236 * Specifies zero or more user attributes which will be excluded from the default user attribute mappings when 237 * enabled using {@link #includeAllUserAttributesByDefault}. Attributes which have explicit mappings should be 238 * excluded in order to prevent duplication. 239 * 240 * @param attributeNames The list of attributes to be excluded. 241 * @return A reference to this object. 242 */ 243 public Resource excludedDefaultUserAttributes(final String... attributeNames) { 244 return excludedDefaultUserAttributes(Arrays.asList(attributeNames)); 245 } 246 247 /** 248 * Specifies zero or more user attributes which will be excluded from the default user attribute mappings when 249 * enabled using {@link #includeAllUserAttributesByDefault}. Attributes which have explicit mappings should be 250 * excluded in order to prevent duplication. 251 * 252 * @param attributeNames The list of attributes to be excluded. 253 * @return A reference to this object. 254 */ 255 public Resource excludedDefaultUserAttributes(final Collection<String> attributeNames) { 256 propertyMapper.excludedDefaultUserAttributes(attributeNames); 257 return this; 258 } 259 260 /** 261 * Specifies the name of the JSON property which contains the resource's type, whose value is the 262 * resource ID. The resource type property is inherited by sub-types and must be available to any resources 263 * referenced from {@link SubResource sub-resources}. 264 * 265 * @param resourceTypeProperty 266 * The name of the JSON property which contains the resource's type, or {@code null} if this resource does 267 * not have a resource type property or if it should be inherited from a super-type. 268 * @return A reference to this object. 269 */ 270 public Resource resourceTypeProperty(final JsonPointer resourceTypeProperty) { 271 this.resourceTypeProperty = resourceTypeProperty; 272 return this; 273 } 274 275 /** 276 * Specifies an LDAP object class which is to be associated with this resource. Multiple object classes may be 277 * specified. The object classes are used for determining the type of resource being accessed during all requests 278 * other than create. Object classes are inherited by sub-types and must be defined for any resources that are 279 * non-abstract and which can be created. 280 * 281 * @param objectClass 282 * An LDAP object class associated with this resource's LDAP representation. 283 * @return A reference to this object. 284 */ 285 public Resource objectClass(final String objectClass) { 286 this.objectClasses.add(objectClass); 287 return this; 288 } 289 290 /** 291 * Specifies LDAP object classes which are to be associated with this resource. Multiple object classes may be 292 * specified. The object classes are used for determining the type of resource being accessed during all requests 293 * other than create. Object classes are inherited by sub-types and must be defined for any resources that are 294 * non-abstract and which can be created. 295 * 296 * @param objectClasses 297 * The LDAP object classes associated with this resource's LDAP representation. 298 * @return A reference to this object. 299 */ 300 public Resource objectClasses(final String... objectClasses) { 301 this.objectClasses.add((Object[]) objectClasses); 302 return this; 303 } 304 305 /** 306 * Registers an action which should be supported by this resource. By default, no actions are supported. 307 * 308 * @param action 309 * The action supported by this resource. 310 * @return A reference to this object. 311 */ 312 public Resource supportedAction(final Action action) { 313 this.supportedActions.add(action); 314 return this; 315 } 316 317 /** 318 * Registers zero or more actions which should be supported by this resource. By default, no actions are supported. 319 * 320 * @param actions 321 * The actions supported by this resource. 322 * @return A reference to this object. 323 */ 324 public Resource supportedActions(final Action... actions) { 325 this.supportedActions.addAll(Arrays.asList(actions)); 326 return this; 327 } 328 329 /** 330 * Specifies a parent-child relationship with another resource. Sub-resources are inherited by sub-types and may 331 * be overridden. 332 * 333 * @param subResource 334 * The sub-resource definition. 335 * @return A reference to this object. 336 */ 337 public Resource subResource(final SubResource subResource) { 338 this.subResources.add(subResource); 339 return this; 340 } 341 342 /** 343 * Specifies a parent-child relationship with zero or more resources. Sub-resources are inherited by sub-types and 344 * may be overridden. 345 * 346 * @param subResources 347 * The sub-resource definitions. 348 * @return A reference to this object. 349 */ 350 public Resource subResources(final SubResource... subResources) { 351 this.subResources.addAll(asList(subResources)); 352 return this; 353 } 354 355 boolean hasSupportedAction(final Action action) { 356 return supportedActions.contains(action); 357 } 358 359 boolean hasSubTypes() { 360 return !subTypes.isEmpty(); 361 } 362 363 boolean mayHaveSubResources() { 364 return !subResources.isEmpty() || hasSubTypesWithSubResources(); 365 } 366 367 boolean hasSubTypesWithSubResources() { 368 if (hasSubTypesWithSubResources == null) { 369 for (final Resource subType : subTypes) { 370 if (!subType.subResources.isEmpty() || subType.hasSubTypesWithSubResources()) { 371 hasSubTypesWithSubResources = true; 372 return true; 373 } 374 } 375 hasSubTypesWithSubResources = false; 376 } 377 return hasSubTypesWithSubResources; 378 } 379 380 Set<Resource> getSubTypes() { 381 return subTypes; 382 } 383 384 Resource resolveSubTypeFromJson(final JsonValue content) throws ResourceException { 385 if (!hasSubTypes()) { 386 // The resource type is implied because this resource does not have sub-types. In particular, resources 387 // are not required to have type information if they don't have sub-types. 388 return this; 389 } 390 final JsonValue jsonType = content.get(resourceTypeProperty); 391 if (jsonType == null || !jsonType.isString()) { 392 throw newBadRequestException(ERR_MISSING_TYPE_PROPERTY_IN_CREATE.get(resourceTypeProperty)); 393 } 394 final String type = jsonType.asString(); 395 final Resource subType = resolveSubTypeFromString(type); 396 if (subType == null) { 397 throw newBadRequestException(ERR_UNRECOGNIZED_TYPE_IN_CREATE.get(type, getAllowedResourceTypes())); 398 } 399 if (subType.isAbstract) { 400 throw newBadRequestException(ERR_ABSTRACT_TYPE_IN_CREATE.get(type, getAllowedResourceTypes())); 401 } 402 return subType; 403 } 404 405 private String getAllowedResourceTypes() { 406 final List<String> allowedTypes = new ArrayList<>(); 407 getAllowedResourceTypes(allowedTypes); 408 return joinAsString(", ", allowedTypes); 409 } 410 411 private void getAllowedResourceTypes(final List<String> allowedTypes) { 412 if (!isAbstract) { 413 allowedTypes.add(id); 414 } 415 for (final Resource subType : subTypes) { 416 subType.getAllowedResourceTypes(allowedTypes); 417 } 418 } 419 420 Resource resolveSubTypeFromString(final String type) { 421 if (id.equalsIgnoreCase(type)) { 422 return this; 423 } 424 for (final Resource subType : subTypes) { 425 final Resource resolvedSubType = subType.resolveSubTypeFromString(type); 426 if (resolvedSubType != null) { 427 return resolvedSubType; 428 } 429 } 430 return null; 431 } 432 433 Resource resolveSubTypeFromObjectClasses(final Entry entry) { 434 if (!hasSubTypes()) { 435 // This resource does not have sub-types. 436 return this; 437 } 438 final Attribute objectClassesFromEntry = entry.getAttribute("objectClass"); 439 final Resource subType = resolveSubTypeFromObjectClasses(objectClassesFromEntry); 440 if (subType == null) { 441 // Best effort. 442 return this; 443 } 444 return subType; 445 } 446 447 private Resource resolveSubTypeFromObjectClasses(final Attribute objectClassesFromEntry) { 448 if (!objectClassesFromEntry.containsAll(objectClasses)) { 449 return null; 450 } 451 // This resource is a potential match, but sub-types may be better. 452 for (final Resource subType : subTypes) { 453 final Resource resolvedSubType = subType.resolveSubTypeFromObjectClasses(objectClassesFromEntry); 454 if (resolvedSubType != null) { 455 return resolvedSubType; 456 } 457 } 458 return this; 459 } 460 461 Attribute getObjectClassAttribute() { 462 return objectClasses; 463 } 464 465 RequestHandler getSubResourceRouter() { 466 return subResourceRouter; 467 } 468 469 String getResourceId() { 470 return id; 471 } 472 473 void build(final Rest2Ldap rest2Ldap) { 474 // Prevent re-entrant calls. 475 if (isBuilt) { 476 return; 477 } 478 isBuilt = true; 479 480 if (superTypeId != null) { 481 superType = rest2Ldap.getResource(superTypeId); 482 if (superType == null) { 483 throw new LocalizedIllegalArgumentException(ERR_UNRECOGNIZED_RESOURCE_SUPER_TYPE.get(id, superTypeId)); 484 } 485 // Inherit content from super-type. 486 superType.build(rest2Ldap); 487 superType.subTypes.add(this); 488 if (resourceTypeProperty == null) { 489 resourceTypeProperty = superType.resourceTypeProperty; 490 } 491 objectClasses.addAll(superType.objectClasses); 492 subResourceRouter.addAllRoutes(superType.subResourceRouter); 493 allProperties.putAll(superType.allProperties); 494 } 495 allProperties.putAll(declaredProperties); 496 for (final Map.Entry<String, PropertyMapper> property : allProperties.entrySet()) { 497 propertyMapper.property(property.getKey(), property.getValue()); 498 } 499 for (final SubResource subResource : subResources) { 500 subResource.build(rest2Ldap, id); 501 subResource.addRoutes(subResourceRouter); 502 } 503 } 504 505 PropertyMapper getPropertyMapper() { 506 return propertyMapper; 507 } 508 509 /** 510 * Returns the api description that describes a single instance resource. 511 * 512 * @param isReadOnly 513 * whether the associated resource is read only 514 * @return a new api description that describes a single instance resource. 515 */ 516 ApiDescription instanceApi(boolean isReadOnly) { 517 if (allProperties.isEmpty() && superType == null && subTypes.isEmpty()) { 518 // It is not used in the api description 519 // so do not generate anything for this resource 520 return null; 521 } 522 523 org.forgerock.api.models.Resource.Builder resource = org.forgerock.api.models.Resource. 524 resource() 525 .title(id) 526 .description(toLS(description)) 527 .resourceSchema(schemaRef("#/definitions/" + id)) 528 .mvccSupported(isMvccSupported()); 529 530 resource.read(readOperation()); 531 if (!isReadOnly) { 532 resource.update(updateOperation()); 533 resource.patch(patchOperation()); 534 for (Action action : supportedActions) { 535 resource.action(actions(action)); 536 } 537 } 538 539 return ApiDescription.apiDescription() 540 .id("unused").version("unused") 541 .definitions(definitions()) 542 .services(services(resource)) 543 .paths(paths()) 544 .errors(errors()) 545 .build(); 546 } 547 548 /** 549 * Returns the api description that describes a collection resource. 550 * 551 * @param isReadOnly 552 * whether the associated resource is read only 553 * @return a new api description that describes a collection resource. 554 */ 555 ApiDescription collectionApi(boolean isReadOnly) { 556 org.forgerock.api.models.Resource.Builder resource = org.forgerock.api.models.Resource. 557 resource() 558 .title(id) 559 .description(toLS(description)) 560 .resourceSchema(schemaRef("#/definitions/" + id)) 561 .mvccSupported(isMvccSupported()); 562 563 resource.items(buildItems(isReadOnly)); 564 resource.create(createOperation(CreateMode.ID_FROM_SERVER)); 565 resource.query(Query.query() 566 .stability(EVOLVING) 567 .type(QueryType.FILTER) 568 .queryableFields(ALL_FIELDS) 569 .pagingModes(COOKIE, OFFSET) 570 .countPolicies(NONE) 571 .error(errorRef(ERROR_BAD_REQUEST)) 572 .error(errorRef(ERROR_UNAUTHORIZED)) 573 .error(errorRef(ERROR_FORBIDDEN)) 574 .error(errorRef(ERROR_REQUEST_TIMEOUT)) 575 .error(errorRef(ERROR_ADMIN_LIMIT_EXCEEDED)) 576 .error(errorRef(ERROR_INTERNAL_SERVER_ERROR)) 577 .error(errorRef(ERROR_UNAVAILABLE)) 578 .build()); 579 580 return ApiDescription.apiDescription() 581 .id("unused").version("unused") 582 .definitions(definitions()) 583 .services(services(resource)) 584 .paths(paths()) 585 .errors(errors()) 586 .build(); 587 } 588 589 private Services services(org.forgerock.api.models.Resource.Builder resource) { 590 return Services.services() 591 .put(id, resource.build()) 592 .build(); 593 } 594 595 private Paths paths() { 596 return Paths.paths() 597 // do not put anything in the path to avoid unfortunate string concatenation 598 // also use UNVERSIONED and rely on the router to stamp the version 599 .put("", versionedPath().put(UNVERSIONED, resourceRef("#/services/" + id)).build()) 600 .build(); 601 } 602 603 private Definitions definitions() { 604 final Definitions.Builder definitions = Definitions.definitions(); 605 for (Resource res : collectTypeHierarchy(this)) { 606 definitions.put(res.id, res.buildJsonSchema()); 607 } 608 return definitions.build(); 609 } 610 611 private static Iterable<Resource> collectTypeHierarchy(Resource currentType) { 612 final List<Resource> typeHierarchy = new ArrayList<>(); 613 614 Resource ancestorType = currentType; 615 while (ancestorType.superType != null) { 616 ancestorType = ancestorType.superType; 617 typeHierarchy.add(ancestorType); 618 } 619 620 typeHierarchy.add(currentType); 621 622 addSubTypes(typeHierarchy, currentType); 623 return typeHierarchy; 624 } 625 626 private static void addSubTypes(final List<Resource> typeHierarchy, Resource res) { 627 for (Resource subType : res.subTypes) { 628 typeHierarchy.add(subType); 629 addSubTypes(typeHierarchy, subType); 630 } 631 } 632 633 private LocalizableString toLS(LocalizableMessage msg) { 634 if (msg != null) { 635 // FIXME this code does cannot work today because LocalizableMessage.getKey() does not exist 636 //if (msg.resourceName() != null) { 637 // return new LocalizableString("i18n:" + msg.resourceName() + "#" + msg.getKey()); 638 //} 639 return new LocalizableString(msg.toString()); 640 } 641 return null; 642 } 643 644 /** 645 * Returns the api description that describes a resource with sub resources. 646 * 647 * @param producer 648 * the api producer 649 * @return a new api description that describes a resource with sub resources. 650 */ 651 ApiDescription subResourcesApi(ApiProducer<ApiDescription> producer) { 652 return subResourceRouter.api(producer); 653 } 654 655 private boolean isMvccSupported() { 656 return allProperties.containsKey("_rev"); 657 } 658 659 private Items buildItems(boolean isReadOnly) { 660 final Items.Builder builder = Items.items(); 661 builder.pathParameter(Parameter 662 .parameter() 663 .name("id") 664 .type("string") 665 .source(PATH) 666 .required(true) 667 .build()) 668 .read(readOperation()); 669 if (!isReadOnly) { 670 builder.create(createOperation(CreateMode.ID_FROM_CLIENT)); 671 builder.update(updateOperation()); 672 builder.delete(deleteOperation()); 673 builder.patch(patchOperation()); 674 for (Action action : supportedActions) { 675 builder.action(actions(action)); 676 } 677 } 678 return builder.build(); 679 } 680 681 private org.forgerock.api.models.Action actions(Action action) { 682 switch (action) { 683 case MODIFY_PASSWORD: 684 return modifyPasswordAction(); 685 case RESET_PASSWORD: 686 return resetPasswordAction(); 687 default: 688 throw new RuntimeException("Not implemented for action " + action); 689 } 690 } 691 692 private static Create createOperation(CreateMode createMode) { 693 return Create.create() 694 .stability(EVOLVING) 695 .mode(createMode) 696 .error(errorRef(ERROR_BAD_REQUEST)) 697 .error(errorRef(ERROR_UNAUTHORIZED)) 698 .error(errorRef(ERROR_FORBIDDEN)) 699 .error(errorRef(ERROR_NOT_FOUND)) 700 .error(errorRef(ERROR_REQUEST_TIMEOUT)) 701 .error(errorRef(ERROR_VERSION_MISMATCH)) 702 .error(errorRef(ERROR_REQUEST_ENTITY_TOO_LARGE)) 703 .error(errorRef(ERROR_ADMIN_LIMIT_EXCEEDED)) 704 .error(errorRef(ERROR_INTERNAL_SERVER_ERROR)) 705 .error(errorRef(ERROR_UNAVAILABLE)) 706 .build(); 707 } 708 709 private static Delete deleteOperation() { 710 return Delete.delete() 711 .stability(EVOLVING) 712 .error(errorRef(ERROR_BAD_REQUEST)) 713 .error(errorRef(ERROR_UNAUTHORIZED)) 714 .error(errorRef(ERROR_FORBIDDEN)) 715 .error(errorRef(ERROR_NOT_FOUND)) 716 .error(errorRef(ERROR_REQUEST_TIMEOUT)) 717 .error(errorRef(ERROR_VERSION_MISMATCH)) 718 .error(errorRef(ERROR_REQUEST_ENTITY_TOO_LARGE)) 719 .error(errorRef(ERROR_READ_FOUND_MULTIPLE_ENTRIES)) 720 .error(errorRef(ERROR_ADMIN_LIMIT_EXCEEDED)) 721 .error(errorRef(ERROR_INTERNAL_SERVER_ERROR)) 722 .error(errorRef(ERROR_UNAVAILABLE)) 723 .build(); 724 } 725 726 private static Patch patchOperation() { 727 return Patch.patch() 728 .stability(EVOLVING) 729 .operations(ADD, REMOVE, REPLACE, INCREMENT) 730 .error(errorRef(ERROR_BAD_REQUEST)) 731 .error(errorRef(ERROR_UNAUTHORIZED)) 732 .error(errorRef(ERROR_FORBIDDEN)) 733 .error(errorRef(ERROR_NOT_FOUND)) 734 .error(errorRef(ERROR_REQUEST_TIMEOUT)) 735 .error(errorRef(ERROR_VERSION_MISMATCH)) 736 .error(errorRef(ERROR_REQUEST_ENTITY_TOO_LARGE)) 737 .error(errorRef(ERROR_READ_FOUND_MULTIPLE_ENTRIES)) 738 .error(errorRef(ERROR_ADMIN_LIMIT_EXCEEDED)) 739 .error(errorRef(ERROR_INTERNAL_SERVER_ERROR)) 740 .error(errorRef(ERROR_UNAVAILABLE)) 741 .build(); 742 } 743 744 private static Read readOperation() { 745 return Read.read() 746 .stability(EVOLVING) 747 .error(errorRef(ERROR_BAD_REQUEST)) 748 .error(errorRef(ERROR_UNAUTHORIZED)) 749 .error(errorRef(ERROR_FORBIDDEN)) 750 .error(errorRef(ERROR_NOT_FOUND)) 751 .error(errorRef(ERROR_REQUEST_TIMEOUT)) 752 .error(errorRef(ERROR_READ_FOUND_MULTIPLE_ENTRIES)) 753 .error(errorRef(ERROR_ADMIN_LIMIT_EXCEEDED)) 754 .error(errorRef(ERROR_INTERNAL_SERVER_ERROR)) 755 .error(errorRef(ERROR_UNAVAILABLE)) 756 .build(); 757 } 758 759 private static Update updateOperation() { 760 return Update.update() 761 .stability(EVOLVING) 762 .error(errorRef(ERROR_BAD_REQUEST)) 763 .error(errorRef(ERROR_UNAUTHORIZED)) 764 .error(errorRef(ERROR_FORBIDDEN)) 765 .error(errorRef(ERROR_NOT_FOUND)) 766 .error(errorRef(ERROR_REQUEST_TIMEOUT)) 767 .error(errorRef(ERROR_VERSION_MISMATCH)) 768 .error(errorRef(ERROR_REQUEST_ENTITY_TOO_LARGE)) 769 .error(errorRef(ERROR_READ_FOUND_MULTIPLE_ENTRIES)) 770 .error(errorRef(ERROR_ADMIN_LIMIT_EXCEEDED)) 771 .error(errorRef(ERROR_INTERNAL_SERVER_ERROR)) 772 .error(errorRef(ERROR_UNAVAILABLE)) 773 .build(); 774 } 775 776 private static org.forgerock.api.models.Action modifyPasswordAction() { 777 return org.forgerock.api.models.Action.action() 778 .stability(EVOLVING) 779 .name("modifyPassword") 780 .request(passwordModifyRequest()) 781 .description("Modify a user password. This action requires HTTPS.") 782 .error(errorRef(ERROR_BAD_REQUEST)) 783 .error(errorRef(ERROR_UNAUTHORIZED)) 784 .error(errorRef(ERROR_PASSWORD_MODIFY_REQUIRES_HTTPS)) 785 .error(errorRef(ERROR_PASSWORD_MODIFY_REQUIRES_AUTHENTICATION)) 786 .error(errorRef(ERROR_FORBIDDEN)) 787 .error(errorRef(ERROR_NOT_FOUND)) 788 .error(errorRef(ERROR_REQUEST_TIMEOUT)) 789 .error(errorRef(ERROR_VERSION_MISMATCH)) 790 .error(errorRef(ERROR_REQUEST_ENTITY_TOO_LARGE)) 791 .error(errorRef(ERROR_READ_FOUND_MULTIPLE_ENTRIES)) 792 .error(errorRef(ERROR_ADMIN_LIMIT_EXCEEDED)) 793 .error(errorRef(ERROR_INTERNAL_SERVER_ERROR)) 794 .error(errorRef(ERROR_UNAVAILABLE)) 795 .build(); 796 } 797 798 private static org.forgerock.api.models.Schema passwordModifyRequest() { 799 final JsonValue jsonSchema = json(object( 800 field("type", "object"), 801 field("description", "Supply the old password and new password."), 802 field("required", array("oldPassword", "newPassword")), 803 field("properties", object( 804 field("oldPassword", object( 805 field("type", "string"), 806 field("name", "Old Password"), 807 field("description", "Current password as a UTF-8 string."), 808 field("format", "password"))), 809 field("newPassword", object( 810 field("type", "string"), 811 field("name", "New Password"), 812 field("description", "New password as a UTF-8 string."), 813 field("format", "password"))))))); 814 return schema(jsonSchema); 815 } 816 817 private static org.forgerock.api.models.Action resetPasswordAction() { 818 return org.forgerock.api.models.Action.action() 819 .stability(EVOLVING) 820 .name("resetPassword") 821 .response(resetPasswordResponse()) 822 .description("Reset a user password to a generated value. This action requires HTTPS.") 823 .error(errorRef(ERROR_BAD_REQUEST)) 824 .error(errorRef(ERROR_UNAUTHORIZED)) 825 .error(errorRef(ERROR_PASSWORD_MODIFY_REQUIRES_HTTPS)) 826 .error(errorRef(ERROR_PASSWORD_MODIFY_REQUIRES_AUTHENTICATION)) 827 .error(errorRef(ERROR_FORBIDDEN)) 828 .error(errorRef(ERROR_NOT_FOUND)) 829 .error(errorRef(ERROR_REQUEST_TIMEOUT)) 830 .error(errorRef(ERROR_VERSION_MISMATCH)) 831 .error(errorRef(ERROR_REQUEST_ENTITY_TOO_LARGE)) 832 .error(errorRef(ERROR_READ_FOUND_MULTIPLE_ENTRIES)) 833 .error(errorRef(ERROR_ADMIN_LIMIT_EXCEEDED)) 834 .error(errorRef(ERROR_INTERNAL_SERVER_ERROR)) 835 .error(errorRef(ERROR_UNAVAILABLE)) 836 .build(); 837 } 838 839 private static org.forgerock.api.models.Schema resetPasswordResponse() { 840 final JsonValue jsonSchema = json(object( 841 field("type", "object"), 842 field("properties", object( 843 field("generatedPassword", object( 844 field("type", "string"), 845 field("description", "Generated password to communicate to the user."))))))); 846 return schema(jsonSchema); 847 } 848 849 private Schema buildJsonSchema() { 850 final List<String> requiredFields = new ArrayList<>(); 851 JsonValue properties = json(JsonValue.object()); 852 for (Map.Entry<String, PropertyMapper> prop : declaredProperties.entrySet()) { 853 final String propertyName = prop.getKey(); 854 final PropertyMapper mapper = prop.getValue(); 855 if (mapper.isRequired()) { 856 requiredFields.add(propertyName); 857 } 858 final JsonValue jsonSchema = mapper.toJsonSchema(); 859 if (jsonSchema != null) { 860 properties.put(propertyName, jsonSchema.getObject()); 861 } 862 } 863 864 final JsonValue jsonSchema = json(object(field("type", "object"))); 865 final String discriminator = getDiscriminator(); 866 if (discriminator != null) { 867 jsonSchema.put("discriminator", discriminator); 868 } 869 if (!requiredFields.isEmpty()) { 870 jsonSchema.put("required", requiredFields); 871 } 872 if (properties.size() > 0) { 873 jsonSchema.put("properties", properties.getObject()); 874 } 875 876 if (superType != null) { 877 return schema(json(object( 878 field("allOf", array( 879 object(field("$ref", "#/definitions/" + superType.id)), 880 jsonSchema.getObject()))))); 881 } 882 return schema(jsonSchema); 883 } 884 885 private String getDiscriminator() { 886 if (resourceTypeProperty != null) { 887 // Subtypes inherit the resourceTypeProperty from their parent. 888 // The discriminator must only be output for the type that defined it. 889 final String propertyName = resourceTypeProperty.leaf(); 890 return declaredProperties.containsKey(propertyName) ? propertyName : null; 891 } 892 return null; 893 } 894 895 private Errors errors() { 896 return Errors 897 .errors() 898 .put("passwordModifyRequiresHttps", 899 error(FORBIDDEN, "Password modify requires a secure connection.")) 900 .put("passwordModifyRequiresAuthn", 901 error(FORBIDDEN, "Password modify requires user to be authenticated.")) 902 .put("readFoundMultipleEntries", 903 error(INTERNAL_ERROR, "Multiple entries where found when trying to read a single entry.")) 904 .put("adminLimitExceeded", 905 error(INTERNAL_ERROR, "The request exceeded an administrative limit.")) 906 .build(); 907 } 908 909 static ApiError error(int code, String description) { 910 return ApiError.apiError().code(code).description(description).build(); 911 } 912 913 static ApiError error(int code, LocalizableString description) { 914 return ApiError.apiError().code(code).description(description).build(); 915 } 916 917 static ApiError errorRef(String referenceValue) { 918 return ApiError.apiError().reference(ref(referenceValue)).build(); 919 } 920 921 static org.forgerock.api.models.Resource resourceRef(String referenceValue) { 922 return org.forgerock.api.models.Resource.resource().reference(ref(referenceValue)).build(); 923 } 924 925 static org.forgerock.api.models.Schema schemaRef(String referenceValue) { 926 return Schema.schema().reference(ref(referenceValue)).build(); 927 } 928 929 private static org.forgerock.api.models.Schema schema(JsonValue jsonSchema) { 930 return Schema.schema().schema(jsonSchema).build(); 931 } 932 933 static Reference ref(String referenceValue) { 934 return Reference.reference().value(referenceValue).build(); 935 } 936}