UnboundID Server SDK

Ping Identity
UnboundID Server SDK Documentation

ExampleOneToOneSCIMSubResourceTypeHandler.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 2021-2024 Ping Identity Corporation
 */
package com.unboundid.directory.sdk.examples;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.unboundid.directory.sdk.common.types.LogSeverity;
import com.unboundid.directory.sdk.scim2.api.SCIMSubResourceTypeHandler;
import com.unboundid.directory.sdk.scim2.config.SCIMSubResourceTypeHandlerConfig;
import com.unboundid.directory.sdk.scim2.types.SCIMReplaceRequest;
import com.unboundid.directory.sdk.scim2.types.SCIMRequest;
import com.unboundid.directory.sdk.scim2.types.SCIMLDAPInterface;
import com.unboundid.directory.sdk.scim2.types.SCIMRetrieveRequest;
import com.unboundid.directory.sdk.scim2.types.SCIMServerContext;
import com.unboundid.ldap.sdk.Control;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.ExtendedResult;
import com.unboundid.ldap.sdk.Filter;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.ResultCode;
import com.unboundid.ldap.sdk.SearchRequest;
import com.unboundid.ldap.sdk.SearchScope;
import com.unboundid.ldap.sdk.unboundidds.extensions.PasswordPolicyStateExtendedRequest;
import com.unboundid.ldap.sdk.unboundidds.extensions.PasswordPolicyStateExtendedResult;
import com.unboundid.ldap.sdk.unboundidds.extensions.PasswordPolicyStateOperation;
import com.unboundid.scim2.common.BaseScimResource;
import com.unboundid.scim2.common.ScimResource;
import com.unboundid.scim2.common.annotations.Attribute;
import com.unboundid.scim2.common.annotations.Schema;
import com.unboundid.scim2.common.exceptions.ScimException;
import com.unboundid.scim2.common.exceptions.ServerErrorException;
import com.unboundid.scim2.common.types.AttributeDefinition;
import com.unboundid.scim2.common.types.SchemaResource;
import com.unboundid.scim2.common.utils.JsonUtils;
import com.unboundid.util.StaticUtils;
import com.unboundid.util.args.ArgumentParser;

import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.List;

/**
 * This is an example SCIM Sub-Resource Type Handler that demonstrates how to
 * implement a <i>one-to-one</i> SCIM sub-resource type, which is a SCIM
 * sub-resource type that provides exactly one sub-resource per parent SCIM
 * resource.
 * <p>
 * This sub-resource type provides a simplified API for a user's account state,
 * allowing clients to read and update the user's "disabled" account state flag.
 */
public class ExampleOneToOneSCIMSubResourceTypeHandler
    extends SCIMSubResourceTypeHandler
{
  /**
   * The schema URN of this sub-resource type's core schema.
   */
  public static final String SCIM_SCHEMA_ID =
      "urn:pingidentity:schemas:2.0:AccountStateExample";

  /**
   * The name of this sub-resource type's core schema.
   */
  public static final String SCIM_SCHEMA_NAME =
      "PingDirectory account state";

  /**
   * A description of this sub-resource type's core schema.
   */
  public static final String SCIM_SCHEMA_DESCRIPTION =
      "Example account state sub-resource schema";

  /**
   * The "disabled" SCIM attribute name.
   */
  public static final String SCIM_ATTR_DISABLED_NAME =
      "disabled";

  /**
   * The "disabled" SCIM attribute description.
   */
  public static final String SCIM_ATTR_DISABLED_DESCRIPTION =
      "Account disabled";

  private SCIMServerContext serverContext;

  private SchemaResource coreSchema;

  private String parentResourceType;

  private SCIMLDAPInterface ldapInterface;

  /**
   * Retrieves a human-readable name for this extension.
   *
   * @return  A human-readable name for this extension.
   */
  @Override
  public String getExtensionName()
  {
    return "Example One-To-One SCIM Sub-Resource Type Handler";
  }

  /**
   * Retrieves a human-readable description for this extension.  Each element
   * of the array that is returned will be considered a separate paragraph in
   * generated documentation.
   *
   * @return  A human-readable description for this extension, or {@code null}
   *          or an empty array if no description should be available.
   */
  @Override
  public String[] getExtensionDescription()
  {
    return new String[] {
        "This is an example SCIM Sub-Resource Type Handler that exposes a " +
            "single sub-resource with an abridged view of a PingDirectory " +
            "user's account state."
    };
  }

  /**
   * Initializes this SCIM Sub-Resource Type Handler, setting up helper
   * objects, creating its core SCIM schema dynamically and registering it with
   * the server.
   *
   * @param serverContext  A handle to the server context for the server in
   *                       which this extension is running.
   * @param config         The configuration for this SCIM Sub-Resource Type
   *                       Handler.
   * @param parser         The argument parser which has been initialized from
   *                       the configuration for this SCIM Sub-Resource Type
   *                       Handler.
   * @throws Exception     If a problem occurs while initializing this SCIM
   *                       Sub-Resource Type Handler.
   */
  @Override
  public void initializeHandler(final SCIMServerContext serverContext,
                                final SCIMSubResourceTypeHandlerConfig config,
                                final ArgumentParser parser) throws Exception
  {
    this.serverContext = serverContext;
    this.serverContext.logTraceMessage(LogSeverity.NOTICE,
        "Initializing SCIM Sub-Resource Type Handler");

    coreSchema = createCoreSchema();
    this.serverContext.registerSCIMSchema(coreSchema);

    parentResourceType = config.getParentSCIMResourceTypeName();

    try
    {
      ldapInterface = serverContext.getSCIMLDAPInterface(parentResourceType);
    }
    catch (ScimException e)
    {
      String error = String.format("Initialization error: %s; Stack trace: %s",
          e.getMessage(), StaticUtils.getStackTrace(e));
      serverContext.logTraceMessage(LogSeverity.DEBUG, error);
      throw new LDAPException(ResultCode.LOCAL_ERROR, e);
    }
  }

  /**
   * Performs cleanup that is needed when this SCIM Sub-Resource Type Handler
   * is taken out of service.
   */
  @Override
  public void finalizeHandler()
  {
    serverContext.logTraceMessage(LogSeverity.NOTICE,
        "Finalizing SCIM Sub-Resource Type Handler");
    serverContext.deregisterSCIMSchema(coreSchema);
  }

  /**
   * Gets the SCIM Sub-Resource Type's core schema.
   *
   * @return The core schema for the SCIM Sub-Resource Type.
   */
  public SchemaResource getCoreSchema()
  {
    return coreSchema;
  }

  /**
   * Indicates that this SCIM Sub-Resource Type Handler supports a single
   * sub-resource per parent resource.
   *
   * @return Always returns false to indicate that this SCIM Sub-Resource Type
   *         Handler supports only a single sub-resource per parent resource.
   */
  @Override
  public boolean supportsOneToMany()
  {
    // Only one account state sub-resource is exposed per parent SCIM resource.
    return false;
  }

  /**
   * Handles SCIM retrieve requests. This reads the account state.
   *
   * @param request         A SCIM retrieve request.
   * @return                The retrieved sub-resource.
   * @throws ScimException  If an error occurs.
   */
  @Override
  public ScimResource retrieve(final SCIMRetrieveRequest request)
      throws ScimException
  {
    PasswordPolicyStateExtendedResult accountStateResult =
        getAccountState(request);
    return ExampleAccountStateResource.create(accountStateResult);
  }

  /**
   * Handles SCIM replace requests. This updates the account state.
   *
   * @param request         A SCIM replace request.
   * @return                The replaced sub-resource.
   * @throws ScimException  If an error occurs.
   */
  @Override
  public ScimResource replace(final SCIMReplaceRequest request)
      throws ScimException
  {
    PasswordPolicyStateExtendedResult accountStateResult =
        updateAccountState(request);
    return ExampleAccountStateResource.create(accountStateResult);
  }

  /**
   * Creates an object representing the SCIM sub-resource type's core SCIM
   * schema.
   *
   * @return  The account state sub-resource type's core SCIM schema.
   */
  private SchemaResource createCoreSchema()
  {
    List<AttributeDefinition> attributes = new ArrayList<>();

    AttributeDefinition disabledAttribute =
        new AttributeDefinition.Builder()
            .setName(SCIM_ATTR_DISABLED_NAME)
            .setDescription(SCIM_ATTR_DISABLED_DESCRIPTION)
            .setRequired(true)
            .setType(AttributeDefinition.Type.BOOLEAN)
            .build();
    attributes.add(disabledAttribute);

    return new SchemaResource(SCIM_SCHEMA_ID, SCIM_SCHEMA_NAME,
        SCIM_SCHEMA_DESCRIPTION, attributes);
  }

  /**
   * Retrieves a user's account state via LDAP.
   *
   * @param request         The associated SCIM GET request. This is used to
   *                        obtain the DN associated with the parent SCIM
   *                        sub-resource.
   * @return                The LDAP account state result.
   * @throws ScimException  If an error of any kind occurs while processing
   *                        the account state request.
   */
  private PasswordPolicyStateExtendedResult getAccountState(
      final SCIMRequest request) throws ScimException
  {
    String entryDN =
        getParentEntryDN(request.getHttpServletRequest(),
            request.getResourceId());
    Control[] controls = new Control[0];
    PasswordPolicyStateExtendedRequest accountStateRequest =
        new PasswordPolicyStateExtendedRequest(entryDN, controls);

    return submitLDAPAccountStateRequest(
        request.getHttpServletRequest(), accountStateRequest);
  }

  /**
   * Updates a user's account state via LDAP.
   *
   * @param request         The associated SCIM PUT request. This is used to
   *                        obtain the DN associated with the parent SCIM
   *                        sub-resource.
   * @return                The LDAP account state update result.
   * @throws ScimException  If an error of any kind occurs while processing
   *                        the account state update request.
   */
  private PasswordPolicyStateExtendedResult updateAccountState(
      final SCIMReplaceRequest request) throws ScimException
  {
    ExampleAccountStateResource accountStateUpdate;
    try
    {
      accountStateUpdate = JsonUtils.nodeToValue(
          request.getRequestResource().asGenericScimResource().getObjectNode(),
          ExampleAccountStateResource.class);
    }
    catch (JsonProcessingException e)
    {
      String error = String.format("Unable to parse account state update " +
              "request: %s; Stack trace: %s",
          e.getMessage(), StaticUtils.getStackTrace(e));
      serverContext.logTraceMessage(LogSeverity.DEBUG, error);
      throw new ServerErrorException(
          "Unable to parse account state update request", null, e);
    }

    String entryDN =
        getParentEntryDN(request.getHttpServletRequest(),
            request.getResourceId());

    Control[] controls = new Control[0];

    PasswordPolicyStateOperation disableOp =
        PasswordPolicyStateOperation.createSetAccountDisabledStateOperation(
            accountStateUpdate.isDisabled());

    PasswordPolicyStateExtendedRequest accountStateRequest =
        new PasswordPolicyStateExtendedRequest(entryDN, controls, disableOp);

    return submitLDAPAccountStateRequest(
        request.getHttpServletRequest(), accountStateRequest);
  }

  /**
   * Submits an account state request to the LDAP backend.
   *
   * @param httpServletRequest   The associated HTTP servlet request.
   * @param accountStateRequest  The account state request.
   * @return                     The LDAP result object.
   * @throws ScimException       If the LDAP result is not successful or if a
   *                             processing error of any other kind occurs.
   */
  private PasswordPolicyStateExtendedResult submitLDAPAccountStateRequest(
      final HttpServletRequest httpServletRequest,
      final PasswordPolicyStateExtendedRequest accountStateRequest)
      throws ScimException
  {
    ExtendedResult ldapResult =
        ldapInterface.processExtendedRequest(
            httpServletRequest, accountStateRequest);

    if (ldapResult.getResultCode() != ResultCode.SUCCESS)
    {
      String error = String.format(
          "PasswordPolicyStateExtendedRequest failed with " +
              "LDAP result code %s", ldapResult.getResultCode().toString());
      serverContext.logTraceMessage(LogSeverity.SEVERE_ERROR, error);
      throw new ServerErrorException(error);
    }

    if (ldapResult instanceof PasswordPolicyStateExtendedResult)
    {
      return (PasswordPolicyStateExtendedResult) ldapResult;
    }

    String error = String.format("The LDAP server responded to a " +
        "PasswordPolicyStateExtendedRequest with an unexpected result type " +
        "(OID: %s)", ldapResult.getOID());
    serverContext.logTraceMessage(LogSeverity.SEVERE_ERROR, error);
    throw new ServerErrorException(error);
  }

  /**
   * Given the ID of a SCIM resource, returns the associated LDAP entry DN.
   * <p>
   * This extension uses this method to obtain the parent SCIM resource's DN,
   * which is needed to perform LDAP account state requests.
   *
   * @param httpServletRequest  The associated HTTP servlet request.
   * @param scimResourceID      The SCIM resource ID.
   * @return                    The entry DN associated with the parent SCIM
   *                            resource.
   * @throws ScimException      If the LDAP search fails, including if an LDAP
   *                            entry associated with the parent SCIM resource
   *                            cannot be found.
   */
  private String getParentEntryDN(final HttpServletRequest httpServletRequest,
                                  final String scimResourceID)
      throws ScimException
  {
    Filter filter = Filter.createEqualityFilter(
        serverContext.getIDAttribute(parentResourceType), scimResourceID);
    SearchRequest searchRequest =
        new SearchRequest(ldapInterface.getBaseDN(), SearchScope.SUB,
            filter, "1.1");
    Entry entry =
        ldapInterface.searchForEntry(httpServletRequest, searchRequest);
    return entry.getDN();
  }


  /**
   * A POJO representing an account state SCIM sub-resource handled by this
   * extension.
   */
  @Schema(id=SCIM_SCHEMA_ID,
      name=SCIM_SCHEMA_NAME,
      description = SCIM_SCHEMA_DESCRIPTION)
  private static class ExampleAccountStateResource extends BaseScimResource
  {
    @Attribute(description = SCIM_ATTR_DISABLED_DESCRIPTION,
        isRequired = true,
        mutability = AttributeDefinition.Mutability.READ_WRITE,
        returned = AttributeDefinition.Returned.DEFAULT)
    private boolean disabled = false;

    /**
     * Creates an ExampleAccountStateResource from the provided
     * {@link PasswordPolicyStateExtendedResult} object.
     *
     * @param accountStateResult  The result of an LDAP
     *                            {@link PasswordPolicyStateExtendedRequest}.
     * @return                    A new ExampleAccountStateResource.
     */
    public static ExampleAccountStateResource create(
        final PasswordPolicyStateExtendedResult accountStateResult)
    {
      return new ExampleAccountStateResource()
          .setDisabled(accountStateResult.getBooleanValue(
              PasswordPolicyStateOperation.OP_TYPE_GET_ACCOUNT_DISABLED_STATE));
    }

    /**
     * Whether or not the account is disabled.
     *
     * @return  True if the account is disabled or false otherwise.
     */
    public boolean isDisabled()
    {
      return disabled;
    }

    /**
     * Sets the disabled flag.
     *
     * @param disabled  True if the account is disabled or false otherwise.
     * @return          This ExampleAccountStateResource.
     */
    public ExampleAccountStateResource setDisabled(final boolean disabled)
    {
      this.disabled = disabled;
      return this;
    }
  }
}