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}