001/*
002 * Copyright 2015-2024 Ping Identity Corporation
003 *
004 * This program is free software; you can redistribute it and/or modify
005 * it under the terms of the GNU General Public License (GPLv2 only)
006 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
007 * as published by the Free Software Foundation.
008 *
009 * This program is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
012 * GNU General Public License for more details.
013 *
014 * You should have received a copy of the GNU General Public License
015 * along with this program; if not, see <http://www.gnu.org/licenses>.
016 */
017
018package com.unboundid.scim2.client;
019
020import com.fasterxml.jackson.jakarta.rs.json.JacksonJsonProvider;
021import com.unboundid.scim2.client.requests.CreateRequestBuilder;
022import com.unboundid.scim2.client.requests.DeleteRequestBuilder;
023import com.unboundid.scim2.client.requests.ModifyRequestBuilder;
024import com.unboundid.scim2.client.requests.ReplaceRequestBuilder;
025import com.unboundid.scim2.client.requests.RetrieveRequestBuilder;
026import com.unboundid.scim2.client.requests.SearchRequestBuilder;
027import com.unboundid.scim2.common.ScimResource;
028import com.unboundid.scim2.common.annotations.NotNull;
029import com.unboundid.scim2.common.annotations.Nullable;
030import com.unboundid.scim2.common.exceptions.ScimException;
031import com.unboundid.scim2.common.messages.ListResponse;
032import com.unboundid.scim2.common.messages.PatchOperation;
033import com.unboundid.scim2.common.messages.PatchRequest;
034import com.unboundid.scim2.common.types.Meta;
035import com.unboundid.scim2.common.types.ResourceTypeResource;
036import com.unboundid.scim2.common.types.SchemaResource;
037import com.unboundid.scim2.common.types.ServiceProviderConfigResource;
038import com.unboundid.scim2.common.utils.JsonUtils;
039
040import jakarta.ws.rs.client.WebTarget;
041import jakarta.ws.rs.core.MediaType;
042import java.net.URI;
043
044import static com.unboundid.scim2.common.utils.ApiConstants.MEDIA_TYPE_SCIM;
045import static com.unboundid.scim2.common.utils.ApiConstants.ME_ENDPOINT;
046import static com.unboundid.scim2.common.utils.ApiConstants.RESOURCE_TYPES_ENDPOINT;
047import static com.unboundid.scim2.common.utils.ApiConstants.SCHEMAS_ENDPOINT;
048import static com.unboundid.scim2.common.utils.ApiConstants.SERVICE_PROVIDER_CONFIG_ENDPOINT;
049
050/**
051 * The main entry point to the client API used to access a SCIM 2 service
052 * provider.
053 */
054public class ScimService implements ScimInterface
055{
056  /**
057   * The authenticated subject alias.
058   */
059  @NotNull
060  public static final URI ME_URI = URI.create(ME_ENDPOINT);
061
062  /**
063   * The SCIM media type.
064   */
065  @NotNull
066  public static final MediaType MEDIA_TYPE_SCIM_TYPE =
067      MediaType.valueOf(MEDIA_TYPE_SCIM);
068
069  @NotNull
070  private final WebTarget baseTarget;
071
072  @Nullable
073  private volatile ServiceProviderConfigResource serviceProviderConfig;
074
075  /**
076   * Create a new client instance to the SCIM 2 service provider at the
077   * provided WebTarget. The path of the WebTarget should be the base URI
078   * SCIM 2 service (i.e., {@code https://host/scim/v2}).
079   *
080   * @param baseTarget The web target for the base URI of the SCIM 2 service
081   *                   provider.
082   */
083  public ScimService(@NotNull final WebTarget baseTarget)
084  {
085    this.baseTarget = baseTarget.register(
086        new JacksonJsonProvider(JsonUtils.createObjectMapper(),
087            JacksonJsonProvider.BASIC_ANNOTATIONS));
088  }
089
090  /**
091   * Retrieve the service provider configuration.
092   *
093   * @return the service provider configuration.
094   * @throws ScimException if an error occurs.
095   */
096  @NotNull
097  public ServiceProviderConfigResource getServiceProviderConfig()
098      throws ScimException
099  {
100    if(serviceProviderConfig == null)
101    {
102      serviceProviderConfig = retrieve(
103          baseTarget.path(SERVICE_PROVIDER_CONFIG_ENDPOINT).getUri(),
104          ServiceProviderConfigResource.class);
105    }
106    return serviceProviderConfig;
107  }
108
109  /**
110   * Retrieve the resource types supported by the service provider.
111   *
112   * @return The list of resource types supported by the service provider.
113   * @throws ScimException if an error occurs.
114   */
115  @NotNull
116  public ListResponse<ResourceTypeResource> getResourceTypes()
117      throws ScimException
118  {
119    return searchRequest(RESOURCE_TYPES_ENDPOINT).
120        invoke(ResourceTypeResource.class);
121  }
122
123  /**
124   * Retrieve a known resource type supported by the service provider.
125   *
126   * @param name The name of the resource type.
127   * @return The resource type with the provided name.
128   * @throws ScimException if an error occurs.
129   */
130  @NotNull
131  public ResourceTypeResource getResourceType(@NotNull final String name)
132      throws ScimException
133  {
134    return retrieve(RESOURCE_TYPES_ENDPOINT, name, ResourceTypeResource.class);
135  }
136
137  /**
138   * Retrieve the schemas supported by the service provider.
139   *
140   * @return The list of schemas supported by the service provider.
141   * @throws ScimException if an error occurs.
142   */
143  @NotNull
144  public ListResponse<SchemaResource> getSchemas()
145      throws ScimException
146  {
147    return searchRequest(SCHEMAS_ENDPOINT).invoke(SchemaResource.class);
148  }
149
150  /**
151   * Retrieve a known schema supported by the service provider.
152   *
153   * @param id The schema URN.
154   * @return The resource type with the provided URN.
155   * @throws ScimException if an error occurs.
156   */
157  @NotNull
158  public SchemaResource getSchema(@NotNull final String id)
159      throws ScimException
160  {
161    return retrieve(SCHEMAS_ENDPOINT, id, SchemaResource.class);
162  }
163
164  /**
165   * Create the provided new SCIM resource at the service provider.
166   *
167   * @param endpoint The resource endpoint such as: "{@code Users}" or "Groups" as
168   *                 defined by the associated resource type.
169   * @param resource The new resource to create.
170   * @param <T> The Java type of the resource.
171   * @return The successfully create SCIM resource.
172   * @throws ScimException if an error occurs.
173   */
174  @NotNull
175  public <T extends ScimResource> T create(@NotNull final String endpoint,
176                                           @NotNull final T resource)
177      throws ScimException
178  {
179    return createRequest(endpoint, resource).invoke();
180  }
181
182  /**
183   * Retrieve a known SCIM resource from the service provider.
184   *
185   * @param endpoint The resource endpoint such as: "{@code Users}" or "{@code Groups}" as
186   *                 defined by the associated resource type.
187   * @param id The resource identifier (for example the value of the "{@code id}"
188   *           attribute).
189   * @param cls The Java class object used to determine the type to return.
190   * @param <T> The Java type of the resource.
191   * @return The successfully retrieved SCIM resource.
192   * @throws ScimException if an error occurs.
193   */
194  @NotNull
195  public <T extends ScimResource> T retrieve(@NotNull final String endpoint,
196                                             @NotNull final String id,
197                                             @NotNull final Class<T> cls)
198      throws ScimException
199  {
200    return retrieveRequest(endpoint, id).invoke(cls);
201  }
202
203  /**
204   * Retrieve a known SCIM resource from the service provider.
205   *
206   * @param url The URL of the resource to retrieve.
207   * @param cls The Java class object used to determine the type to return.
208   * @param <T> The Java type of the resource.
209   * @return The successfully retrieved SCIM resource.
210   * @throws ScimException if an error occurs.
211   */
212  @NotNull
213  public <T extends ScimResource> T retrieve(@NotNull final URI url,
214                                             @NotNull final Class<T> cls)
215      throws ScimException
216  {
217    return retrieveRequest(url).invoke(cls);
218  }
219
220  /**
221   * Retrieve a known SCIM resource from the service provider. If the
222   * service provider supports resource versioning and the resource has not been
223   * modified, the provided resource will be returned.
224   *
225   * @param resource The resource to retrieve.
226   * @param <T> The Java type of the resource.
227   * @return The successfully retrieved SCIM resource.
228   * @throws ScimException if an error occurs.
229   */
230  @NotNull
231  public <T extends ScimResource> T retrieve(@NotNull final T resource)
232      throws ScimException
233  {
234    RetrieveRequestBuilder.Generic<T> builder = retrieveRequest(resource);
235    return builder.invoke();
236  }
237
238  /**
239   * Modify a SCIM resource by replacing the resource's attributes at the
240   * service provider. If the service provider supports resource versioning,
241   * the resource will only be modified if it has not been modified since it
242   * was retrieved.
243   *
244   * @param resource The previously retrieved and revised resource.
245   * @param <T> The Java type of the resource.
246   * @return The successfully replaced SCIM resource.
247   * @throws ScimException if an error occurs.
248   */
249  @NotNull
250  public <T extends ScimResource> T replace(@NotNull final T resource)
251      throws ScimException
252  {
253    ReplaceRequestBuilder<T> builder = replaceRequest(resource);
254    return builder.invoke();
255  }
256
257  /**
258   * Delete a SCIM resource at the service provider.
259   *
260   * @param endpoint The resource endpoint such as: "{@code Users}" or "{@code Groups}" as
261   *                 defined by the associated resource type.
262   * @param id The resource identifier (for example the value of the "{@code id}"
263   *           attribute).
264   * @throws ScimException if an error occurs.
265   */
266  public void delete(@NotNull final String endpoint, @NotNull final String id)
267      throws ScimException
268  {
269    deleteRequest(endpoint, id).invoke();
270  }
271
272  /**
273   * Delete a SCIM resource at the service provider.
274   *
275   * @param url The URL of the resource to delete.
276   * @throws ScimException if an error occurs.
277   */
278  public void delete(@NotNull final URI url)
279      throws ScimException
280  {
281    deleteRequest(url).invoke();
282  }
283
284  /**
285   * Delete a SCIM resource at the service provider.
286   *
287   * @param resource The resource to delete.
288   * @param <T> The Java type of the resource.
289   * @throws ScimException if an error occurs.
290   */
291  public <T extends ScimResource> void delete(@NotNull final T resource)
292      throws ScimException
293  {
294    DeleteRequestBuilder builder = deleteRequest(resource);
295    builder.invoke();
296  }
297
298  /**
299   * Build a request to create the provided new SCIM resource at the service
300   * provider.
301   *
302   * @param endpoint The resource endpoint such as: "{@code Users}" or "{@code Groups}" as
303   *                 defined by the associated resource type.
304   * @param resource The new resource to create.
305   * @param <T> The Java type of the resource.
306   * @return The request builder that may be used to specify additional request
307   * parameters and to invoke the request.
308   */
309  @NotNull
310  public <T extends ScimResource> CreateRequestBuilder<T> createRequest(
311      @NotNull final String endpoint,
312      @NotNull final T resource)
313  {
314    return new CreateRequestBuilder<T>(baseTarget.path(endpoint), resource);
315  }
316
317  /**
318   * Build a request to retrieve a known SCIM resource from the service
319   * provider.
320   *
321   * @param endpoint The resource endpoint such as: "{@code Users}" or "{@code Groups}" as
322   *                 defined by the associated resource type.
323   * @param id The resource identifier (for example the value of the "{@code id}"
324   *           attribute).
325   * @return The request builder that may be used to specify additional request
326   * parameters and to invoke the request.
327   */
328  @NotNull
329  public RetrieveRequestBuilder.Typed retrieveRequest(
330      @NotNull final String endpoint,
331      @NotNull final String id)
332  {
333    return new RetrieveRequestBuilder.Typed(baseTarget.path(endpoint).path(id));
334  }
335
336  /**
337   * Build a request to retrieve a known SCIM resource from the service
338   * provider.
339   *
340   * @param url The URL of the resource to retrieve.
341   * @return The request builder that may be used to specify additional request
342   * parameters and to invoke the request.
343   */
344  @NotNull
345  public RetrieveRequestBuilder.Typed retrieveRequest(@NotNull final URI url)
346  {
347    return new RetrieveRequestBuilder.Typed(resolveWebTarget(url));
348  }
349
350  /**
351   * Build a request to retrieve a known SCIM resource from the service
352   * provider.
353   *
354   * @param resource The resource to retrieve.
355   * @param <T> The Java type of the resource.
356   * @return The request builder that may be used to specify additional request
357   * parameters and to invoke the request.
358   */
359  @NotNull
360  public <T extends ScimResource> RetrieveRequestBuilder.Generic<T>
361      retrieveRequest(@NotNull final T resource)
362  {
363    return new RetrieveRequestBuilder.Generic<T>(
364        resolveWebTarget(checkAndGetLocation(resource)), resource);
365  }
366
367  /**
368   * Build a request to query and retrieve resources of a single resource type
369   * from the service provider.
370   *
371   * @param endpoint The resource endpoint such as: "{@code Users}" or "{@code Groups}" as
372   *                 defined by the associated resource type.
373   * @return The request builder that may be used to specify additional request
374   * parameters and to invoke the request.
375   */
376  @NotNull
377  public SearchRequestBuilder searchRequest(@NotNull final String endpoint)
378  {
379    return new SearchRequestBuilder(baseTarget.path(endpoint));
380  }
381
382  /**
383   * Build a request to modify a SCIM resource by replacing the resource's
384   * attributes at the service provider.
385   *
386   * @param uri The URL of the resource to modify.
387   * @param resource The resource to replace.
388   * @param <T> The Java type of the resource.
389   * @return The request builder that may be used to specify additional request
390   * parameters and to invoke the request.
391   */
392  @NotNull
393  public <T extends ScimResource> ReplaceRequestBuilder<T> replaceRequest(
394      @NotNull final URI uri,
395      @NotNull final T resource)
396  {
397    return new ReplaceRequestBuilder<T>(resolveWebTarget(uri), resource);
398  }
399
400  /**
401   * Build a request to modify a SCIM resource by replacing the resource's
402   * attributes at the service provider.
403   *
404   * @param resource The previously retrieved and revised resource.
405   * @param <T> The Java type of the resource.
406   * @return The request builder that may be used to specify additional request
407   * parameters and to invoke the request.
408   */
409  @NotNull
410  public <T extends ScimResource> ReplaceRequestBuilder<T> replaceRequest(
411      @NotNull final T resource)
412  {
413    return new ReplaceRequestBuilder<T>(
414        resolveWebTarget(checkAndGetLocation(resource)), resource);
415  }
416
417  /**
418   * {@inheritDoc}
419   */
420  @Override
421  @NotNull
422  public <T extends ScimResource> T modify(
423      @NotNull final String endpoint,
424      @NotNull final String id,
425      @NotNull final PatchRequest patchRequest,
426      @NotNull final Class<T> clazz)
427          throws ScimException
428  {
429    ModifyRequestBuilder.Typed requestBuilder = new ModifyRequestBuilder.Typed(
430        baseTarget.path(endpoint).path(id));
431    for(PatchOperation op : patchRequest.getOperations())
432    {
433      requestBuilder.addOperation(op);
434    }
435    return requestBuilder.invoke(clazz);
436  }
437
438  /**
439   * Modify a SCIM resource by updating one or more attributes using a sequence
440   * of operations to "{@code add}", "{@code remove}", or "{@code replace}"
441   * values. The service provider configuration may be used to discover service
442   * provider support for PATCH.
443   *
444   * @param endpoint The resource endpoint such as: "{@code Users}" or
445   *                 "{@code Groups}" as defined by the associated resource
446   *                 type.
447   * @param id The resource identifier (for example the value of the "{@code id}"
448   *           attribute).
449   * @return The request builder that may be used to specify additional request
450   * parameters and to invoke the request.
451   */
452  @NotNull
453  public ModifyRequestBuilder.Typed modifyRequest(
454      @NotNull final String endpoint,
455      @NotNull final String id)
456  {
457    return new ModifyRequestBuilder.Typed(
458        baseTarget.path(endpoint).path(id));
459  }
460
461
462  /**
463   * Modify a SCIM resource by updating one or more attributes using a sequence
464   * of operations to "{@code add}", "{@code remove}", or "{@code replace}"
465   * values. The service provider configuration may be used to discover service
466   * provider support for PATCH.
467   *
468   * @param url The URL of the resource to modify.
469   * @return The request builder that may be used to specify additional request
470   * parameters and to invoke the request.
471   */
472  @NotNull
473  public  ModifyRequestBuilder.Typed modifyRequest(@NotNull final URI url)
474  {
475    return new ModifyRequestBuilder.Typed(resolveWebTarget(url));
476  }
477
478  /**
479   * {@inheritDoc}
480   */
481  @Override
482  @NotNull
483  public <T extends ScimResource> T modify(
484      @NotNull final T resource,
485      @NotNull final PatchRequest patchRequest)
486          throws ScimException
487  {
488    ModifyRequestBuilder.Generic<T> requestBuilder =
489        new ModifyRequestBuilder.Generic<T>(resolveWebTarget(
490            checkAndGetLocation(resource)), resource);
491
492    for(PatchOperation op : patchRequest.getOperations())
493    {
494      requestBuilder.addOperation(op);
495    }
496    return requestBuilder.invoke();
497 }
498
499  /**
500   * Modify a SCIM resource by updating one or more attributes using a sequence
501   * of operations to "{@code add}", "{@code remove}", or "{@code replace}"
502   * values. The service provider configuration may be used to discover service
503   * provider support for PATCH.
504   *
505   * @param resource The resource to modify.
506   * @param <T> The Java type of the resource.
507   * @return The request builder that may be used to specify additional request
508   * parameters and to invoke the request.
509   */
510  @NotNull
511  public <T extends ScimResource> ModifyRequestBuilder.Generic<T> modifyRequest(
512      @NotNull final T resource)
513  {
514    return new ModifyRequestBuilder.Generic<T>(
515        resolveWebTarget(checkAndGetLocation(resource)), resource);
516  }
517
518  /**
519   * Build a request to delete a SCIM resource at the service provider.
520   *
521   * @param endpoint The resource endpoint such as: "{@code Users}" or
522   *                 "{@code Groups}" as defined by the associated resource
523   *                 type.
524   * @param id The resource identifier (for example the value of the "{@code id}"
525   *           attribute).
526   * @return The request builder that may be used to specify additional request
527   * parameters and to invoke the request.
528   * @throws ScimException if an error occurs.
529   */
530  @NotNull
531  public DeleteRequestBuilder deleteRequest(@NotNull final String endpoint,
532                                            @NotNull final String id)
533      throws ScimException
534  {
535    return new DeleteRequestBuilder(baseTarget.path(endpoint).path(id));
536  }
537
538  /**
539   * Build a request to delete a SCIM resource at the service provider.
540   *
541   * @param url The URL of the resource to delete.
542   * @return The request builder that may be used to specify additional request
543   * parameters and to invoke the request.
544   * @throws ScimException if an error occurs.
545   */
546  @NotNull
547  public DeleteRequestBuilder deleteRequest(@NotNull final URI url)
548      throws ScimException
549  {
550    return new DeleteRequestBuilder(resolveWebTarget(url));
551  }
552
553  /**
554   * Build a request to delete a SCIM resource at the service provider.
555   *
556   * @param resource The resource to delete.
557   * @param <T> The Java type of the resource.
558   * @return The request builder that may be used to specify additional request
559   * parameters and to invoke the request.
560   * @throws ScimException if an error occurs.
561   */
562  @NotNull
563  public <T extends ScimResource> DeleteRequestBuilder deleteRequest(
564      @NotNull final T resource)
565          throws ScimException
566  {
567    return deleteRequest(checkAndGetLocation(resource));
568  }
569
570  /**
571   * Resolve a URL (relative or absolute) to a web target.
572   *
573   * @param url The URL to resolve.
574   * @return The WebTarget.
575   */
576  @NotNull
577  private WebTarget resolveWebTarget(@NotNull final URI url)
578  {
579    URI relativePath;
580    if(url.isAbsolute())
581    {
582      relativePath = baseTarget.getUri().relativize(url);
583      if (relativePath.equals(url))
584      {
585        // The given resource's location is from another service provider
586        throw new IllegalArgumentException("Given resource's location " +
587            url + " is not under this service's " +
588            "base path " + baseTarget.getUri());
589      }
590    }
591    else
592    {
593      relativePath = url;
594    }
595
596    return baseTarget.path(relativePath.getRawPath());
597  }
598
599  /**
600   * Get the meta.location attribute value of the SCIM resource.
601   *
602   * @param resource The SCIM resource.
603   * @return The meta.location attribute value.
604   * @throws IllegalArgumentException if the resource does not contain the
605   * meta.location attribute value.
606   */
607  @NotNull
608  private URI checkAndGetLocation(@NotNull final ScimResource resource)
609      throws IllegalArgumentException
610  {
611    Meta meta = resource.getMeta();
612    if(meta == null || meta.getLocation() == null)
613    {
614      throw new IllegalArgumentException(
615          "Resource URI must be specified by meta.location");
616    }
617    return meta.getLocation();
618  }
619
620  /**
621   * {@inheritDoc}
622   */
623  @NotNull
624  public <T extends ScimResource> ListResponse<T> search(
625      @NotNull final String endpoint,
626      @Nullable final String filter,
627      @NotNull final Class<T> clazz)
628          throws ScimException
629  {
630    return searchRequest(endpoint).filter(filter).invoke(clazz);
631  }
632}