UnboundID Server SDK

Ping Identity
UnboundID Server SDK Documentation

ExamplePolicyObligation.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
 *
 *
 *      Copyright 2017-2019 Ping Identity Corporation
 */


package com.unboundid.directory.sdk.examples;

import com.fasterxml.jackson.databind.JsonNode;
import com.unboundid.directory.sdk.broker.api.PolicyObligation;
import com.unboundid.directory.sdk.broker.config.PolicyObligationConfig;
import com.unboundid.directory.sdk.broker.types.BrokerContext;
import com.unboundid.directory.sdk.broker.types.NotFulfilledException;
import com.unboundid.directory.sdk.broker.types.ObligationContext;
import com.unboundid.scim2.common.GenericScimResource;
import com.unboundid.scim2.common.exceptions.ScimException;
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 java.util.List;


/**
 * Example of a third-party Policy Obligation.  This example can be applied
 * to any policy request with an action of "retrieve", which will include
 * SCIM GET operations as well as resources returned by SCIM POST, PUT, or
 * PATCH.  The example implements a simple obfuscation scheme for one or more
 * attributes of the SCIM response, using a configurable obfuscation method.
 *
 * This example demonstrates the use of both "obligation arguments" and
 * extension arguments.  Obligation arguments are defined using a JEXL
 * expression that is computed during the policy evaluation associated with
 * each SCIM request.  The JEXL expression may reference values obtained from
 * the policy request context for each specific SCIM request.  This example
 * takes a single obligation argument:
 * <UL>
 *   <LI>attributePaths -- A JEXL expression that returns an array of JSON
 *        paths, each of which selects a value from the SCIM resource that
 *        is to be obfuscated.
 *   </LI>
 * </UL>
 *
 * 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 example dsconfig command creates an instance of this
 * obligation in a hypothetical example policy and rule.  It will cause a
 * user's last name to be obfuscated by replacing the first three characters
 * with the letter 'Z':
 * <p>
 *  <code>
 *   dsconfig create-policy-obligation --obligation-name "Example Obligation"
 *   --type third-party
 *   --policy-name "Example Policy"
 *   --rule-name "Example Rule"
 *   --set extension-class:\
 *         com.unboundid.directory.sdk.examples.ExamplePolicyObligation
 *   --set "obligation-arguments:attributePaths=['name.familyName']"
 *   --set extension-argument:obfuscateChar=Z
 *   --set extension-argument:count=3
 *  </code>
 * </p>
 */
public class ExamplePolicyObligation extends PolicyObligation
{

  /**
   * The name of the obligation argument that will be used to specify a
   * list of paths to the attributes and/or sub-attributes to be
   * obfuscated.
   */
  private static final String ARG_ATTRIBUTE_PATHS = "attributePaths";

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


  /**
   * 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 policy
   * obligation implementations must include a default constructor,
   * but any initialization should generally be performed in the
   * {@code initializePolicyObligation} method.
   */
  public ExamplePolicyObligation()
  {
  }


  /**
   * Retrieves a human-readable name for this extension.
   * @return extension name
   */
  @Override
  public String getExtensionName() {
    return "Example Policy Obligation";
  }


  /**
   * Retrieves a human-readable description of this extension.
   * @return text description string
   */
  @Override
  public String[] getExtensionDescription() {
    return new String[]
    {
        "This Policy Obligation implementation serves as an example that may" +
            " be used to demonstrate the process for creating a third-party" +
            " Policy Obligation.  It implements a simple obfuscation of" +
            " a resource attribute.  It takes two arguments at start-up:" +
            " 'obfuscate-char' contains the character to use for obfuscation,"+
            " and 'count' contains the number of characters to replace with" +
            " the obfuscation-char (starting from the first character)." +
            " The attributes to be obfuscated are computed based on a JEXL" +
            " expression specified when an instance of this obligation is" +
            " created within a policy rule."
    };
  }


  /**
   * Updates the provided argument parser to define any configuration arguments
   * which may be used by this policy obligation. 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 this policy obligation. 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 initializePolicyObligation(
      final BrokerContext serverContext,
      final PolicyObligationConfig 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 before a SCIM resource is returned to the calling
   * client application.
   * @param context   the ObligationContext containing any arguments passed
   *                  from policy.  This example expects an argument with
   *                  name "attributePaths".
   * @param resource  the SCIM resource.
   * @return          the obfuscated resource
   * @throws NotFulfilledException if the obligation could not be fulfilled.
   *                  Throwing this exception will result in an unauthorized
   *                  response (401) to the calling client.
   * @throws ScimException if an internal error occurs trying to fulfill the
   *                  obligation.  Throwing this exception will result in a
   *                  server exception response (500) to the calling client.
   */
  @Override
  public GenericScimResource onScimRetrieve(
      final ObligationContext context,
      final GenericScimResource resource)
      throws NotFulfilledException, ScimException {

    List<String> attributePaths = context.getArgumentValue(ARG_ATTRIBUTE_PATHS);
    for (String attributePath : attributePaths) {
      JsonNode attributeNode = resource.getValue(attributePath);

      // 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 +
            " 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));
        }
      }

      resource.replaceValue(attributePath, obfuscated.toString());
    }
    return resource;
  }
}