/*
* 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
*
*
* Copyright 2017-2021 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:
*
* - obfuscateChar -- The character to use to overwrite a portion of the
* obfuscated attribute. If not specified defaults to "X".
*
* - count -- The number of characters to overwrite in the obfuscated
* attribute, starting with the first character.
*
*
*
*
* 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.
*
*
* 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
*
*
*
*/
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 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;
}
}