/* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License, Version 1.0 only * (the "License"). You may not use this file except in compliance * with the License. * * You can obtain a copy of the license at * docs/licenses/cddl.txt * or http://www.opensource.org/licenses/cddl1.php. * See the License for the specific language governing permissions * and limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each * file and include the License file at * docs/licenses/cddl.txt. If applicable, * add the following below this CDDL HEADER, with the fields enclosed * by brackets "[]" replaced with your own identifying information: * Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END * * * Portions Copyright 2017-2024 Ping Identity Corporation */ package com.unboundid.directory.sdk.examples; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.TextNode; import com.unboundid.directory.sdk.broker.api.Advice; import com.unboundid.directory.sdk.broker.config.AdviceConfig; import com.unboundid.directory.sdk.broker.types.AdviceStatement; import com.unboundid.directory.sdk.broker.types.BrokerContext; import com.unboundid.directory.sdk.broker.types.HttpRequestResponse; import com.unboundid.directory.sdk.broker.types.NotFulfilledException; import com.unboundid.directory.sdk.broker.types.PolicyRequestDetails; import com.unboundid.scim2.common.Path; import com.unboundid.scim2.common.exceptions.ScimException; import com.unboundid.scim2.common.utils.JsonUtils; import com.unboundid.util.args.Argument; import com.unboundid.util.args.ArgumentException; import com.unboundid.util.args.ArgumentParser; import com.unboundid.util.args.StringArgument; import com.unboundid.util.args.IntegerArgument; import javax.ws.rs.ServerErrorException; import javax.ws.rs.core.Response; import java.io.IOException; import java.util.Iterator; import java.util.List; /** * Example of a third-party policy Advice. This example can be applied * to any policy request with an action of "retrieve", which will include * many HTTP GET operations as well as resources returned by POST, PUT, or * PATCH. The example implements a simple obfuscation scheme for one or more * attributes of the JSON response, using a configurable obfuscation method. * * The following extension arguments are used to configure how obfuscation is * performed: * <UL> * <LI>obfuscateChar -- The character to use to overwrite a portion of the * obfuscated attribute. If not specified defaults to "X". * </LI> * <LI>count -- The number of characters to overwrite in the obfuscated * attribute, starting with the first character. * * </LI> * </UL> * * The following dsconfig command creates an instance of this obligation. It * will cause the obfuscated attributes to be obfuscated by replacing the first * three characters with the letter 'Z'. The attributes to be obfuscated are * determined by policy and are specified in the advice payload when this * type of advice is returned from policy. * <p> * <code> * dsconfig create-advice \ * --advice-name "Example Advice" \ * --type third-party \ * --set advice-id:obfuscate-attributes, * --set extension-class:\ * com.unboundid.directory.sdk.examples.ExampleAdvice \ * --set extension-argument:obfuscateChar=Z \ * --set extension-argument:count=3 * </code> * </p> * */ public class ExampleAdvice extends Advice { /** * The name of the initialization argument that will be used to specify the * obfuscation character. */ private static final String ARG_OBFUSCATE_CHAR = "obfuscateChar"; /** * The name of the initialization argument that will be used to specify the * number of characters to obfuscate. */ private static final String ARG_COUNT = "count"; /** * JSON object mapper. */ private static final ObjectMapper objectMapper = new ObjectMapper(); /** * The character to use for obfuscation. */ private Character obfuscationCharacter; /** * The number of characters to obfuscate. */ private int obfuscationCount; /** * Handle to the ServerContext object. */ private BrokerContext serverContext; /** * Creates a new instance of this policy obligation. All advice * implementations must include a default constructor, but any * initialization should generally be performed in the * {@code initializeAdvice} method. */ public ExampleAdvice() { } /** * Retrieves a human-readable name for this extension. * @return extension name */ @Override public String getExtensionName() { return "Example Obfuscation Advice"; } /** * Retrieves a human-readable description of this extension. * @return text description string */ @Override public String[] getExtensionDescription() { return new String[] { "This Advice implementation serves as an example that may be used" + " to demonstrate the process for creating a third-party" + " policy Advice. It implements a simple obfuscation of a" + " JSON resource attribute. It takes two arguments at start-up:" + " 'obfuscateChar' contains the character to use for obfuscation,"+ " and 'count' contains the number of characters to replace with" + " the obfuscateChar value (starting from the first character)." + " The attributes to be obfuscated are passed from the policy" + " as the Advice payload and are formatted as a JSON string" + " array. The elements of the array may be a simple attribute" + " name or a JSON path into the resource structure." }; } /** * Updates the provided argument parser to define any configuration arguments * which may be used by this Advice. The argument parser may also be updated * to define relationships between arguments (e.g., to specify required, * exclusive, or dependent argument sets). * * @param parser The argument parser to be updated with the configuration * arguments which may be used by this provider. * * @throws ArgumentException If a problem is encountered while updating the * provided argument parser. */ @Override public void defineConfigArguments(final ArgumentParser parser) throws ArgumentException { // obfuscate character is optional, default is 'X' final Argument obfuscateCharArg = new StringArgument( null, ARG_OBFUSCATE_CHAR, false, 1, null, "The character to use for obfuscation", "X"); // obfuscation count is optional, default is 4 final Argument obfuscateCountArg = new IntegerArgument( null, ARG_COUNT, false, 1, null, "The number of characters to replace with the obfuscation character", 1, Integer.MAX_VALUE, 4); parser.addArgument(obfuscateCharArg); parser.addArgument(obfuscateCountArg); } /** * Initializes the Advice implementation. Any initialization should be * performed here. This method should generally store the * {@link BrokerContext} in a class member so that it can be used elsewhere * in the implementation. * * @param serverContext A handle to the server context for the server in * which this extension is running. Extensions should * typically store this in a class member. * @param config The general configuration for this object. * @param parser The argument parser which has been initialized from * the configuration for this policy obligation. * @throws Exception If a problem occurs while initializing this store * adapter plugin. */ @Override public void initializeAdvice( final BrokerContext serverContext, final AdviceConfig config, final ArgumentParser parser) throws Exception { this.serverContext = serverContext; final StringArgument obfuscateCharArg = parser.getStringArgument(ARG_OBFUSCATE_CHAR); if (obfuscateCharArg.getValue().length() != 1) { throw new Exception( "Value of '" + ARG_OBFUSCATE_CHAR + "' must be a single character."); } this.obfuscationCharacter = obfuscateCharArg.getValue().charAt(0); final IntegerArgument obfuscateCountArg = parser.getIntegerArgument(ARG_COUNT); this.obfuscationCount = obfuscateCountArg.getValue(); } /** * This method is called after a PingAuthorize resource is retrieved * but before it is returned to the calling client application. * @param requestDetails information about the authorization request that * triggered this policy obligation * @param statements List of advice instances with payload and * attributes containing advice details. * @param requestResponse The response object to the body of which * obfuscation is applied. * @return The obfuscated response. @throws NotFulfilledException if the advice cannot be successfully * applied. If the advice is defined to be obligatory, * throwing this exception will cause the requested * operation to fail. If the advice is not obligatory, * throwing this exception will cause an error to be * logged, however the requested operation will not * otherwise be impacted. */ @Override public HttpRequestResponse apply( final PolicyRequestDetails requestDetails, final List<? extends AdviceStatement> statements, final HttpRequestResponse requestResponse) throws NotFulfilledException { final JsonNode resourceNode = requestResponse.getBody(); if (!(resourceNode instanceof ObjectNode)) { throw new ServerErrorException( "Unsupported use of obfuscation obligation on object of type " + resourceNode.getNodeType().toString(), Response.Status.INTERNAL_SERVER_ERROR); } // obfuscation should only be done when retrieving data, not saving. if (!requestDetails.getAction().equals("retrieve")) { throw new ServerErrorException( "Obfuscation obligation cannot be used on requests with action '" + requestDetails.getAction() + "'", Response.Status.INTERNAL_SERVER_ERROR); } ObjectNode resource = (ObjectNode) resourceNode; for (AdviceStatement statement : statements) { JsonNode payloadNode; try { payloadNode = objectMapper.readTree(statement.getPayload()); } catch (IOException e) { // couldn't JSON parse the payload throw new ServerErrorException( "Error processing payload for obfuscation advice '" + statement.getName() + "': " + e.getMessage(), Response.Status.INTERNAL_SERVER_ERROR); } if (!payloadNode.isArray()) { // payload is expected to be an array, e.g. ["attr1", "attr2"] throw new ServerErrorException(String.format( "The payload for advice '%s' is expected to be of type JSON " + "array", statement.getName()), Response.Status.INTERNAL_SERVER_ERROR); } Iterator<JsonNode> iter = payloadNode.elements(); while (iter.hasNext()) { JsonNode attrNode = iter.next(); if (!attrNode.isTextual()) { throw new ServerErrorException(String.format( "Advice %s: non-string payload item found", statement.getName()), Response.Status.INTERNAL_SERVER_ERROR); } try { Path attributePath = Path.fromString(attrNode.asText()); JsonNode attributeNode = JsonUtils.getValue(attributePath, resource); // if the attribute doesn't exist, there's nothing to do if (attributeNode == null || attributeNode.isNull()) { continue; } // if the attribute is not string-valued, the obligation cannot // be fulfilled. if (!attributeNode.isTextual()) { throw new NotFulfilledException("Attribute " + attributePath.toString() + " is not string-valued."); } String valueToObfuscate = attributeNode.textValue(); StringBuilder obfuscated = new StringBuilder(); for (int i = 0; i < valueToObfuscate.length(); i++) { if (i < obfuscationCount) { obfuscated.append(obfuscationCharacter); } else { obfuscated.append(valueToObfuscate.charAt(i)); } } JsonUtils.replaceValue(attributePath, resource, TextNode.valueOf(obfuscated.toString())); } catch (ScimException e) { throw new ServerErrorException(e.getMessage(), Response.Status.INTERNAL_SERVER_ERROR); } } } requestResponse.setBody(resource); return requestResponse; } }