ManagedObjectSet.java revision 6e143fb1765b09ff8dff832ba0cd6914efbefc24
/*
* The contents of this file are subject to the terms of the Common Development and
* Distribution License (the License). You may not use this file except in compliance with the
* License.
*
* You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
* specific language governing permission and limitations under the License.
*
* When distributing Covered Software, include this CDDL Header Notice in each file and include
* the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
* Header, with the fields enclosed by brackets [] replaced by your own identifying
* information: "Portions copyright [year] [name of copyright owner]".
*
* Portions copyright 2011-2015 ForgeRock AS.
*/
/**
* Provides access to a set of managed objects of a given type: managed/[type]/{id}.
*
*/
/** Actions supported by this resource provider */
enum Action {
}
/** Supported script hooks */
enum ScriptHook {
/** Script to execute when the creation of an object is being requested. */
/** Script to execute when the read of an object is being requested. */
/** Script to execute when the update of an object is being requested. */
/** Script to execute when the deletion of an object is being requested. */
/** Script to execute after the create of an object has completed. */
/** Script to execute after the update of an object has completed. */
/** Script to execute after the delete of an object has completed. */
/** Script to execute when a managed object requires validation. */
/** Script to execute once an object is retrieved from the repository. */
/** Script to execute when an object is about to be stored in the repository. */
/** Script to execute when synchronization of managed objects to external targets is complete. */
}
/**
* Setup logging for the {@link ManagedObjectSet}.
*/
/** The managed objects service that instantiated this managed object set. */
private final CryptoService cryptoService;
/** The connection factory for access to the router */
private final ConnectionFactory connectionFactory;
/** Audit Activity Log helper */
private final ActivityLogger activityLogger;
/** Name of the managed object type. */
private final ResourcePath managedObjectPath;
/** The schema to use to validate the structure and content of the managed object. */
private final ManagedObjectSchema schema;
/** Map of scripts to execute on specific {@link ScriptHook}s. */
private final Map<ScriptHook, ScriptEntry> scriptHooks = new EnumMap<ScriptHook, ScriptEntry>(ScriptHook.class);
/** Properties for which triggers are executed during object set operations. */
/** reference to the sync service route; used to decided whether or not to perform a sync action */
/** Map of relationship property names and their accompanying sets */
/** Flag for indicating if policy enforcement is enabled */
private final boolean enforcePolicies;
/**
* Constructs a new managed object set.
*
* @param scriptRegistry
* the script registry
* @param cryptoService
* the cryptographic service
* @param syncRoute
* a reference to the RouteService on "sync"
* @param connectionFactory
* the router connection factory
* @param config
* configuration object to use to initialize managed object set.
* @throws JsonValueException
* when the configuration is malformed
* @throws ScriptException
* when the script configuration is malformed or the script is
* invalid.
*/
final AtomicReference<RouteService> syncRoute, ConnectionFactory connectionFactory, JsonValue config)
throws JsonValueException, ScriptException {
this.cryptoService = cryptoService;
this.connectionFactory = connectionFactory;
}
}
}
}
}
enforcePolicies = Boolean.parseBoolean(IdentityServer.getInstance().getProperty("openidm.policy.enforcement.enabled", "true"));
}
/**
* Generates a fully-qualified object identifier for the managed object.
*
* @param resourceId
* the local managed object identifier to qualify.
* @return the fully-qualified managed object identifier.
*/
return resourceId != null
}
/**
* Generates a fully-qualified object identifier for the repository.
*
* @param resourceId
* the local managed object identifier to qualify.
* @return the fully-qualified repository object identifier.
*/
}
/**
* Executes a script if it exists, populating an {@code "object"} property
* in the root scope.
*
* @param hook
* the script-hook to execute
* @param value
* the object to be populated in the script scope.
* @param additionalProps
* a Map of additional properties to add the the script scope
* @throws ForbiddenException
* if the script throws an exception.
* @throws InternalServerErrorException
* if any other exception is encountered.
*/
private void execScript(final Context context, ScriptHook hook, JsonValue value, JsonValue additionalProps)
throws ResourceException {
}
}
try {
} catch (ScriptThrownException ste) {
// Allow for scripts to set their own exception
} catch (ScriptException se) {
}
}
}
/**
* Prepares a map of additional bindings for the script hook invocation.
*
* @param context the current Context
* @param request the Request being processed
* @param resourceId the resourceId of the object being manipulated
* @param oldObject the old object value
* @param newObject the new object value
* @return a JsonValue map of script bindings
*/
// TODO once SCRIPT-1 is implemented, this can be removed and the resourceName can be obtained via context.router.getBaseUri()
return scriptBindings;
}
/**
* Executes all of the necessary trigger scripts when an object is retrieved
* from the repository.
*
* @param context the current Context
* @param request the Request being processed
* @param resourceId the resourceId of the object being manipulated
* @param value
* the JSON value that was retrieved from the repository.
* @throws ForbiddenException
* if a validation trigger throws an exception.
* @throws InternalServerErrorException
* if any other exception occurs.
*/
private void onRetrieve(Context context, Request request, String resourceId, ResourceResponse value) throws ResourceException {
}
}
private void populateVirtualProperties(Context context, JsonValue content) throws ForbiddenException,
}
}
}
/**
* Executes all of the necessary trigger scripts when an object is to be
* stored in the repository.
*
* @param value
* the JSON value to be stored in the repository.
* @throws ForbiddenException
* if a validation trigger throws an exception.
* @throws InternalServerErrorException
* if any other exception occurs.
*/
}
// TODO: schema validation here (w. optimizations)
}
}
/**
* Decrypt the value
*
* @param value
* a json value with potentially encrypted value(s)
* @return object with values decrypted
* @throws InternalServerErrorException
* if decryption failed for any reason
*/
try {
} catch (JsonException je) {
throw new InternalServerErrorException(je);
}
}
/**
* Decrypt the value
*
* @param value
* a json value with potentially encrypted value(s)
* @return object with values decrypted
* @throws InternalServerErrorException
* if decryption failed for any reason
*/
private ResourceResponse decrypt(final ResourceResponse value) throws InternalServerErrorException {
try {
// makes a copy, which we can modify
} catch (JsonException je) {
throw new InternalServerErrorException(je);
}
}
/**
* Forbid the use of sub objects
*
* @param id
* the identifier to check
* @throws ForbiddenException
* if the identifier identifies a sub object
*/
throw new ForbiddenException("Sub-objects are not supported");
}
}
/**
* Forbid operation without id, on the whole object set
*
* @param id
* the identifier to check
* @throws ForbiddenException
* if there is no identifier.
*/
throw new ForbiddenException("Operation not allowed on entire object set");
}
}
/**
* Update a resource as part of an update or patch request.
*
* @param context the current Context
* @param request the source Request
* @param resourceId the resource id of the object being modified
* @param rev the revision of hte object being modified
* @param oldValue the old value of the object
* @param newValue the new value of the object
* @return a {@link ResourceResponse} object representing the updated resource
* @throws ResourceException
*/
private ResourceResponse update(final Context context, Request request, String resourceId, String rev,
throws ResourceException {
}
// Execute the onUpdate script if configured
// Populate the virtual properties (so they are updated for sync-ing)
// Strip relationships from object before storing
// Perform pre-property encryption
// Perform update
// Execute the postUpdate script if configured
performSyncAction(context, request, resourceId, SynchronizationService.SyncServiceAction.notifyUpdate,
// Put relationships back in before we respond
return response;
}
/**
* Persist all relationship fields contained in the JsonValue map to their accompanying
* {@link #relationshipProviders}
*
* @param context The current context
* @param resourceId The id of the resource these relationships are associated with
* @param json A JsonValue map that contains relationship fields and value(s) to be persisted
* @return A {@link JsonValue} map containing each relationship field and its persisted value(s)
*/
private JsonValue persistRelationships(Context context, String resourceId, final JsonValue json) throws ResourceException {
if (relationshipValue != null) {
}
}
));
}
}
// Join json maps
}
return joined;
}
}
/**
* Applies a patch document to an object, or by finding an object in the
* object set itself via query parameters. As this is an action, the patch
* document to be applied is in the {@code _entity} parameter.
*
* @param context the current Context
* @param request the {@link ActionRequest}
* @return a {@link ResourceResponse} representing the patched object.
* @throws ResourceException
*/
private ResourceResponse patchAction(final Context context, final ActionRequest request) throws ResourceException {
throw new BadRequestException(
"The request could not be processed because the provided content is not a JSON array");
}
// Build query request from action parameters looking for query parameters
// use JsonValue to coerce Map<String, String> to Map<String, Object> - blech
new QueryResourceHandler() {
return true;
}
});
try {
} catch (ResourceException e) {
throw e;
} catch (Exception e) {
throw new InternalServerErrorException(e.getMessage(), e);
}
throw new InternalServerErrorException("Query result must yield one matching object");
} else {
throw new NotFoundException("Query returned no results");
}
}
public Promise<ResourceResponse, ResourceException> createInstance(Context context, CreateRequest request) {
// Check if the new id is specified in content, and use it if it is
}
try {
// decrypt any incoming encrypted properties
// Populate the virtual properties (so they are available for sync-ing)
// Remove relationships so they don't get persisted
// includes per-property encryption
.setContent(value);
// Execute the postCreate script if configured
createResponse.getContent()));
// Sync any targets after managed object is created
// Place persisted relationships in content before sending to the handler
} catch (ResourceException e) {
return e.asPromise();
} catch (Exception e) {
}
}
public Promise<ResourceResponse, ResourceException> readInstance(final Context context, String resourceId,
try {
} catch (ResourceException e) {
return e.asPromise();
} catch (Exception e) {
}
}
/**
* Fetch the current relationship(s) for all relationship fields.
*
* @param context The current context
* @param resourceId The id of the resource to fetch relationships of
* @return A {@link JsonValue} map containing all relationship fields and their values
*/
private JsonValue fetchRelationshipFields(Context context, String resourceId) throws ExecutionException, InterruptedException {
}
return joined;
}
public Promise<ResourceResponse, ResourceException> updateInstance(final Context context, final String resourceId,
final UpdateRequest request) {
+ request.getRevision());
try {
// decrypt any incoming encrypted properties
}
} catch (ResourceException e) {
return e.asPromise();
} catch (Exception e) {
}
}
public Promise<ResourceResponse, ResourceException> deleteInstance(final Context context, final String resourceId,
+ request.getRevision());
try {
}
ResourceResponse deletedResource = connectionFactory.getConnection().delete(context, deleteRequest);
// Delete any relationships assocated with this resource
}
// Wait for deletions to complete before continuing
// Execute the postDelete script if configured
execScript(context, ScriptHook.postDelete, null, prepareScriptBindings(context, request, resourceId,
// Perform notifyDelete synchronization
performSyncAction(context, request, resourceId, SynchronizationService.SyncServiceAction.notifyDelete,
} catch (ResourceException e) {
return e.asPromise();
} catch (Exception e) {
}
}
public Promise<ResourceResponse, ResourceException> patchInstance(Context context, String resourceId,
try {
} catch (ResourceException e) {
return e.asPromise();
}
}
/**
* Patches the given resource and will also remove private properties if it is an external call based upon context.
*
* @param context
* @param request
* @param resourceId
* @param revision Expected revision of the resource. Patch will fail if non-null and not matching.
* @param patchOperations
*
* @return The patched ResourceResponse with private properties omitted if called externally.
*
* @throws ResourceException
*/
throws ResourceException {
// Get the oldest value for diffing in the log
// JsonValue oldValue = new JsonValue(cryptoService.getRouter().read(repoId(id)));
}
/**
* Patches the given resource and will also remove private properties if it is an external call based upon context.
*
* @param context
* @param request
* @param resource The resource to be patched
* @param revision
* @param patchOperations
*
* @return The patched ResourceResponse with private properties omitted if called externally.
*
* @throws ResourceException
*/
throws ResourceException {
// FIXME: There's no way to decrypt a patch document. :-( Luckily, it'll work for now with patch action.
boolean retry = forceUpdate;
do {
try {
// decrypt any incoming encrypted properties
// If we haven't defined a revision, we need to get the current revision
}
if (!modified) {
return null;
}
// Check if policies should be enforced
if (enforcePolicies) {
// this parameter is used in conjunction with the test in policy.js
// to ensure that the reauth policy is enforced
}
JsonValue result = connectionFactory.getConnection().action(context, policyAction).getJsonContent();
}
}
ResourceResponse patchedResource = update(context, request, resource.getId(), rev, resource.getContent(), newValue);
retry = false;
} catch (PreconditionFailedException e) {
if (forceUpdate) {
} else {
// If it fails and we're not trying to force an update, we gave it our best shot
throw e;
}
} catch (ResourceException e) {
throw e;
}
} while (retry);
return null;
}
public Promise<QueryResponse, ResourceException> queryCollection(final Context context, final QueryRequest request,
final QueryResourceHandler handler) {
// The "executeOnRetrieve" parameter is used to indicate if is returning a full managed object
// The onRetrieve script should only be run queries that return full managed objects
? false
try {
new QueryResourceHandler() {
// Check if the onRetrieve script should be run
if (onRetrieve) {
try {
} catch (ResourceException e) {
ex[0] = e;
return false;
}
}
}
});
}
return queryResponse.asPromise();
} catch (ResourceException e) {
return e.asPromise();
}
}
public Promise<ActionResponse, ResourceException> actionInstance(Context context, String resourceId,
try {
case patch:
case triggerSyncCheck:
// Sync changes if required, in particular virtual/calculated attribute changes
default:
}
} catch (ResourceException e) {
return e.asPromise();
} catch (IllegalArgumentException e) {
// from getActionAsEnum
} catch (Exception e) {
}
}
/**
* Processes action requests.
* <p>
* If the {@code _action} parameter is {@code patch}, then the request is
* handled as a partial modification to an object, either explicitly
* (identifier is supplied) or by query (query parameters specify the query
* to perform to yield a single object to patch.
*/
public Promise<ActionResponse, ResourceException> actionCollection(Context context, ActionRequest request) {
try {
case patch:
default:
}
} catch (ResourceException e) {
return e.asPromise();
} catch (IllegalArgumentException e) {
// from getActionAsEnum
}
}
// -------- Implements the ScriptListener
case ScriptEvent.UNREGISTERING:
}
}
/**
* Returns the name of the managed object set.
*/
return name;
}
public String getTemplate() {
}
/**
* Prepares the response contents by removing the following: any private properties (if the request is
* from an external call), any virtual or relationship properties that are not set to returnByDefault.
*
* @param context the current ServerContext
* @param resource the Resource to prepare
* @param fields a list of fields to return specified in the request
* @return the prepared Resource object
* @throws ResourceException
*/
private ResourceResponse prepareResponse(Context context, ResourceResponse resource, List<JsonPointer> fields) {
// Return all relationship fields, so remove them from fieldsToRemove map
}
// Allow the field by removing it from the fieldsToRemove list.
} else {
// Check for resource expansion and build up map of fields to expand
if (expansionPair != null) {
// Allow the field by removing it from the fieldsToRemove list (if there)
// Add the field to the expansion map
// Initialize the list of fields in the resource expansion map
// replace the field in the fields list with the relationship field
}
}
}
}
}
}
// Remove all relationship and virtual fields that are not returned by default, or explicitly listed
}
// Loop over the relationship fields to expand
// The schema for the field to expand
// The list of fields to include from the expanded resource
// The value of the relationship field
try {
// Perform the resource expansion
if (schemaField.isArray()) {
// The field is an array of relationship objects
}
} else {
// The field is a relationship object
}
} else {
}
} catch (ResourceException e) {
}
}
// only cull private properties if this is an external call
}
}
}
// Update the list of fields in the response
}
return resource;
}
/**
* Expands the provided resource represented by a {@link JsonValue} relationship object. A read request
* will be issued for the resource identified by the "_ref" field in the supplied relationship object. A
* supplied {@link List} of fields indicates which fields to read and then merge with the relationship
* object.
*
* @param context the {@link Context} of the request
* @param value the value of the relationship object
* @param fieldsList the list of fields to read and merge with the relationship object.
* @throws ResourceException if an error is encountered.
*/
throws ResourceException {
// Create and issue a read request on the referenced resource with the specified list of fields
// Merge the result with the supplied relationship object
} else {
}
}
/**
* Returns a deep copy of the supplied {@link JsonValue}.
* Used to remove special relation fields before persisting the
* value to the repository.
*
* @param value The JsonValue map to strip relationship fields from
* @return A deep copy of the JsonValue value with relationship fields removed
*/
}
return stripped;
}
/**
* Sends a sync action request to the synchronization service
*
* @param context the Context of the request
* @param request the Request being processed
* @param resourceId the additional resourceId parameter telling the synchronization service which object
* is being synchronized
* @param action the {@link org.forgerock.openidm.sync.impl.SynchronizationService.SyncServiceAction}
* @param oldValue the previous object value before the change (if applicable, or null if not)
* @param newValue the object value to sync
* @throws ResourceException in case of a failure that was not handled by the ResultHandler
*/
private void performSyncAction(final Context context, final Request request, final String resourceId,
final SynchronizationService.SyncServiceAction action, final JsonValue oldValue, final JsonValue newValue)
throws ResourceException {
// The "sync" route may be down (unconfigured) or in the process of being re-configured;
// if this is the case, we don't want a router error on the ActionRequest below. Just log
// the warning and return. When the SynchronizationService comes back up (or when the
// reconfiguration is complete), the AtomicReference<RouteService> in ManagedObjectService
// will get set again.
return;
}
try {
.setAdditionalParameter(SynchronizationService.ACTION_PARAM_RESOURCE_CONTAINER, managedObjectPath.toString())
boolean success = false;
try {
success = true;
} catch (ResourceException e) {
success = false;
} catch (Exception e) {
success = false;
}
try {
// Execute the sync script
} catch (ResourceException e) {
throw e;
}
throw syncScriptError[0];
}
} catch (NotFoundException e) {
throw e;
}
}
/**
* Get the {@link ResourcePath} associated with this set.
* @return The {@link ResourcePath} associated with this object set.
*/
public ResourcePath getPath() {
return managedObjectPath;
}
/**
* Get the {@link ManagedObjectSchema} associated with this set.
* @return The {@link ManagedObjectSchema} associated with this object set.
*/
public ManagedObjectSchema getSchema() {
return schema;
}
/**
* Get the current map of {@link RelationshipProvider} for each relationship field.
*/
return relationshipProviders;
}
}