Skip to content

How to use the client implementation

Pascal Knüppel edited this page Nov 2, 2023 · 5 revisions

Using the client implementation is pretty simple. Just perform the following steps

  1. create a http client configuration with the "ScimClientConfig" object
  2. create an instance of "ScimRequestBuilder"

in the following I will provide some exmaple on how to use the request builder:

ScimClientConfig scimClientConfig = ScimClientConfig.builder()
                                                        .connectTimeout(5)
                                                        .requestTimeout(5)
                                                        .socketTimeout(5)
                                                        .clientAuth(getClientAuthKeystore())
                                                        .truststore(getTruststore())
                                                        // hostname verifier disabled for tests
                                                        .hostnameVerifier((s, sslSession) -> true)
                                                        .build();
final String scimApplicationBaseUrl = "https://localhost:8443/realm/master/scim/v2";
ScimRequestBuilder scimRequestBuilder = new ScimRequestBuilder(scimApplicationBaseUrl, scimClientConfig);

Like this you can create a request builder that should be able to handle each resource from the server.


NOTE

The "ScimRequestBuilder" is an AutoCloseable. But you do not need to create a new instance after calling the close() method. The close()-method will simply close the current underlying apache-http-client instance and a new instance is provided as soon as you try to send a new request.

This is done to preserve Cookie-Authentication for as long as you need it. But you should close() the instance to make sure that you do not get any memory leaks.


Send a create request

User user = User.builder().userName("goldfish").build();
String endpointPath = EndpointPaths.USERS; // holds the value "/Users"
ServerResponse<User> response = scimRequestBuilder.create(User.class, endpointPath)
                                                  .setResource(user)
                                                  .sendRequest();
if (response.isSuccess()) 
{
  User createdUser = response.getResource();
  // do something with it
}
else if(response.getErrorResponse() == null)
{
  // the response was not an error response as described in RFC7644
  String errorMessage = response.getResponseBody();
}
else
{
  ErrorResponse errorResponse = response.getErrorResponse();
  // do something with it
}

Send a get request

String id = getIdOfExistingUser();
String endpointPath = EndpointPaths.USERS; // holds the value "/Users"
ServerResponse<User> response = scimRequestBuilder.get(User.class, endpointPath, id).sendRequest();
if (response.isSuccess()) 
{
  User returnedUser = response.getResource();
  // do something with it
}
else if(response.getErrorResponse() == null)
{
  // the response was not an error response as described in RFC7644
  String errorMessage = response.getResponseBody();
}
else
{
  ErrorResponse errorResponse = response.getErrorResponse();
  // do something with it
}

Send a list request

String endpointPath = EndpointPaths.USERS; // holds the value "/Users"
ServerResponse<ListResponse<User>> response = scimRequestBuilder.list(User.class, endpointPath)
                                                                  .count(50)
                                                                  .filter("username", Comparator.CO, "ai")
                                                                     .and("locale", Comparator.EQ, "EN")
                                                                  .build()
                                                                  .sortBy("username")
                                                                  .sortOrder(SortOrder.DESCENDING)
                                                                .post() // http method to use (get or post)
                                                                .sendRequest();
if (response.isSuccess()) 
{
  ListResponse<User> returnedUserList = response.getResource();
  // do something with it
}
else if(response.getErrorResponse() == null)
{
  // the response was not an error response as described in RFC7644
  String errorMessage = response.getResponseBody();
}
else
{
  ErrorResponse errorResponse = response.getErrorResponse();
  // do something with it
}

Send a list get-all request (@since 1.19.0)

String endpointPath = EndpointPaths.USERS; // holds the value "/Users"
ServerResponse<ListResponse<User>> response = scimRequestBuilder.list(User.class, endpointPath)
                                                                  .filter("username", Comparator.CO, "ai")
                                                                     .and("locale", Comparator.EQ, "EN")
                                                                  .build()
                                                                  .sortBy("username")
                                                                  .sortOrder(SortOrder.DESCENDING)
                                                                .post() // http method to use (get or post)
                                                                .getAll(); // <- available since 1.19.0
if (response.isSuccess()) 
{
  ListResponse<User> returnedUserList = response.getResource();
  // do something with it
}
else if(response.getErrorResponse() == null)
{
  // the response was not an error response as described in RFC7644
  String errorMessage = response.getResponseBody();
}
else
{
  ErrorResponse errorResponse = response.getErrorResponse();
  // do something with it
}

Send an update request

String id = getIdOfAnExistingUser();
String endpointPath = EndpointPaths.USERS; // holds the value "/Users"
User updateUser = User.builder().nickName("hello world").build();
ServerResponse<User> response = scimRequestBuilder.update(User.class, endpointPath, id)
                                                  .setResource(updateUser)
                                                  .sendRequest();
if (response.isSuccess()) 
{
  User updatedUser = response.getResource();
  // do something with it
}
else if(response.getErrorResponse() == null)
{
  // the response was not an error response as described in RFC7644
  String errorMessage = response.getResponseBody();
}
else
{
  ErrorResponse errorResponse = response.getErrorResponse();
  // do something with it
}

Send a patch request

final String emailValue = "[email protected]";
final String emailType = "fun";
final boolean emailPrimary = true;
final String givenName = "Link";
final String locale = "JAP";

Email email = Email.builder().value(emailValue).type(emailType).primary(emailPrimary).build();
User addingResource = User.builder().emails(Collections.singletonList(email)).build();

String id = getIdOfAnExistingUser();
String endpointPath = EndpointPaths.USERS; // holds the value "/Users"

ServerResponse<User> response = scimRequestBuilder.patch(User.class, EndpointPaths.USERS, randomUser.getId().get())
                                                  .addOperation()
                                                    .path("name.givenname")
                                                    .op(PatchOp.ADD)
                                                    .value(givenName)
                                                  .next()
                                                    .path("locale")
                                                    .op(PatchOp.REPLACE)
                                                    .value(locale)
                                                  .next()
                                                    .op(PatchOp.ADD)
                                                    .valueNode(addingResource)
                                                  .build()
                                                  .sendRequest();
if (response.isSuccess()) 
{
  User updatedUser = response.getResource();
  // do something with it
}
else if(response.getErrorResponse() == null)
{
  // the response was not an error response as described in RFC7644
  String errorMessage = response.getResponseBody();
}
else
{
  ErrorResponse errorResponse = response.getErrorResponse();
  // do something with it
}

Get the complete SCIM ServiceProvider configuration

real world example:

/**
   * loads the meta-data from the given remote SCIM provider and adds the result to the
   * {@link ScimRemoteProvider}-configuration
   */
  public MetaConfiguration loadConfigurationFromRemoteProvider(ScimRemoteProviderEntity scimRemoteProvider)
  {
    try (ScimRequestBuilder scimRequestBuilder = ScimRequestBuilderCreator.createScimRequestBuilder(scimRemoteProvider))
    {
      MetaConfigRequestDetails requestDetails = MetaConfigRequestDetails.builder()
                                                                        .excludeMetaSchemas(true)
                                                                        .excludeMetaResourceTypes(true)
                                                                        .build();
      ServerResponse<MetaConfiguration> response;
      try
      {
        response = scimRequestBuilder.loadMetaConfiguration(requestDetails).sendRequest();
      }
      catch (Exception ex)
      {
        String cause = Optional.ofNullable(ExceptionUtils.getRootCause(ex))
                               .map(Throwable::getMessage)
                               .orElse(ex.getMessage());
        String errorMessage = String.format("Failed to load meta-configuration from remote SCIM provider: %s"
                                            + "\n\terrorMessage: %s",
                                            scimRemoteProvider.getBaseUrl(),
                                            cause);
        throw new PreconditionFailedException(errorMessage, ex);
      }

      if (response.isSuccess())
      {
        return response.getResource();
      }
      else
      {
        String errorMessage = String.format("Failed to load meta-configuration from remote SCIM provider: %s"
                                            + "\n\tresponseStatus: %s\n\tresponse-body: \"%s\"",
                                            scimRemoteProvider.getBaseUrl(),
                                            response.getHttpStatus(),
                                            response.getResponseBody());
        throw new PreconditionFailedException(errorMessage);
      }
    }
  }

Bulk Requests

Send a bulk request

The bulk response handling differs from the other requests. This is not directly the fault of the specification its more that in some cases the specification was not clear enough on how the response should be returned which is why I needed to handle several different cases

final String bulkId = UUID.randomUUID().toString();
BulkBuilder builder = scimRequestBuilder.bulk();

Member groupMember = Member.builder().value("bulkId:" + bulkId).type(ResourceTypeNames.USER).build();
Group adminGroup = Group.builder().displayName("admin").members(Arrays.asList(groupMember)).build();

ServerResponse<BulkResponse> response = builder.bulkRequestOperation(EndpointPaths.USERS)
                                                 .data(User.builder().userName("goldfish").build())
                                                 .method(HttpMethod.POST)
                                                 .bulkId(bulkId)
                                               .next()
                                                 .bulkRequestOperation(EndpointPaths.GROUPS)
                                                 .method(HttpMethod.POST)
                                                 .bulkId(UUID.randomUUID().toString()) // required param on post
                                                 .data(groupMember)
                                               .sendRequest();
if (response.isSuccess()) 
{
  BulkResponse bulkResponse = response.getResource();
  // do something with it
}
else if(response.getErrorResponse() == null && response.getResource() == null)
{
  // the response was not an error response as described in RFC7644
  String errorMessage = response.getResponseBody();
}
else if(response.getErrorResponse() == null)
{
  BulkResponse bulkResponse = response.getResource();
  // do something with it (you will find at least one error message in the bulk response)
}
else 
{
  ErrorResponse errorResponse = response.getErrorResponse();
  // do something with it
}

Automatic Bulk Request Splitting

The SCIM-provider has a maximum number of operations that are allowed in a single bulk-request. Splitting such bulk requests into several single requests if the number of your operations exceeds this limit may become a real pain. For this reason it is possible to allow automatic splitting of these requests by even preserving the order of the requests.

In order to activate this feature you need to enable it within the ScimClientConfig

ScimClientConfig scimClientConfig = ScimClientConfig.builder().enableAutomaticBulkRequestSplitting(true).build();
ScimRequestBuilder scimRequestBuilder = new ScimRequestBuilder(baseUrl, scimClientConfig);

In order for the client implementation to split the bulk-requests correctly the maximum number of operations from the server is required. There are two different strategies to obtain this information from the server:

Setting the Service Provider Config manually

ScimRequestBuilder scimRequestBuilder = new ScimRequestBuilder(...);
ServiceProvider serviceProvider = obtainServiceProviderConfig();
scimRequestBuilder.setServiceProvider(serviceProvider);

Service Provider Config is retrieved by the BulkBuilder-implementation

If you do not set the Service Provider Config manually the BulkBuilder implementation will try to resolve it implicitly by sending a request to the /ServiceProviderConfig endpoint to obtain the configuration.

If the bulk-request-operations are having relations with bulkIds to each other the client will try its best to split the operations into different requests while maintaining the bulkId-relations. If necessary the relations will be resolved directly by the client-implementation.

Interacting with splitted bulk requests

The splitted operations are put into different requests and the client will send one request after another and will write some debug-messages into the log so you will be able to observe what is happening.

Normally the client will send all requests to the server merge all responses into a single response together and will then return them. If you want to act on each single response though you can give a response-resolver implementation into the sendRequest-method:

here is an example of a client implementation that I use to add test-data into the keycloak server:

ServerResponse<BulkResponse> serverResponse = bulkBuilder.sendRequest(response -> {
  if (response.isSuccess())
  {
    log.info("bulk request succeeded with response: {}", response.getResponseBody());
    BulkResponse bulkResponse = response.getResource();
    for ( BulkResponseOperation bulkResponseOperation : bulkResponse.getBulkResponseOperations() )
    {
      BulkRequestOperation requestOperation = bulkBuilder.getOperationByBulkId(bulkResponseOperation.getBulkId()
                                                                                                    .get());
      User user = JsonHelper.readJsonDocument(requestOperation.getData().get(), User.class);
      if (HttpStatus.CREATED == bulkResponseOperation.getStatus())
      {
        log.debug("User with name {} was successfully created", user.getUserName().orElse(null));
      }
      else
      {
        if (requestOperation == null)
        {
          log.warn("found failed operation: {}", bulkResponseOperation.toPrettyString());
        }
        else
        {
          log.warn("User with name {} could not be created. Status is: {}",
                   user.getUserName().orElse(null),
                   bulkResponseOperation.getStatus());
        }
      }
    }
  }
  else
  {
    log.error("creating of users failed: " + response.getErrorResponse().getDetail().orElse(null));
  }
});

Sending splitted bulk-requests in parallel-streams

If you have a lot of operations to be executed that are e.g. splitted by the client implementation into over 200 requests with each 50 operations you can send these requests in parallel streams.

ScimClientConfig scimClientConfig = ScimClientConfig.builder().enableAutomaticBulkRequestSplitting(true).build();
ScimRequestBuilder scimRequestBuilder = new ScimRequestBuilder(baseUrl, scimClientConfig);
final boolean runSplittedRequestsParallel = true;
BulkBuilder bulkBuilder = createBulkBuilderWithOperations();
bulkBuilder.sendRequest(runSplittedRequestsParallel);

The client will by default use the ForkJoinPool.commonPool(). If you want to override this behaviour you need to manipulate the loaded ServiceProvider within the scimRequestBuilder:

ServiceProvider serviceProvider = loadServiceProviderConfig();
serviceProvider.setThreadPool(new ForkJoinPool(10));
scimRequestBuilder.setServiceProvider(serviceProvider);

or

serviceProvider = scimRequestBuilder.loadServiceProviderConfiguration();
serviceProvider.setThreadPool(new ForkJoinPool(10));

WARNING:

Do not use parallel streams if you use bulkId references in your operations. This will most probably break the order and lead to unexpected errors in the client.