001/* 002 * Copyright 2011-2016 UnboundID Corp. 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.scim.sdk; 019 020import com.unboundid.scim.data.GroupResource; 021import com.unboundid.scim.data.BaseResource; 022import com.unboundid.scim.data.ResourceFactory; 023import com.unboundid.scim.data.ServiceProviderConfig; 024import com.unboundid.scim.data.UserResource; 025import com.unboundid.scim.schema.CoreSchema; 026import com.unboundid.scim.schema.ResourceDescriptor; 027import org.apache.http.auth.AuthScope; 028import org.apache.http.auth.UsernamePasswordCredentials; 029import org.apache.http.impl.client.BasicCredentialsProvider; 030import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; 031import org.apache.wink.client.RestClient; 032import org.glassfish.jersey.apache.connector.ApacheClientProperties; 033import org.glassfish.jersey.apache.connector.ApacheConnectorProvider; 034import org.glassfish.jersey.client.ClientConfig; 035 036import javax.ws.rs.client.ClientRequestContext; 037import javax.ws.rs.client.ClientRequestFilter; 038import javax.ws.rs.core.MediaType; 039import java.io.IOException; 040import java.net.URI; 041import java.util.List; 042 043import static com.unboundid.scim.schema.CoreSchema 044 .createCustomGroupResourceDescriptor; 045import static com.unboundid.scim.schema.CoreSchema 046 .createCustomUserResourceDescriptor; 047 048 049/** 050 * The SCIMService class represents a client connection to a SCIM service 051 * provider. It handles setting up and configuring the connection which will 052 * be used by the SCIMEndpoints that are obtained form this SCIMService. 053 */ 054public class SCIMService 055{ 056 private final RestClient client; 057 private final URI baseURL; 058 059 private MediaType acceptType = MediaType.APPLICATION_JSON_TYPE; 060 private MediaType contentType = MediaType.APPLICATION_JSON_TYPE; 061 private final boolean[] overrides = new boolean[3]; 062 private String userAgent; 063 private boolean useUrlSuffix; 064 065 /** 066 * Constructs a new SCIMService from a url and a hersey client config. 067 * @param baseUrl The SCIM Service Provider URL. 068 * @param clientConfig The client config object. 069 */ 070 public SCIMService(final URI baseUrl, 071 final org.glassfish.jersey.client.ClientConfig clientConfig) 072 { 073 this.baseURL = baseUrl; 074 this.client = new RestClient(clientConfig); 075 } 076 077 /** 078 * Constructs a new SCIMService. 079 * 080 * @param baseUrl The SCIM Service Provider URL. 081 */ 082 public SCIMService(final URI baseUrl) 083 { 084 this(baseUrl, createDefaultClientConfig()); 085 } 086 087 /** 088 * Constructs a new SCIMService with OAuth authentication support 089 * using the provided credentials. 090 091 * @param baseUrl The SCIM Service Provider URL. 092 * @param oAuthToken The OAuth token. 093 */ 094 public SCIMService(final URI baseUrl, final OAuthToken oAuthToken) { 095 this(baseUrl, createDefaultClientConfig().register( 096 new ClientRequestFilter() 097 { 098 public void filter(final ClientRequestContext clientRequestContext) 099 throws IOException 100 { 101 try 102 { 103 clientRequestContext.getHeaders().add( 104 "Authorization", oAuthToken.getFormattedValue()); 105 } 106 catch (Exception ex) 107 { 108 throw new RuntimeException( 109 "Unable to add authorization handler", ex); 110 } 111 } 112 } 113 )); 114 } 115 116 /** 117 * Constructs a new SCIMService with basic authentication support 118 * using the provided credentials. 119 * 120 * @param baseUrl The SCIM Service Provider URL. 121 * @param username The username. 122 * @param password The password. 123 */ 124 public SCIMService(final URI baseUrl, final String username, 125 final String password) 126 { 127 this(baseUrl, createDefaultClientConfig(). 128 property(ApacheClientProperties.CREDENTIALS_PROVIDER, 129 createBasicCredentialsProvider(username, password)). 130 property(ApacheClientProperties.PREEMPTIVE_BASIC_AUTHENTICATION, true)); 131 } 132 133 /** 134 * Returns a SCIMEndpoint with the current settings that can be used to 135 * invoke CRUD operations. Any changes to the SCIMService configuration will 136 * not be reflected in the returned SCIMEndpoint. 137 * 138 * @param resourceDescriptor The ResourceDescriptor of the endpoint. 139 * @param resourceFactory The ResourceFactory that should be used to 140 * create SCIM resource instances. 141 * @param <R> The type of SCIM resource instances. 142 * @return The SCIMEndpoint that can be used to invoke CRUD operations. 143 */ 144 public <R extends BaseResource> SCIMEndpoint<R> getEndpoint( 145 final ResourceDescriptor resourceDescriptor, 146 final ResourceFactory<R> resourceFactory) 147 { 148 return new SCIMEndpoint<R>(this, client, resourceDescriptor, 149 resourceFactory); 150 } 151 152 153 /** 154 * Returns a SCIMEndpoint for the specified endpoint. 155 * 156 * @param endpointPath SCIM endpoint relative path, e.g. "Users". 157 * @return SCIMEndpoint that can be used to invoke CRUD operations. 158 * @throws SCIMException for an invalid endpoint path 159 */ 160 public SCIMEndpoint<BaseResource> getEndpoint( 161 final String endpointPath) throws SCIMException 162 { 163 return getEndpoint(endpointPath, BaseResource.BASE_RESOURCE_FACTORY); 164 } 165 166 167 /** 168 * Returns a SCIMEndpoint for the specified endpoint. 169 * 170 * @param endpointPath SCIM endpoint relative path, e.g. "Users". 171 * @param resourceFactory The ResourceFactory that should be used to 172 * create SCIM resource instances. 173 * @param <R> the type of SCIM resource instances. 174 * @return SCIMEndpoint that can be used to invoke CRUD operations. 175 * @throws SCIMException for an invalid endpoint path. 176 */ 177 public <R extends BaseResource> SCIMEndpoint<R> getEndpoint( 178 final String endpointPath, 179 final ResourceFactory<R> resourceFactory) 180 throws SCIMException 181 { 182 ResourceDescriptor descriptor = 183 getResourceDescriptorForEndpoint(endpointPath); 184 if (descriptor == null) { 185 throw new ResourceNotFoundException( 186 "No schema found for endpoint " + endpointPath); 187 } 188 return getEndpoint(descriptor, resourceFactory); 189 } 190 191 192 193 /** 194 * Returns a SCIMEndpoint for the Users endpoint defined in the core schema. 195 * 196 * @return The SCIMEndpoint for the Users endpoint defined in the core schema. 197 */ 198 public SCIMEndpoint<UserResource> getUserEndpoint() 199 { 200 return new SCIMEndpoint<UserResource>(this, client, 201 CoreSchema.USER_DESCRIPTOR, UserResource.USER_RESOURCE_FACTORY); 202 } 203 204 205 206 /** 207 * Returns a SCIMEndpoint for the Users endpoint defined in the core schema 208 * with a custom user resource name and users endpoint name. 209 * 210 * @param userResourceName Provide a custom user resource name. 211 * @param usersEndpointName Provide a custom users endpoint name. 212 * @return The SCIMEndpoint for the Users endpoint defined in the core schema. 213 */ 214 public SCIMEndpoint<UserResource> getUserEndpoint( 215 final String userResourceName, final String usersEndpointName) 216 { 217 return new SCIMEndpoint<UserResource>(this, client, 218 createCustomUserResourceDescriptor(userResourceName, usersEndpointName), 219 UserResource.USER_RESOURCE_FACTORY); 220 } 221 222 223 224 /** 225 * Returns a SCIMEndpoint for the Groups endpoint defined in the core schema. 226 * 227 * @return The SCIMEndpoint for the Groups endpoint defined in the 228 * core schema. 229 */ 230 public SCIMEndpoint<GroupResource> getGroupEndpoint() 231 { 232 return new SCIMEndpoint<GroupResource>(this, client, 233 CoreSchema.GROUP_DESCRIPTOR, GroupResource.GROUP_RESOURCE_FACTORY); 234 } 235 236 237 238 /** 239 * Returns a SCIMEndpoint for the Groups endpoint defined in the core schema 240 * with a custom group resource name and groups endpoint name. 241 * 242 * @param groupResourceName Provide a custom group resource name. 243 * @param groupsEndpointName Provide a custom groups endpoint name. 244 * @return The SCIMEndpoint for the Groups endpoint defined in the 245 * core schema. 246 */ 247 public SCIMEndpoint<GroupResource> getGroupEndpoint( 248 final String groupResourceName, final String groupsEndpointName) 249 { 250 return new SCIMEndpoint<GroupResource>(this, client, 251 createCustomGroupResourceDescriptor(groupResourceName, 252 groupsEndpointName), GroupResource.GROUP_RESOURCE_FACTORY); 253 } 254 255 256 257 /** 258 * Returns a SCIMEndpoint for the Schemas endpoint. This endpoint allows for 259 * the retrieval of schema for all service provider supported resources. 260 * 261 * @return The SCIMEndpoint for the Schemas endpoint. 262 */ 263 public SCIMEndpoint<ResourceDescriptor> getResourceSchemaEndpoint() 264 { 265 return new SCIMEndpoint<ResourceDescriptor>(this, client, 266 CoreSchema.RESOURCE_SCHEMA_DESCRIPTOR, 267 ResourceDescriptor.RESOURCE_DESCRIPTOR_FACTORY); 268 } 269 270 /** 271 * Retrieves the ResourceDescriptor for the specified resource from the 272 * SCIM service provider. 273 * 274 * @param resourceName The name of the resource. 275 * @param schema The schema URN of the resource or <code>null</code> 276 * to match only based on the name of the resource. 277 * @return The ResourceDescriptor for the specified resource or 278 * <code>null</code> if none are found. 279 * @throws SCIMException If the ResourceDescriptor could not be read. 280 */ 281 public ResourceDescriptor getResourceDescriptor(final String resourceName, 282 final String schema) 283 throws SCIMException 284 { 285 final SCIMEndpoint<ResourceDescriptor> endpoint = 286 getResourceSchemaEndpoint(); 287 String filter = "name eq \"" + resourceName + "\""; 288 if(schema != null) 289 { 290 filter += " and schema eq \"" + schema + "\""; 291 } 292 final Resources<ResourceDescriptor> resources = endpoint.query(filter); 293 if(resources.getTotalResults() == 0) 294 { 295 return null; 296 } 297 if(resources.getTotalResults() > 1) 298 { 299 throw new InvalidResourceException( 300 "The service provider returned multiple resource descriptors " + 301 "with resource name '" + resourceName); 302 } 303 304 ResourceDescriptor descriptor = resources.iterator().next(); 305 descriptor.setStrictMode(false); 306 307 return descriptor; 308 } 309 310 /** 311 * Retrieves the ResourceDescriptor for the specified endpoint from the 312 * SCIM service provider. 313 * 314 * @param endpoint The name of the SCIM endpoint, e.g. "Users". 315 * @return The ResourceDescriptor for the specified endpoint or 316 * <code>null</code> if none are found. 317 * @throws SCIMException If the ResourceDescriptor could not be read. 318 */ 319 public ResourceDescriptor getResourceDescriptorForEndpoint( 320 final String endpoint) 321 throws SCIMException 322 { 323 final SCIMEndpoint<ResourceDescriptor> schemaEndpoint = 324 getResourceSchemaEndpoint(); 325 String filter = "endpoint eq \"" + endpoint + "\""; 326 327 final Resources<ResourceDescriptor> resources = 328 schemaEndpoint.query(filter); 329 if(resources.getTotalResults() == 0) 330 { 331 return null; 332 } 333 if(resources.getTotalResults() > 1) 334 { 335 throw new InvalidResourceException( 336 "The service provider returned multiple resource descriptors " + 337 "for endpoint '" + endpoint); 338 } 339 340 ResourceDescriptor descriptor = resources.iterator().next(); 341 descriptor.setStrictMode(false); 342 343 return descriptor; 344 } 345 346 /** 347 * Retrieves the Service Provider Config from the SCIM service provider. 348 * 349 * @return The Service Provider Config. 350 * 351 * @throws SCIMException If the Service Provider Config could not be read. 352 */ 353 public ServiceProviderConfig getServiceProviderConfig() 354 throws SCIMException 355 { 356 final SCIMEndpoint<ServiceProviderConfig> endpoint = 357 getEndpoint(CoreSchema.SERVICE_PROVIDER_CONFIG_SCHEMA_DESCRIPTOR, 358 ServiceProviderConfig.SERVICE_PROVIDER_CONFIG_RESOURCE_FACTORY); 359 360 // The ServiceProviderConfig is a special case where there is only a 361 // single resource at the endpoint, so the id is not specified. 362 return endpoint.get(null); 363 } 364 365 366 367 /** 368 * Invoke a bulk request. The service provider will perform as 369 * many operations as possible without regard to the number of failures. 370 * 371 * @param operations The operations to be performed. 372 * 373 * @return The bulk response. 374 * 375 * @throws SCIMException If the request fails. 376 */ 377 public BulkResponse processBulkRequest( 378 final List<BulkOperation> operations) 379 throws SCIMException 380 { 381 return processBulkRequest(operations, -1); 382 } 383 384 385 386 /** 387 * Invoke a bulk request. 388 * 389 * @param operations The operations to be performed. 390 * @param failOnErrors The number of errors that the service provider will 391 * accept before the operation is terminated and an 392 * error response is returned. A value of -1 indicates 393 * the the service provider will continue to perform 394 * as many operations as possible without regard to 395 * failures. 396 * 397 * @return The bulk response. 398 * 399 * @throws SCIMException If the request fails. 400 */ 401 public BulkResponse processBulkRequest( 402 final List<BulkOperation> operations, 403 final int failOnErrors) 404 throws SCIMException 405 { 406 final BulkEndpoint request = new BulkEndpoint(this, client); 407 408 return request.processRequest(operations, failOnErrors); 409 } 410 411 412 413 /** 414 * Retrieves the SCIM Service Provider URL. 415 * 416 * @return The SCIM Service Provider URL. 417 */ 418 public URI getBaseURL() { 419 return baseURL; 420 } 421 422 /** 423 * Retrieves the content media type that should be used when writing data to 424 * the SCIM service provider. 425 * 426 * @return The content media type that should be used when writing data to 427 * the SCIM service provider. 428 */ 429 public MediaType getContentType() { 430 return contentType; 431 } 432 433 /** 434 * Sets the content media type that should be used when writing data to 435 * the SCIM service provider. 436 * 437 * @param contentType he content media type that should be used when writing 438 * data to the SCIM service provider. 439 */ 440 public void setContentType(final MediaType contentType) { 441 this.contentType = contentType; 442 } 443 444 /** 445 * Retrieves the accept media type that should be used when reading data from 446 * the SCIM service provider. 447 * 448 * @return The accept media type that should be used when reading data from 449 * the SCIM service provider. 450 */ 451 public MediaType getAcceptType() { 452 return acceptType; 453 } 454 455 /** 456 * Sets the accept media type that should be used when reading data from 457 * the SCIM service provider. 458 * 459 * @param acceptType The accept media type that should be used when reading 460 * data from the SCIM service provider. 461 */ 462 public void setAcceptType(final MediaType acceptType) { 463 this.acceptType = acceptType; 464 } 465 466 /** 467 * Retrieves the user-agent string that will be used in the HTTP request 468 * headers. 469 * 470 * @return The user-agent string. This may be null, in which case a default 471 * user-agent will be used. 472 */ 473 public String getUserAgent() { 474 return userAgent; 475 } 476 477 /** 478 * Sets the user-agent string to use in the request headers. 479 * 480 * @param userAgent The user-agent string that should be used. 481 */ 482 public void setUserAgent(final String userAgent) { 483 this.userAgent = userAgent; 484 } 485 486 /** 487 * Whether to override DELETE operations with POST. 488 * 489 * @return <code>true</code> to override DELETE operations with POST or 490 * <code>false</code> to use the DELETE method. 491 */ 492 public boolean isOverrideDelete() { 493 return overrides[2]; 494 } 495 496 /** 497 * Sets whether to override DELETE operations with POST. 498 * 499 * @param overrideDelete <code>true</code> to override DELETE operations with 500 * POST or <code>false</code> to use the DELETE method. 501 */ 502 public void setOverrideDelete(final boolean overrideDelete) { 503 this.overrides[2] = overrideDelete; 504 } 505 506 /** 507 * Whether to override PATCH operations with POST. 508 * 509 * @return <code>true</code> to override PATCH operations with POST or 510 * <code>false</code> to use the PATCH method. 511 */ 512 public boolean isOverridePatch() { 513 return overrides[1]; 514 } 515 516 /** 517 * Sets whether to override PATCH operations with POST. 518 * 519 * @param overridePatch <code>true</code> to override PATCH operations with 520 * POST or <code>false</code> to use the PATCH method. 521 */ 522 public void setOverridePatch(final boolean overridePatch) { 523 this.overrides[1] = overridePatch; 524 } 525 526 /** 527 * Whether to override PUT operations with POST. 528 * 529 * @return <code>true</code> to override PUT operations with POST or 530 * <code>false</code> to use the PUT method. 531 */ 532 public boolean isOverridePut() { 533 return overrides[0]; 534 } 535 536 /** 537 * Sets whether to override PUT operations with POST. 538 * 539 * @param overridePut <code>true</code> to override PUT operations with 540 * POST or <code>false</code> to use the PUT method. 541 */ 542 public void setOverridePut(final boolean overridePut) { 543 this.overrides[0] = overridePut; 544 } 545 546 /** 547 * Whether to use URL suffix to specify the desired response data format 548 * (ie. .json or .xml) instead of using the HTTP Accept Header. 549 * 550 * @return {@code true} to use URL suffix to specify the desired response 551 * data format or {@code false} to use the HTTP Accept Header. 552 */ 553 public boolean isUseUrlSuffix() 554 { 555 return useUrlSuffix; 556 } 557 558 /** 559 * Sets whether to use URL suffix to specify the desired response data format 560 * (ie. .json or .xml) instead of using the HTTP Accept Header. 561 * 562 * @param useUrlSuffix {@code true} to use URL suffix to specify the desired 563 * response data format or {@code false} to use the HTTP 564 * Accept Header. 565 */ 566 public void setUseUrlSuffix(final boolean useUrlSuffix) 567 { 568 this.useUrlSuffix = useUrlSuffix; 569 } 570 571 /** 572 * Create a new ClientConfig with the default settings. 573 * 574 * @return A new ClientConfig with the default settings. 575 */ 576 private static ClientConfig createDefaultClientConfig() { 577 final PoolingHttpClientConnectionManager mgr = 578 new PoolingHttpClientConnectionManager(); 579 mgr.setMaxTotal(100); 580 mgr.setDefaultMaxPerRoute(100); 581 582 ClientConfig jerseyConfig = new ClientConfig(); 583 ApacheConnectorProvider connectorProvider = new ApacheConnectorProvider(); 584 jerseyConfig.connectorProvider(connectorProvider); 585 return jerseyConfig; 586 } 587 588 /** 589 * Create a new BasicCredentialsProvider with the provided credentials. 590 * 591 * @param username The username. 592 * @param password The password. 593 * @return A new BasicCredentialsProvider. 594 */ 595 private static BasicCredentialsProvider createBasicCredentialsProvider( 596 final String username, final String password) 597 { 598 BasicCredentialsProvider provider = new BasicCredentialsProvider(); 599 provider.setCredentials( 600 AuthScope.ANY, 601 new UsernamePasswordCredentials(username, password) 602 ); 603 return provider; 604 } 605}