UnboundID Server SDK

Ping Identity
UnboundID Server SDK Documentation

ExampleAdvice.java

/*
 * 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;
  }
}