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