/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright (c) 2015 ForgeRock AS. All Rights Reserved * * 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 * http://forgerock.org/license/CDDLv1.0.html * See the License for the specific language governing * permission and limitations under the License. * * When distributing Covered Code, include this CDDL * Header Notice in each file and include the License file * at http://forgerock.org/license/CDDLv1.0.html * If applicable, add the following below the CDDL Header, * with the fields enclosed by brackets [] replaced by * your own identifying information: * "Portions Copyrighted [year] [name of copyright owner]" */ package org.forgerock.openidm.managed; import javax.script.ScriptException; import org.forgerock.json.JsonException; import org.forgerock.json.JsonPointer; import org.forgerock.json.JsonValue; import org.forgerock.json.JsonValueException; import org.forgerock.json.crypto.JsonCrypto; import org.forgerock.json.crypto.JsonCryptoException; import org.forgerock.json.crypto.JsonEncryptor; import org.forgerock.json.resource.ForbiddenException; import org.forgerock.json.resource.InternalServerErrorException; import org.forgerock.openidm.crypto.CryptoService; import org.forgerock.openidm.util.RelationshipUtil; import org.forgerock.script.Script; import org.forgerock.script.ScriptEntry; import org.forgerock.script.ScriptRegistry; import org.forgerock.script.exception.ScriptThrownException; import org.forgerock.services.context.Context; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Represents a single field or property in a managed object's schema */ public class SchemaField { /** * Setup logging for the {@link SchemaField}. */ private final static Logger logger = LoggerFactory.getLogger(SchemaField.class); public static JsonPointer FIELD_ALL_RELATIONSHIPS = new JsonPointer("*" + RelationshipUtil.REFERENCE_ID); public static JsonPointer FIELD_REFERENCE = new JsonPointer(RelationshipUtil.REFERENCE_ID); public static JsonPointer FIELD_PROPERTIES = new JsonPointer(RelationshipUtil.REFERENCE_PROPERTIES); /** Schema field types */ enum SchemaFieldType { CORE, RELATIONSHIP } /** Schema field scopes */ private static enum Scope { PUBLIC, PRIVATE } /** The field name */ private String name; /** The field type */ private SchemaFieldType type; /** A boolean indicating if the field is returned by default */ private boolean returnByDefault = true; /** A boolean indicating if the field is nullable */ private boolean nullable = false; /** A boolean indicating if the field is virtual */ private boolean virtual; /** A boolean indicating if the field is an array */ private boolean isArray = false; /** A boolean indicating if the field is a reverse relationship */ private boolean isReverseRelationship = false; /** Matches against firstPropertyName if this is an inverse relationship */ private String reversePropertyName; /** Indicates if the field will be validated before saving or updating the object */ private boolean validationRequired = false; /** The CryptoService implementation */ private CryptoService cryptoService; /** The encryption configuration */ private JsonValue encryptionValue; /** The hashing configuration */ private JsonValue hashingValue; /** Script to execute when a property requires validation. */ private final ScriptEntry onValidate; /** Script to execute once an property is retrieved from the repository. */ private final ScriptEntry onRetrieve; /** Script to execute when an property is about to be stored in the repository. */ private final ScriptEntry onStore; /** The encryptor to use for encrypting JSON values */ private JsonEncryptor encryptor; /** String that indicates the privacy level of the property. */ private final Scope scope; /** * Constructor */ SchemaField(final String name, final JsonValue schema, final ScriptRegistry scriptRegistry, final CryptoService cryptoService) throws JsonValueException, ScriptException { this.name = name; this.cryptoService = cryptoService; this.scope = schema.get("scope").defaultTo(Scope.PUBLIC.name()).asEnum(Scope.class); // Initialize the type initializeType(schema); // Set the onRetrieve script if defined. this.onRetrieve = schema.isDefined("onRetrieve") ? scriptRegistry.takeScript(schema.get("onRetrieve")) : null; // Set the onStore script if defined. this.onStore = schema.isDefined("onStore") ? scriptRegistry.takeScript(schema.get("onStore")) : null; // Set the onValidate script if defined. this.onValidate = schema.isDefined("onValidate") ? scriptRegistry.takeScript(schema.get("onValidate")) : null; // Check if the field is a virtual field this.virtual = schema.get("isVirtual").defaultTo(false).asBoolean(); // Set the returnByDefault value for non-core fields if (isRelationship() || isVirtual()) { this.returnByDefault = schema.get("returnByDefault").defaultTo(false).asBoolean(); } // Initialize the encryptor if encryption is defined. encryptionValue = schema.get("encryption"); if (encryptionValue.isNotNull()) { setEncryptor(); } // Set the hashing value, if a secure hash is defined, and make sure the hashing algorithm is defined. hashingValue = schema.get("secureHash"); if (hashingValue.isNotNull()) { hashingValue.get("algorithm").required(); } } /** * Initializes the schema field's type. Recursively calls itself on the "items" schema if the base type is an array. * * @param schema a JSON object describing the schema field. * @throws JsonValueException when error is encountered while parsing the JSON object. */ private void initializeType(JsonValue schema) throws JsonValueException { JsonValue type = schema.get("type"); if (type.isString() && type.asString().equals("array")) { isArray = true; initializeType(schema.get("items")); } else { if (type.isString()) { setType(type.asString()); } else if (type.isList()) { for (JsonValue t : type) { setType(t.asString()); } } else { throw new JsonValueException(type, "Schema field 'type' must be a String or List"); } if (isRelationship()) { this.isReverseRelationship = schema.get("reverseRelationship").defaultTo(false).asBoolean(); if (this.isReverseRelationship) { this.reversePropertyName = schema.get("reversePropertyName").required().asString(); } } // Set validation flag this.validationRequired = schema.get("validate").defaultTo(false).asBoolean(); } } /** * A synchronized method for setting the encryptor is if hasn't already been set and there exists an encryption * configuration. */ private synchronized void setEncryptor() { if (encryptor == null && encryptionValue.isNotNull()) { try { encryptor = cryptoService.getEncryptor( encryptionValue.get("cipher").defaultTo("AES/CBC/PKCS5Padding").asString(), encryptionValue.get("key").required().asString()); } catch (JsonCryptoException jce) { logger.warn("Unable to set encryptor"); } } } /** * Sets the type of the schema field. * * @param type the type of this schema field */ private void setType(String type) { if (type.equals("relationship")) { this.type = SchemaFieldType.RELATIONSHIP; } else if (type.equals("null")) { this.nullable = true; } else { this.type = SchemaFieldType.CORE; } } /** * Returns the type of this schema field. * * @return the type of this schema field */ public SchemaFieldType getType() { return type; } /** * Returns a boolean indicating if the field is a reverse relationship. * * @return true if the relationship is reverse, otherwise false. */ public boolean isReverseRelationship() { return isReverseRelationship; } /** * Returns the name used by the reverse relationship. * * @return The property name used by the reverse relationship. */ public String getReversePropertyName() { return reversePropertyName; } /** * Returns a boolean indicating if the field is returned by default. * * @return true if the field is returned by default, false otherwise. */ public boolean isReturnedByDefault() { return returnByDefault; } /** * Returns a boolean indicating if the field is a relationship. * * @return true if the field is a relationship, false otherwise. */ public boolean isRelationship() { return type == SchemaFieldType.RELATIONSHIP; } /** * Returns a boolean indicating if the field is virtual. * * @return true if the field is virtual, false otherwise. */ public boolean isVirtual() { return virtual; } /** * Returns a boolean indicating if the field is nullable. * * @return true if the field is nullable, false otherwise. */ public boolean isNullable() { return nullable; } /** * Returns a boolean indicating if the field is an array. * * @return true if the field is an array, false otherwise. */ public boolean isArray() { return isArray; } /** * Returns a String representing the field's name. * * @return the field's name. */ public String getName() { return name; } /** * Returns true if the field should be validated before any action is taken on the managed object. * * @return True if the field should be validated before any action is taken on the managed object. */ public boolean isValidationRequired() { return validationRequired; } /** * Returns a boolean indicating if the property is private. * * @return true if the property is private, false otherwise. */ boolean isPrivate() { return Scope.PRIVATE.equals(scope); } /** * Executes a script that performs a transformation of a property. Populates the {@code "property"} property in the * script scope with the property value. Any changes to the property are reflected back into the managed object if * the script successfully completes. * * @param type type of script to execute. * @param script the script to execute, or {@code null} to execute nothing. * @param managedObject the managed object containing the property value. * @throws InternalServerErrorException if script execution fails. */ private void execScript(Context context, String type, ScriptEntry script, JsonValue managedObject) throws InternalServerErrorException { if (script != null) { Object result = null; Script scope = script.getScript(context); scope.put("property", managedObject.get(name).getObject()); scope.put("propertyName", name); scope.put("object", managedObject.getObject()); scope.put("context", context); try { result = scope.eval(); } catch (ScriptException se) { String msg = name + " " + type + " script encountered exception"; logger.debug(msg, se); throw new InternalServerErrorException(msg, se); } logger.debug("Script {} result: {}", context, result); managedObject.put(name, result); } } /** * Executes the script if it exists, to validate a property value. * * @param value the JSON value containing the property value to be validated. * @throws ForbiddenException if validation of the property fails. * @throws InternalServerErrorException if any other exception occurs during execution. */ void onValidate(Context context, JsonValue value) throws ForbiddenException, InternalServerErrorException { if (onValidate != null) { Script scope = onValidate.getScript(context); scope.put("property", value.get(name).getObject()); try { scope.eval(); } catch (ScriptThrownException ste) { // validation failed throw new ForbiddenException(ste.getValue().toString()); } catch (ScriptException se) { String msg = name + " onValidate script encountered exception"; logger.debug(msg, se); throw new InternalServerErrorException(msg, se); } } } /** * Performs tasks when a property has been retrieved from the repository, including: executing the * {@code onRetrieve} script. * * @param value the JSON value that was retrieved from the repository. * @throws InternalServerErrorException if an exception occurs processing the property. */ void onRetrieve(Context context, JsonValue value) throws InternalServerErrorException { execScript(context, "onRetrieve", onRetrieve, value); } /** * Performs tasks when a property is to be stored in the repository, including: executing the {@code onStore} script * and encrypting or hashing the property. * * @param value the JSON value to be stored in the repository. * @throws InternalServerErrorException if an exception occurs processing the property. */ void onStore(Context context, JsonValue value) throws InternalServerErrorException { execScript(context, "onStore", onStore, value); setEncryptor(); try { if (value.isDefined(name)) { JsonValue propValue = value.get(name); if (encryptor != null && !cryptoService.isEncrypted(propValue)) { // Encrypt the field value.put(name, new JsonCrypto(encryptor.getType(), encryptor.encrypt(propValue)).toJsonValue()); } else if (hashingValue.isNotNull() && !cryptoService.isHashed(propValue)) { // Hash the field value.put(name, cryptoService.hash(propValue, hashingValue.get("algorithm").asString())); } } } catch (JsonCryptoException jce) { String msg = name + " property encryption exception"; logger.debug(msg, jce); throw new InternalServerErrorException(msg, jce); } catch (JsonException je) { String msg = name + " property transformation exception"; logger.debug(msg, je); throw new InternalServerErrorException(msg, je); } } }