UnboundID Server SDK

Ping Identity
UnboundID Server SDK Documentation


 * 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]
 *      Portions Copyright 2017-2023 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
  public String getExtensionName() {
    return "Example Obfuscation Advice";

   * Retrieves a human-readable description of this extension.
   * @return text description string
  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.
  public void defineConfigArguments(final ArgumentParser parser)
      throws ArgumentException {

    // obfuscate character is optional, default is 'X'
    final Argument obfuscateCharArg = new StringArgument(
        "The character to use for obfuscation",

    // obfuscation count is optional, default is 4
    final Argument obfuscateCountArg = new IntegerArgument(
        "The number of characters to replace with the obfuscation character",


   * 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.
  public void initializeAdvice(
      final BrokerContext serverContext,
      final AdviceConfig config,
      final ArgumentParser parser) throws Exception {

    this.serverContext = serverContext;

    final StringArgument obfuscateCharArg =
    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 =
    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.
  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 " +

    // 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() + "'",

    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(),
      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()),
      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,

          // if the attribute doesn't exist, there's nothing to do
          if (attributeNode == null || attributeNode.isNull()) {

          // 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) {
            else {
          JsonUtils.replaceValue(attributePath, resource,
        catch (ScimException e) {
          throw new ServerErrorException(e.getMessage(),

    return requestResponse;