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}