-
Notifications
You must be signed in to change notification settings - Fork 75
Making SCIM requests
Requests to a SCIM service provider are made through a ScimService
instance. The ScimService
class wraps a JAX-RS Client
instance, providing methods for building and making SCIM requests.
Responses are deserialized to instances of GenericScimResource
or instances derived from BaseScimResource
. See working with SCIM resources for more information.
A new ScimService
instance requires a JAX-RS WebTarget
that specifies the base URI of a SCIM service provider. A WebTarget
instance is obtained through a JAX-RS Client
instance.
Client restClient = ClientBuilder.newClient();
WebTarget target =
restClient.target(new URI("https://example.com/scim/v2"));
ScimService scimService = new ScimService(target);
Note that the details of creating a JAX-RS Client
will vary depending on your needs. See JAX-RS client examples for examples of JAX-RS Client
configuration.
ScimService
supports making all SCIM request types (resource retrieval, searches, resource creation, resource replacement, resource modification, and resource deletion), and it provides a few different ways to do each. These are described below.
SCIM service providers advertise provider-specific metadata at three special endpoints; ScimService
provides accessor methods for these.
Endpoint | Description | Accessor method(s) |
---|---|---|
/ServiceProviderConfig |
Describes the service provider configuration, including whether or not optional aspects of the SCIM standard are supported. | ScimService.getServiceProviderConfig() |
/Schemas |
Describes the schemas supported by the service provider. Schemas are named sets of attribute descriptors. |
ScimService.getSchemas() , ScimService.getSchema(String)
|
/ResourceTypes |
Describes the resource types supported by the service provider. Resource types represent entities such as users or groups and are composed of one or more schemas. |
ScimService.getSchemas() , ScimService.getSchema(String)
|
The following example illustrates calling the /ServiceProviderConfig
endpoint to determine if the service provider supports partial modifications using PATCH:
ServiceProviderConfigResource providerConfig =
scimService.getServiceProviderConfig();
if (providerConfig.getPatch().isSupported()) {
// Etc.
}
SCIM resources such as users can be identified by either a URI or the combination of a SCIM resource type endpoint and a unique resource identifier.
A URI could be a SCIM resource's complete absolute URI, such as "https://example.com/scim/v2/Users/2819c223-7f76-453a-919d-413861904646", or it could be relative to the base URI that was specified when creating the ScimService
, such as "Users/2819c223-7f76-453a-919d-413861904646".
If specifying a SCIM resource by ID, then the resource type endpoint and the resource ID are needed. Using the examples above, the resource type endpoint is "Users", and the resource ID is "2819c223-7f76-453a-919d-413861904646".
The ScimService
class provides a ME_URI
constant, which can be used in any request method that takes a URI. When used, this specifies the currently authenticated user. This is useful when you are authenticated on behalf of a logged-in user, but you don't necessarily know the user's resource type or unique ID in advance.
All request methods return a class that implements the ScimResource
interface. This can either be an instance of GenericScimResource
or a concrete implementation of the BaseScimResource
class.
GenericScimResource
is a general-purpose interface for interacting with SCIM resources that exposes attributes as instances of Jackson JsonNode
. Use this if you are comfortable with the Jackson API and cannot or do not wish to model your SCIM resource types as POJOs.
BaseScimResource
is an abstract class that serves as a basis for creating POJOs that model SCIM resource types. This is useful if you know details in advance about the SCIM resource types that you'll be working with. The SCIM 2 SDK provides such POJOs for the resource types defined in RFC 7643, the SCIM Core Schema spec. One such example is UserResource
, which you can use as a model for creating your own POJOs.
This topic is discussed in greater detail in the article working with SCIM resources.
Resources are retrieved using the ScimService.retrieve(…)
methods or the ScimService.retrieveRequest(…)
method. The patterns shown here generally apply to the other request methods.
To retrieve a resource by URI:
URI uri = new URI("Users/2819c223-7f76-453a-919d-413861904646");
GenericScimResource scimUser =
scimService.retrieve(uri, GenericScimResource.class);
// Or, using the Me endpoint:
GenericScimResource scimUser =
scimService.retrieve(ScimService.ME_URI, GenericScimResource.class);
To retrieve a resource by resource type and unique ID:
GenericScimResource scimUser =
scimService.retrieve("Users", "2819c223-7f76-453a-919d-413861904646",
GenericScimResource.class);
You can also call the retrieveRequest(…)
method to obtain a ResourceRequestBuilder
, which allows you to modify the request using a fluent API. The invoke(…)
method is called to build and submit the request. For example:
GenericScimResource scimUser =
scimService.retrieveRequest("Users", "2819c223-7f76-453a-919d-413861904646")
.header("X-Custom-Header", "custom-header-value")
.invoke(GenericScimResource.class);
This is a pattern in the ScimService
class. For each request type, at least one method is provided that returns a RequestBuilder
with the same fluent API.
To create a SCIM resource, first create an instance of GenericScimResource
or BaseScimResource
, and provide it to one of the create(…)
methods, along with the desired resource type endpoint.
The following example uses the UserResource
class, which is a concrete implementation of BaseScimResource
.
UserResource userResource = new UserResource();
userResource.setUserName("user.1");
userResource.setPassword("secret");
Name name = new Name()
.setGivenName("Joe")
.setFamilyName("Public");
userResource.setName(name);
Email email = new Email()
.setType("home")
.setPrimary(true)
.setValue("joe.public@example.com");
userResource.setEmails(Collections.singletonList(email));
userResource = scimService.create("Users", userResource);
The replace(…)
method updates a SCIM resource by replacing all of its attributes.
The following example retrieves a resource, performs an in-memory modification, and then replaces the existing resource with the modified copy.
UserResource userResource =
scimService.retrieve("Users", "2819c223-7f76-453a-919d-413861904646",
UserResource.class);
Email newEmail = userResource.getEmails().get(0);
newEmail.setValue("joe@example.com");
userResource.setEmails(Collections.singletonList(newEmail));
userResource = scimService.replace(userResource);
Note that a URI or unique ID did not have to be specified when performing the replace, because that information is already available in the UserResource
object.
If the SCIM service provider supports partial modifications using PATCH, then you can use the modifyRequest(…)
methods. These provide a powerful and expressive means of making specific changes to a SCIM resource.
Fundamentally, a PATCH request consists of one or more operations (add, replace, or remove), a path specifying the target attribute, and the value to set. Paths can be as simple as attribute names or can include filters that select one or more attributes. Values can be expressed as a Jackson JsonNode
or as one of several basic types, such as strings, integers, booleans, dates, or URIs.
The following examples of using PATCH are not exhaustive.
To replace a single-valued attribute:
UserResource userResource =
scimService.modifyRequest("Users", "2819c223-7f76-453a-919d-413861904646")
.replaceValue("displayName", "Joe Public")
.invoke(UserResource.class);
// Or:
UserResource userResource =
scimService.modifyRequest("Users", "2819c223-7f76-453a-919d-413861904646")
.addOperation(PatchOperation.replace("displayName", "Joe Public"))
.invoke(UserResource.class);
// Or:
UserResource userResource =
scimService.modifyRequest("Users", "2819c223-7f76-453a-919d-413861904646")
.addOperation(PatchOperation.replace("displayName",
JsonUtils.valueToNode("Joe Public")))
.invoke(UserResource.class);
// Or:
UserResource userResource =
scimService.modifyRequest("Users", "2819c223-7f76-453a-919d-413861904646")
.addOperation(PatchOperation.replace("displayName",
TextNode.valueOf("Joe Public")))
.invoke(UserResource.class);
To replace a single-valued complex attribute:
Name name = new Name()
.setGivenName("Joseph")
.setFamilyName("Public");
scimService.modifyRequest("Users", "2819c223-7f76-453a-919d-413861904646")
.replaceValue("name", name)
.invoke(UserResource.class);
// Or:
scimService.modifyRequest("Users", "2819c223-7f76-453a-919d-413861904646")
.addOperation(PatchOperation.replace(Path.fromString("name"),
JsonUtils.valueToNode(name)))
.invoke(UserResource.class);
To replace a sub-attribute:
scimService.modifyRequest("Users", "2819c223-7f76-453a-919d-413861904646")
.replaceValue("name.givenName", "Joe")
.invoke(UserResource.class);
To add members to a multivalued attribute:
Email email1 = new Email()
.setType("work")
.setPrimary(false)
.setValue("joe.public@work.com");
Email email2 = new Email()
.setType("other")
.setPrimary(false)
.setValue("joe.public@gmail.com");
scimService.modifyRequest("Users", "2819c223-7f76-453a-919d-413861904646")
.addValues("emails", email1, email2)
.invoke(UserResource.class);
// Or:
List<Email> emails =
new ArrayList<Email>(Arrays.asList(email1, email2));
scimService.modifyRequest("Users", "2819c223-7f76-453a-919d-413861904646")
.addOperation(PatchOperation.add(Path.fromString("emails"),
JsonUtils.valueToNode(emails)))
.invoke(UserResource.class);
To remove an attribute:
// Single-valued attribute
scimService.modifyRequest("Users", "2819c223-7f76-453a-919d-413861904646")
.removeValues("displayName")
.invoke(UserResource.class);
// Multivalued attribute; removes all members
scimService.modifyRequest("Users", "2819c223-7f76-453a-919d-413861904646")
.removeValues("emails")
.invoke(UserResource.class);
Multiple operations may be provided in a single PATCH request:
scimService.modifyRequest("Users", "2819c223-7f76-453a-919d-413861904646")
.replaceValue("displayName", "Joe Public")
.replaceValue("active", false)
.invoke(UserResource.class);
A path for a multivalued attribute can even include a filter, which will cause the modification to be applied to the matching member attribute(s):
Email newEmail = new Email()
.setType("other")
.setPrimary(false)
.setValue("joe.public@hotmail.com");
Filter filter =
Filter.hasComplexValue("emails",
Filter.eq("value", "joe.public@hotmail.com"));
// If path doesn't match, a BadRequestException will be thrown
scimService.modifyRequest("Users", "2819c223-7f76-453a-919d-413861904646")
.replaceValue(filter.toString(), newEmail)
.invoke(UserResource.class);
Deleting a resource is straightforward. A delete method returns no value.
scimService.delete("Users", "2819c223-7f76-453a-919d-413861904646");
// If you have a resource instance
UserResource userResource =
scimService.retrieve("Users", "2819c223-7f76-453a-919d-413861904646",
UserResource.class);
scimService.delete(userResource);
If you need to customize the request, you can use the deleteRequest(…)
method to get a RequestBuilder
, then use its fluent API to adjust the request before calling invoke()
.
scimService.deleteRequest("Users", "2819c223-7f76-453a-919d-413861904646")
.header("X-Custom-Header", "custom-header-value")
.invoke();
Searches are performed by calling searchRequest(…)
to get a SearchRequestBuilder
, chaining search directive methods, and finally calling invoke(…)
, which will return a ListResponse
containing the type provided to invoke(…)
.
A ListResponse
provides methods for getting the total number of results and paging information such as the number of resources per page and the start index. A ListResponse
is iterable.
Filter filter = Filter.eq("lastName", "Jensen");
ListResponse<UserResource> searchResponse =
scimService.searchRequest("Users")
.filter(filter.toString())
.invoke(UserResource);
for (UserResource userResource : searchResponse)
{
// Etc.
}
You can either specify a search filter as a string or build it using the Filter
class, which is based on the operators defined by RFC 7644.
To check that an attribute value equals an expected value:
Filter.eq("userName", "user.1")
To check for the presence of an attribute value:
Filter.pr("userName")
To check that an attribute value starts with an expected value:
Filter.sw("name.givenName", "Barb")
To check that an attribute value ends with an expected value:
Filter.ew("name.familyName", "Jen")
To check that an attribute value is greater than or equal to a specific value:
Filter.gte("age", 18)
To negate a search condition:
Filter.not(Filter.gt("age", 18))
To search using AND:
Filter.and(
Filter.eq("userName", "user.1"),
Filter.eq("name.familyName", "Jensen")
)
To search based on matching sub-attributes of a multivalued attribute:
// Same as: addresses[(type eq "home" and country eq "US")]
Filter.hasComplexValue(
"addresses",
Filter.and(
Filter.eq("type", "home"),
Filter.eq("country", "US")
)
)
A search filter may match many SCIM resources, so a SCIM service provider may limit the number of resources returned per response, requiring the client to page through results.
The client pages through search results by calling the page(int, int)
method, providing the 1-based startIndex of the first query result and the number of resources to return in each response.
Filter filter = Filter.eq("lastName", "Jensen");
ListResponse<UserResource> searchResponse =
scimService.searchRequest("Users")
.filter(filter.toString())
// Start at index 1 and return 10 results
.page(1, 10)
.invoke(UserResource);
// The search response will include up to 10 resources.
for (UserResource userResource : searchResponse)
{
// Etc.
}
Not all clients may need or want to retrieve the full contents of a SCIM resource. For example, a help desk application may need to view a complete representation of a user, but an online ordering application may only need the user's name, address, phone number, and email address. This could be handled at the authorization level — for example, by using OAuth 2 scopes to constrain the data available to an application in the first place — but a client may also specify which attributes to retrieve or which attributes to not retrieve.
This is done through the SCIM 2 SDK by calling a request method that returns a RequestBuilder
with the attributes(String…)
method or the excludeAttributes(String…)
method.
For example, the following would retrieve a user with only the name and emails attributes:
scimService.retrieveRequest("Users", "2819c223-7f76-453a-919d-413861904646")
.attributes("name", "emails")
.invoke(GenericScimResource.class);
And the following would a retrieve a user, returning all available attributes except the name and emails attributes:
scimService.retrieveRequest("Users", "2819c223-7f76-453a-919d-413861904646")
.excludedAttributes("name", "emails")
.invoke(GenericScimResource.class);
Note that some attributes are always returned. This is determined by the schema and therefore varies, but you should at least expect the id attribute to always be present.
In the event of a server-side error, the SCIM 2 SDK will throw a ScimServiceException
. This contains an ErrorResponse
object with the HTTP status code and any available error messages for the failed request. Applications should be prepared to catch and log or present this information, if necessary.
Processing problems with the underlying JAX-RS client or lower-level networking errors such as connection timeouts will result in a javax.ws.rs.ProcessingException
.