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