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.wink;
019
020import com.unboundid.scim.data.BaseResource;
021import com.unboundid.scim.marshal.Unmarshaller;
022import com.unboundid.scim.marshal.json.JsonUnmarshaller;
023import com.unboundid.scim.marshal.xml.XmlUnmarshaller;
024import com.unboundid.scim.schema.ResourceDescriptor;
025import com.unboundid.scim.sdk.AttributePath;
026import com.unboundid.scim.sdk.Debug;
027import com.unboundid.scim.sdk.DeleteResourceRequest;
028import com.unboundid.scim.sdk.ForbiddenException;
029import com.unboundid.scim.sdk.GetResourceRequest;
030import com.unboundid.scim.sdk.GetResourcesRequest;
031import com.unboundid.scim.sdk.InvalidResourceException;
032import com.unboundid.scim.sdk.NotModifiedException;
033import com.unboundid.scim.sdk.OAuthToken;
034import com.unboundid.scim.sdk.OAuthTokenHandler;
035import com.unboundid.scim.sdk.OAuthTokenStatus;
036import com.unboundid.scim.sdk.PageParameters;
037import com.unboundid.scim.sdk.PatchResourceRequest;
038import com.unboundid.scim.sdk.PostResourceRequest;
039import com.unboundid.scim.sdk.PreconditionFailedException;
040import com.unboundid.scim.sdk.PutResourceRequest;
041import com.unboundid.scim.sdk.ResourceNotFoundException;
042import com.unboundid.scim.sdk.ResourceSchemaBackend;
043import com.unboundid.scim.sdk.Resources;
044import com.unboundid.scim.sdk.SCIMBackend;
045import com.unboundid.scim.sdk.SCIMException;
046import com.unboundid.scim.sdk.SCIMFilter;
047import com.unboundid.scim.sdk.SCIMQueryAttributes;
048import com.unboundid.scim.sdk.SCIMRequest;
049import com.unboundid.scim.sdk.SortParameters;
050import com.unboundid.scim.sdk.UnauthorizedException;
051
052
053import javax.ws.rs.core.HttpHeaders;
054import javax.ws.rs.core.MediaType;
055import javax.ws.rs.core.Response;
056import java.io.InputStream;
057import java.util.List;
058import java.util.concurrent.atomic.AtomicReference;
059
060import static com.unboundid.scim.sdk.SCIMConstants.QUERY_PARAMETER_ATTRIBUTES;
061import static com.unboundid.scim.sdk.SCIMConstants.RESOURCE_ENDPOINT_SCHEMAS;
062
063
064/**
065 * This class is an abstract Wink resource implementation for
066 * SCIM operations on a SCIM endpoint.
067 */
068public abstract class AbstractSCIMResource extends AbstractStaticResource
069{
070  private final SCIMApplication application;
071
072  /**
073   * The OAuth 2.0 bearer token handler. This may be null.
074   */
075  private final OAuthTokenHandler tokenHandler;
076
077  private final ResourceSchemaBackend resourceSchemaBackend;
078
079  /**
080   * Create a new AbstractSCIMResource for CRUD operations.
081   *
082   * @param application         The SCIM JAX-RS application associated with this
083   *                            resource.
084   * @param tokenHandler        The token handler to use for OAuth
085   *                            authentication.
086   */
087  public AbstractSCIMResource(final SCIMApplication application,
088                              final OAuthTokenHandler tokenHandler)
089  {
090    this.application = application;
091    this.tokenHandler = tokenHandler;
092    this.resourceSchemaBackend = new ResourceSchemaBackend(application);
093  }
094
095
096
097  /**
098   * Process a GET operation.
099   *
100   * @param requestContext The request context.
101   * @param endpoint       The endpoint requested.
102   * @param userID         The user ID requested.
103   *
104   * @return  The response to the operation.
105   */
106  Response getUser(final RequestContext requestContext,
107                   final String endpoint, final String userID)
108  {
109    SCIMBackend backend;
110    ResourceDescriptor resourceDescriptor = null;
111    Response.ResponseBuilder responseBuilder;
112    try {
113      backend = getBackend(endpoint);
114      resourceDescriptor = backend.getResourceDescriptor(endpoint);
115      if(resourceDescriptor == null)
116      {
117        throw new ResourceNotFoundException(
118                endpoint + " is not a valid resource endpoint");
119      }
120      String authID = requestContext.getAuthID();
121      if(authID == null && tokenHandler == null) {
122        throw new UnauthorizedException("Invalid credentials");
123      }
124      final String attributes =
125          requestContext.getUriInfo().getQueryParameters().getFirst(
126              QUERY_PARAMETER_ATTRIBUTES);
127      final SCIMQueryAttributes queryAttributes =
128          new SCIMQueryAttributes(resourceDescriptor, attributes);
129
130      // Process the request.
131      GetResourceRequest getResourceRequest =
132          new GetResourceRequest(requestContext.getUriInfo().getBaseUri(),
133              authID, resourceDescriptor, userID, queryAttributes,
134              requestContext.getRequest());
135
136      if (authID == null)
137      {
138        AtomicReference<String> authIDRef = new AtomicReference<String>();
139        Response response = validateOAuthToken(requestContext,
140                              getResourceRequest, authIDRef, tokenHandler);
141        if (response != null)
142        {
143          application.getStatsForResource(resourceDescriptor.getName()).
144              incrementStat("get-" + response.getStatus());
145          return response;
146        }
147        else
148        {
149          authID = authIDRef.get();
150          getResourceRequest =
151               new GetResourceRequest(requestContext.getUriInfo().getBaseUri(),
152                   authID, resourceDescriptor, userID, queryAttributes,
153                   requestContext.getRequest());
154        }
155      }
156
157      BaseResource resource =
158          backend.getResource(getResourceRequest);
159
160      // Build the response.
161      responseBuilder = Response.status(Response.Status.OK);
162        setResponseEntity(responseBuilder, requestContext.getProduceMediaType(),
163            resource);
164        application.getStatsForResource(resourceDescriptor.getName()).
165          incrementStat(ResourceStats.GET_OK);
166      responseBuilder.contentLocation(resource.getMeta().getLocation());
167      // cant use responsebuilder.tag ... it will quote the
168      // already quoted string
169      responseBuilder.header(HttpHeaders.ETAG, resource.getMeta().getVersion());
170
171      if(requestContext.getProduceMediaType() ==
172          MediaType.APPLICATION_JSON_TYPE)
173      {
174        application.getStatsForResource(resourceDescriptor.getName()).
175            incrementStat(ResourceStats.GET_RESPONSE_JSON);
176      }
177      else if(requestContext.getProduceMediaType() ==
178              MediaType.APPLICATION_XML_TYPE)
179      {
180        application.getStatsForResource(resourceDescriptor.getName()).
181            incrementStat(ResourceStats.GET_RESPONSE_XML);
182      }
183    } catch (SCIMException e) {
184      Debug.debugException(e);
185      responseBuilder = error(e, requestContext);
186      if(resourceDescriptor != null)
187      {
188        application.getStatsForResource(resourceDescriptor.getName()).
189            incrementStat("get-" + e.getStatusCode());
190      }
191    }
192
193    return responseBuilder.build();
194  }
195
196
197
198  /**
199   * Process a GET operation.
200   *
201   * @param requestContext   The request context.
202   * @param endpoint         The endpoint requested.
203   * @param filterString     The filter query parameter, or {@code null}.
204   * @param baseID           The SCIM resource ID of the search base entry,
205   *                         or {@code null}.
206   * @param searchScope      The LDAP search scope to use, or {@code null}.
207   * @param sortBy           The sortBy query parameter, or {@code null}.
208   * @param sortOrder        The sortOrder query parameter, or {@code null}.
209   * @param pageStartIndex   The startIndex query parameter, or {@code null}.
210   * @param pageSize         The count query parameter, or {@code null}.
211   *
212   * @return  The response to the operation.
213   */
214  protected Response getUsers(final RequestContext requestContext,
215                              final String endpoint,
216                              final String filterString,
217                              final String baseID,
218                              final String searchScope,
219                              final String sortBy,
220                              final String sortOrder,
221                              final String pageStartIndex,
222                              final String pageSize)
223  {
224    SCIMBackend backend;
225    ResourceDescriptor resourceDescriptor = null;
226    Response.ResponseBuilder responseBuilder;
227    try
228    {
229      backend = getBackend(endpoint);
230      resourceDescriptor = backend.getResourceDescriptor(endpoint);
231      if(resourceDescriptor == null)
232      {
233        throw new ResourceNotFoundException(
234                endpoint + " is not a valid resource endpoint");
235      }
236      String authID = requestContext.getAuthID();
237      if(authID == null && tokenHandler == null) {
238        throw new UnauthorizedException("Invalid credentials");
239      }
240      final String attributes =
241          requestContext.getUriInfo().getQueryParameters().getFirst(
242              QUERY_PARAMETER_ATTRIBUTES);
243      final SCIMQueryAttributes queryAttributes =
244          new SCIMQueryAttributes(resourceDescriptor, attributes);
245
246      // Parse the filter parameters.
247      final SCIMFilter filter = parseFilter(filterString, resourceDescriptor);
248
249      // Parse the sort parameters.
250      final SortParameters sortParameters;
251      if (sortBy != null && !sortBy.isEmpty())
252      {
253        sortParameters =
254            new SortParameters(AttributePath.parse(sortBy,
255                                resourceDescriptor.getSchema()), sortOrder);
256      }
257      else
258      {
259        sortParameters = null;
260      }
261
262      // Parse the pagination parameters.
263      int startIndex = -1;
264      int count = -1;
265      if (pageStartIndex != null && !pageStartIndex.isEmpty())
266      {
267        try
268        {
269          startIndex = Integer.parseInt(pageStartIndex);
270        }
271        catch (NumberFormatException e)
272        {
273          Debug.debugException(e);
274          throw new InvalidResourceException(
275              "The pagination startIndex value '" + pageStartIndex +
276              "' is not parsable");
277        }
278
279        if (startIndex <= 0)
280        {
281          throw new InvalidResourceException(
282              "The pagination startIndex value '" + pageStartIndex +
283              "' is invalid because it is not greater than zero");
284        }
285      }
286      if (pageSize != null && !pageSize.isEmpty())
287      {
288        try
289        {
290          count = Integer.parseInt(pageSize);
291        }
292        catch (NumberFormatException e)
293        {
294          Debug.debugException(e);
295          throw new InvalidResourceException(
296              "The pagination count value '" + pageSize +
297              "' is not parsable");
298        }
299
300        if (count <= 0)
301        {
302          throw new InvalidResourceException(
303              "The pagination count value '" + pageSize +
304              "' is invalid because it is not greater than zero");
305        }
306      }
307
308      final PageParameters pageParameters;
309      if (startIndex >= 0 && count >= 0)
310      {
311        pageParameters = new PageParameters(startIndex, count);
312      }
313      else if (startIndex >= 0)
314      {
315        pageParameters = new PageParameters(startIndex, 0);
316      }
317      else if (count >= 0)
318      {
319        pageParameters = new PageParameters(1, count);
320      }
321      else
322      {
323        pageParameters = null;
324      }
325
326      // Process the request.
327      GetResourcesRequest getResourcesRequest =
328          new GetResourcesRequest(requestContext.getUriInfo().getBaseUri(),
329              authID, resourceDescriptor, filter, baseID, searchScope,
330              sortParameters, pageParameters, queryAttributes,
331              requestContext.getRequest());
332
333      if (authID == null)
334      {
335        AtomicReference<String> authIDRef = new AtomicReference<String>();
336        Response response = validateOAuthToken(requestContext,
337                              getResourcesRequest, authIDRef, tokenHandler);
338        if (response != null)
339        {
340          application.getStatsForResource(resourceDescriptor.getName()).
341              incrementStat("query-" + response.getStatus());
342          return response;
343        }
344        else
345        {
346          authID = authIDRef.get();
347          getResourcesRequest =
348              new GetResourcesRequest(requestContext.getUriInfo().getBaseUri(),
349                      authID, resourceDescriptor, filter, baseID, searchScope,
350                      sortParameters, pageParameters, queryAttributes,
351                      requestContext.getRequest());
352        }
353      }
354
355      final Resources resources = backend.getResources(getResourcesRequest);
356
357      // Build the response.
358      responseBuilder =
359          Response.status(Response.Status.OK);
360      setResponseEntity(responseBuilder, requestContext.getProduceMediaType(),
361                        resources);
362
363      application.getStatsForResource(resourceDescriptor.getName()).
364          incrementStat(ResourceStats.QUERY_OK);
365      if(requestContext.getProduceMediaType() ==
366          MediaType.APPLICATION_JSON_TYPE)
367      {
368        application.getStatsForResource(resourceDescriptor.getName()).
369            incrementStat(ResourceStats.QUERY_RESPONSE_JSON);
370      }
371      else if(requestContext.getProduceMediaType() ==
372              MediaType.APPLICATION_XML_TYPE)
373      {
374        application.getStatsForResource(resourceDescriptor.getName()).
375            incrementStat(ResourceStats.QUERY_RESPONSE_XML);
376      }
377    }
378    catch(SCIMException e)
379    {
380      responseBuilder =
381          Response.status(e.getStatusCode());
382      setResponseEntity(responseBuilder, requestContext.getProduceMediaType(),
383                        e);
384      if(resourceDescriptor != null)
385      {
386        application.getStatsForResource(resourceDescriptor.getName()).
387            incrementStat("query-" + e.getStatusCode());
388      }
389    }
390
391    return responseBuilder.build();
392  }
393
394
395  /**
396   * Process a POST operation.
397   *
398   * @param requestContext    The request context.
399   * @param endpoint       The endpoint requested.
400   * @param inputStream       The content to be consumed.
401   *
402   * @return  The response to the operation.
403   */
404  Response postUser(final RequestContext requestContext,
405                    final String endpoint,
406                    final InputStream inputStream)
407  {
408    SCIMBackend backend;
409    ResourceDescriptor resourceDescriptor = null;
410    Response.ResponseBuilder responseBuilder;
411    try
412    {
413      backend = getBackend(endpoint);
414      resourceDescriptor = backend.getResourceDescriptor(endpoint);
415      if(resourceDescriptor == null)
416      {
417        throw new ResourceNotFoundException(
418                endpoint + " is not a valid resource endpoint");
419      }
420      final Unmarshaller unmarshaller;
421      if (requestContext.getConsumeMediaType().equals(
422          MediaType.APPLICATION_JSON_TYPE))
423      {
424        unmarshaller = new JsonUnmarshaller();
425        application.getStatsForResource(resourceDescriptor.getName()).
426            incrementStat(ResourceStats.POST_CONTENT_JSON);
427      }
428      else
429      {
430        unmarshaller = new XmlUnmarshaller();
431        application.getStatsForResource(resourceDescriptor.getName()).
432            incrementStat(ResourceStats.POST_CONTENT_XML);
433      }
434      String authID = requestContext.getAuthID();
435      if(authID == null && tokenHandler == null)
436      {
437        throw new UnauthorizedException("Invalid credentials");
438      }
439
440      // Parse the resource.
441      final BaseResource postedResource = unmarshaller.unmarshal(
442          inputStream, resourceDescriptor, BaseResource.BASE_RESOURCE_FACTORY);
443
444      final String attributes =
445          requestContext.getUriInfo().getQueryParameters().getFirst(
446              QUERY_PARAMETER_ATTRIBUTES);
447      final SCIMQueryAttributes queryAttributes =
448          new SCIMQueryAttributes(resourceDescriptor, attributes);
449
450      // Process the request.
451      PostResourceRequest postResourceRequest =
452          new PostResourceRequest(requestContext.getUriInfo().getBaseUri(),
453              authID, resourceDescriptor, postedResource.getScimObject(),
454              queryAttributes, requestContext.getRequest());
455
456      if (authID == null)
457      {
458        AtomicReference<String> authIDRef = new AtomicReference<String>();
459        Response response = validateOAuthToken(requestContext,
460                              postResourceRequest, authIDRef, tokenHandler);
461        if (response != null)
462        {
463          application.getStatsForResource(resourceDescriptor.getName()).
464              incrementStat("post-" + response.getStatus());
465          return response;
466        }
467        else
468        {
469          authID = authIDRef.get();
470          postResourceRequest =
471              new PostResourceRequest(requestContext.getUriInfo().getBaseUri(),
472                    authID, resourceDescriptor, postedResource.getScimObject(),
473                    queryAttributes, requestContext.getRequest());
474        }
475      }
476
477      final BaseResource resource = backend.postResource(postResourceRequest);
478      // Build the response.
479      responseBuilder = Response.status(Response.Status.CREATED);
480      setResponseEntity(responseBuilder, requestContext.getProduceMediaType(),
481          resource);
482      responseBuilder.location(resource.getMeta().getLocation());
483      // cant use responsebuilder.tag ... it will quote the already
484      // quoted string
485      responseBuilder.header(HttpHeaders.ETAG, resource.getMeta().getVersion());
486      application.getStatsForResource(resourceDescriptor.getName()).
487          incrementStat(ResourceStats.POST_OK);
488      if(requestContext.getProduceMediaType() ==
489          MediaType.APPLICATION_JSON_TYPE)
490      {
491        application.getStatsForResource(resourceDescriptor.getName()).
492            incrementStat(ResourceStats.POST_RESPONSE_JSON);
493      }
494      else if(requestContext.getProduceMediaType() ==
495              MediaType.APPLICATION_XML_TYPE)
496      {
497        application.getStatsForResource(resourceDescriptor.getName()).
498            incrementStat(ResourceStats.POST_RESPONSE_XML);
499      }
500    } catch (SCIMException e) {
501      Debug.debugException(e);
502      responseBuilder = error(e, requestContext);
503      if(resourceDescriptor != null)
504      {
505        application.getStatsForResource(resourceDescriptor.getName()).
506            incrementStat("post-" + e.getStatusCode());
507      }
508    }
509
510    return responseBuilder.build();
511  }
512
513
514
515  /**
516   * Process a PUT operation.
517   *
518   * @param requestContext    The request context.
519   * @param endpoint          The endpoint requested.
520   * @param userID            The target user ID.
521   * @param inputStream       The content to be consumed.
522   *
523   * @return  The response to the operation.
524   */
525  Response putUser(final RequestContext requestContext,
526                   final String endpoint,
527                   final String userID,
528                   final InputStream inputStream)
529  {
530    SCIMBackend backend;
531    ResourceDescriptor resourceDescriptor = null;
532    Response.ResponseBuilder responseBuilder;
533    try {
534      backend = getBackend(endpoint);
535      resourceDescriptor = backend.getResourceDescriptor(endpoint);
536      if(resourceDescriptor == null)
537      {
538        throw new ResourceNotFoundException(
539                endpoint + " is not a valid resource endpoint");
540      }
541      final Unmarshaller unmarshaller;
542      if (requestContext.getConsumeMediaType().equals(
543          MediaType.APPLICATION_JSON_TYPE))
544      {
545        unmarshaller = new JsonUnmarshaller();
546        application.getStatsForResource(resourceDescriptor.getName()).
547            incrementStat(ResourceStats.PUT_CONTENT_JSON);
548      }
549      else
550      {
551        unmarshaller = new XmlUnmarshaller();
552        application.getStatsForResource(resourceDescriptor.getName()).
553            incrementStat(ResourceStats.PUT_CONTENT_XML);
554      }
555      String authID = requestContext.getAuthID();
556      if(authID == null && tokenHandler == null)
557      {
558        throw new UnauthorizedException("Invalid credentials");
559      }
560
561      // Parse the resource.
562      final BaseResource puttedResource = unmarshaller.unmarshal(
563          inputStream, resourceDescriptor, BaseResource.BASE_RESOURCE_FACTORY);
564
565      final String attributes =
566          requestContext.getUriInfo().getQueryParameters().getFirst(
567              QUERY_PARAMETER_ATTRIBUTES);
568      final SCIMQueryAttributes queryAttributes =
569          new SCIMQueryAttributes(resourceDescriptor, attributes);
570
571      // Process the request.
572      PutResourceRequest putResourceRequest =
573          new PutResourceRequest(requestContext.getUriInfo().getBaseUri(),
574              authID, resourceDescriptor, userID,
575              puttedResource.getScimObject(), queryAttributes,
576              requestContext.getRequest());
577
578      if (authID == null)
579      {
580        AtomicReference<String> authIDRef = new AtomicReference<String>();
581        Response response = validateOAuthToken(requestContext,
582                              putResourceRequest, authIDRef, tokenHandler);
583        if (response != null)
584        {
585          application.getStatsForResource(resourceDescriptor.getName()).
586              incrementStat("put-" + response.getStatus());
587          return response;
588        }
589        else
590        {
591          authID = authIDRef.get();
592          putResourceRequest =
593             new PutResourceRequest(requestContext.getUriInfo().getBaseUri(),
594                  authID, resourceDescriptor, userID,
595                  puttedResource.getScimObject(), queryAttributes,
596                  requestContext.getRequest());
597        }
598      }
599
600      final BaseResource scimResponse = backend.putResource(putResourceRequest);
601      // Build the response.
602      responseBuilder = Response.status(Response.Status.OK);
603      setResponseEntity(responseBuilder, requestContext.getProduceMediaType(),
604                        scimResponse);
605      responseBuilder.contentLocation(scimResponse.getMeta().getLocation());
606      // cant use responsebuilder.tag ... it will quote the already
607      // quoted string
608      responseBuilder.header(HttpHeaders.ETAG,
609          scimResponse.getMeta().getVersion());
610      application.getStatsForResource(resourceDescriptor.getName()).
611          incrementStat(ResourceStats.PUT_OK);
612      if(requestContext.getProduceMediaType() ==
613          MediaType.APPLICATION_JSON_TYPE)
614      {
615        application.getStatsForResource(resourceDescriptor.getName()).
616            incrementStat(ResourceStats.PUT_RESPONSE_JSON);
617      }
618      else if(requestContext.getProduceMediaType() ==
619              MediaType.APPLICATION_XML_TYPE)
620      {
621        application.getStatsForResource(resourceDescriptor.getName()).
622            incrementStat(ResourceStats.PUT_RESPONSE_XML);
623      }
624    } catch (SCIMException e) {
625      Debug.debugException(e);
626      responseBuilder = error(e, requestContext);
627      if(resourceDescriptor != null)
628      {
629        application.getStatsForResource(resourceDescriptor.getName()).
630            incrementStat("put-" + e.getStatusCode());
631      }
632    }
633
634    return responseBuilder.build();
635  }
636
637
638
639  /**
640   * Process a PATCH operation.
641   *
642   * @param requestContext    The request context.
643   * @param endpoint          The endpoint requested.
644   * @param userID            The target user ID.
645   * @param inputStream       The content to be consumed.
646   *
647   * @return  The response to the operation.
648   */
649  Response patchUser(final RequestContext requestContext,
650                     final String endpoint,
651                     final String userID,
652                     final InputStream inputStream)
653  {
654    SCIMBackend backend;
655    ResourceDescriptor resourceDescriptor = null;
656    Response.ResponseBuilder responseBuilder;
657    try {
658      backend = getBackend(endpoint);
659      resourceDescriptor = backend.getResourceDescriptor(endpoint);
660      if(resourceDescriptor == null)
661      {
662        throw new ResourceNotFoundException(
663                endpoint + " is not a valid resource endpoint");
664      }
665      String authID = requestContext.getAuthID();
666      if(authID == null && tokenHandler == null)
667      {
668        throw new UnauthorizedException("Invalid credentials");
669      }
670      final Unmarshaller unmarshaller;
671      if (requestContext.getConsumeMediaType().equals(
672              MediaType.APPLICATION_JSON_TYPE))
673      {
674        unmarshaller = new JsonUnmarshaller();
675        application.getStatsForResource(resourceDescriptor.getName()).
676            incrementStat(ResourceStats.PATCH_CONTENT_JSON);
677      }
678      else
679      {
680        unmarshaller = new XmlUnmarshaller();
681        application.getStatsForResource(resourceDescriptor.getName()).
682            incrementStat(ResourceStats.PATCH_CONTENT_XML);
683      }
684      // Parse the resource.
685      final BaseResource patchedResource = unmarshaller.unmarshal(
686           inputStream, resourceDescriptor, BaseResource.BASE_RESOURCE_FACTORY);
687
688      final String attributes =
689              requestContext.getUriInfo().getQueryParameters().getFirst(
690                      QUERY_PARAMETER_ATTRIBUTES);
691      final SCIMQueryAttributes queryAttributes =
692              new SCIMQueryAttributes(resourceDescriptor, attributes);
693
694      // Process the request.
695      PatchResourceRequest patchResourceRequest =
696              new PatchResourceRequest(requestContext.getUriInfo().getBaseUri(),
697                      authID, resourceDescriptor, userID,
698                      patchedResource.getScimObject(),
699                      queryAttributes, requestContext.getRequest());
700
701      if (authID == null)
702      {
703        AtomicReference<String> authIDRef = new AtomicReference<String>();
704        Response response = validateOAuthToken(requestContext,
705                              patchResourceRequest, authIDRef, tokenHandler);
706        if (response != null)
707        {
708          application.getStatsForResource(resourceDescriptor.getName()).
709              incrementStat("patch-" + response.getStatus());
710          return response;
711        }
712        else
713        {
714          authID = authIDRef.get();
715          patchResourceRequest =
716             new PatchResourceRequest(requestContext.getUriInfo().getBaseUri(),
717                   authID, resourceDescriptor, userID,
718                   patchedResource.getScimObject(), queryAttributes,
719                   requestContext.getRequest());
720        }
721      }
722
723      final BaseResource scimResponse =
724              backend.patchResource(patchResourceRequest);
725
726      // Build the response.
727      if (!queryAttributes.allAttributesRequested())
728      {
729        responseBuilder = Response.status(Response.Status.OK);
730        setResponseEntity(responseBuilder, requestContext.getProduceMediaType(),
731                scimResponse);
732      }
733      else
734      {
735        responseBuilder = Response.status(Response.Status.NO_CONTENT);
736      }
737      responseBuilder.contentLocation(scimResponse.getMeta().getLocation());
738      // cant use responsebuilder.tag ... it will quote the already
739      // quoted string
740      responseBuilder.header(HttpHeaders.ETAG,
741          scimResponse.getMeta().getVersion());
742
743      application.getStatsForResource(resourceDescriptor.getName()).
744          incrementStat(ResourceStats.PATCH_OK);
745      if(requestContext.getProduceMediaType() ==
746          MediaType.APPLICATION_JSON_TYPE)
747      {
748        application.getStatsForResource(resourceDescriptor.getName()).
749            incrementStat(ResourceStats.PATCH_RESPONSE_JSON);
750      }
751      else if(requestContext.getProduceMediaType() ==
752              MediaType.APPLICATION_XML_TYPE)
753      {
754        application.getStatsForResource(resourceDescriptor.getName()).
755            incrementStat(ResourceStats.PATCH_RESPONSE_XML);
756      }
757    } catch (SCIMException e) {
758      Debug.debugException(e);
759      responseBuilder = error(e, requestContext);
760      if(resourceDescriptor != null)
761      {
762        application.getStatsForResource(resourceDescriptor.getName()).
763            incrementStat("patch-" + e.getStatusCode());
764      }
765    }
766
767    return responseBuilder.build();
768  }
769
770
771
772  /**
773   * Process a DELETE operation.
774   *
775   * @param requestContext    The request context.
776   * @param endpoint          The endpoint requested.
777   * @param userID            The target user ID.
778   *
779   * @return  The response to the operation.
780   */
781  Response deleteUser(final RequestContext requestContext,
782                      final String endpoint,
783                      final String userID)
784  {
785    SCIMBackend backend;
786    ResourceDescriptor resourceDescriptor = null;
787    // Process the request.
788    Response.ResponseBuilder responseBuilder;
789    try {
790      backend = getBackend(endpoint);
791      resourceDescriptor = backend.getResourceDescriptor(endpoint);
792      if(resourceDescriptor == null)
793      {
794        throw new ResourceNotFoundException(
795                endpoint + " is not a valid resource endpoint");
796      }
797      String authID = requestContext.getAuthID();
798      if(authID == null && tokenHandler == null)
799      {
800        throw new UnauthorizedException("Invalid credentials");
801      }
802
803      DeleteResourceRequest deleteResourceRequest =
804        new DeleteResourceRequest(requestContext.getUriInfo().getBaseUri(),
805            authID, resourceDescriptor, userID,
806            requestContext.getRequest());
807
808      if (authID == null)
809      {
810        AtomicReference<String> authIDRef = new AtomicReference<String>();
811        Response response = validateOAuthToken(requestContext,
812                              deleteResourceRequest, authIDRef, tokenHandler);
813        if (response != null)
814        {
815          application.getStatsForResource(resourceDescriptor.getName()).
816              incrementStat("delete-" + response.getStatus());
817          return response;
818        }
819        else
820        {
821          authID = authIDRef.get();
822          deleteResourceRequest =
823             new DeleteResourceRequest(requestContext.getUriInfo().getBaseUri(),
824                   authID, resourceDescriptor, userID,
825                   requestContext.getRequest());
826        }
827      }
828
829      backend.deleteResource(deleteResourceRequest);
830      // Build the response.
831      responseBuilder = Response.status(Response.Status.OK);
832      application.getStatsForResource(resourceDescriptor.getName()).
833          incrementStat(ResourceStats.DELETE_OK);
834    } catch (SCIMException e) {
835      Debug.debugException(e);
836      responseBuilder = error(e, requestContext);
837      if(resourceDescriptor != null)
838      {
839        application.getStatsForResource(resourceDescriptor.getName()).
840            incrementStat("delete-" + e.getStatusCode());
841      }
842    }
843
844    return responseBuilder.build();
845  }
846
847  /**
848   * Handles OAuth bearer token validation. This method should only be called if
849   * 1) the request was not previously authenticated with HTTP Basic Auth, and
850   * 2) we have a OAuthTokenHandler available.
851   * <p>
852   * It will call into the OAuthTokenHandler implementation to validate the
853   * bearer token and then do one of two things:
854   * <ul>
855   *   <li>If the token is valid, it will set the output parameter 'authIDRef'
856   *       to the DN of the authorization entry and return null.</li>
857   *   <li>If the token is invalid, it will construct a Http Response with the
858   *       appropriate headers and return it.</li>
859   * </ul>
860   *
861   * @param context      The incoming HTTP request context.
862   * @param request      The unmarshalled SCIM Request for passing into the
863   *                     token handler.
864   * @param authIDRef    An output parameter to contain the DN of the
865   *                     authorization entry.
866   * @param tokenHandlerImpl The OAuthTokenHandler to use.
867   * @return             {@code null} if the token was successfully validated,
868   *                     otherwise a Response instance containing the error
869   *                     information.
870   */
871  static Response validateOAuthToken(final RequestContext context,
872                                     final SCIMRequest request,
873                                     final AtomicReference<String> authIDRef,
874                                     final OAuthTokenHandler tokenHandlerImpl)
875  {
876    HttpHeaders headers = context.getHeaders();
877    List<String> headerList = headers.getRequestHeader("Authorization");
878
879    if (headerList == null || headerList.isEmpty())
880    {
881      //If the client lacks any authentication information, just return 401
882      Response.ResponseBuilder builder = Response.status(401);
883      builder.header("WWW-Authenticate", "Bearer realm=\"SCIM\"");
884      return builder.build();
885
886    }
887    else if (headerList.size() > 1)
888    {
889      return invalidRequest("The Authorization header has too many values",
890              context.getProduceMediaType());
891    }
892
893    String header = headerList.get(0);
894    String[] authorization = header.split(" ");
895    if (authorization.length == 2 &&
896          authorization[0].equalsIgnoreCase("Bearer") &&
897            authorization[1].length() > 0)
898    {
899      try
900      {
901        OAuthToken token = tokenHandlerImpl.decodeOAuthToken(authorization[1]);
902
903        if (token == null)
904        {
905          return invalidRequest("Could not decode the access token",
906                  context.getProduceMediaType());
907        }
908
909        if (!tokenHandlerImpl.isTokenAuthentic(token))
910        {
911          return invalidToken("The access token is not authentic",
912                  context.getProduceMediaType());
913        }
914
915        if (!tokenHandlerImpl.isTokenForThisServer(token))
916        {
917          return invalidToken(
918                  "The access token is not intended for this server",
919                  context.getProduceMediaType());
920        }
921
922        if (tokenHandlerImpl.isTokenExpired(token))
923        {
924          return invalidToken("The access token is expired",
925                  context.getProduceMediaType());
926        }
927
928        OAuthTokenStatus status =
929                tokenHandlerImpl.validateToken(token, request);
930
931        if (status.getErrorCode().equals(
932                OAuthTokenStatus.ErrorCode.INVALID_TOKEN))
933        {
934          String errorDescription = status.getErrorDescription();
935          return invalidToken(errorDescription, context.getProduceMediaType());
936        }
937        else if (status.getErrorCode().equals(
938                OAuthTokenStatus.ErrorCode.INSUFFICIENT_SCOPE))
939        {
940          String errorDescription = status.getErrorDescription();
941          String scope = status.getScope();
942          return insufficientScope(scope, errorDescription,
943                  context.getProduceMediaType());
944        }
945
946        String authID = tokenHandlerImpl.getAuthzDN(token);
947        if (authID == null)
948        {
949          return invalidToken(
950                  "The access token did not contain an authorization DN",
951                  context.getProduceMediaType());
952        }
953        else
954        {
955          authIDRef.set(authID);
956          return null;
957        }
958      }
959      catch(Throwable t)
960      {
961        Debug.debugException(t);
962        return invalidRequest(t.getMessage(), context.getProduceMediaType());
963      }
964    }
965    else if(authorization.length == 2 &&
966              authorization[0].equalsIgnoreCase("Basic") &&
967                authorization[1].length() > 0)
968    {
969      //The client tried to do Basic Authentication, and since we made it here,
970      //it failed.
971      Response.ResponseBuilder builder = Response.status(401);
972      builder.header("WWW-Authenticate", "Basic realm=\"SCIM\"");
973      SCIMException exception = new UnauthorizedException(null);
974      setResponseEntity(builder, context.getProduceMediaType(), exception);
975      return builder.build();
976    }
977    else
978    {
979      return invalidRequest("The Authorization header was malformed",
980              context.getProduceMediaType());
981    }
982  }
983
984  /**
985   * Creates an invalid_request Response with the specified error description.
986   *
987   * @param errorDescription The description of the validation error.
988   * @param mediaType The accept-type for SCIMRequest.
989   * @return a Response instance.
990   */
991  private static Response invalidRequest(final String errorDescription,
992                                         final MediaType mediaType)
993  {
994    Response.ResponseBuilder builder = Response.status(400);
995    String authHeaderValue = "Bearer realm=\"SCIM\", error=\"invalid_request\"";
996    if (errorDescription != null && !errorDescription.isEmpty())
997    {
998      authHeaderValue += ", error_description=\"" + errorDescription + "\"";
999    }
1000    builder.header("WWW-Authenticate", authHeaderValue);
1001
1002    SCIMException exception = new InvalidResourceException(errorDescription);
1003    setResponseEntity(builder, mediaType, exception);
1004
1005    return builder.build();
1006  }
1007
1008  /**
1009   * Creates an invalid_token Response with the specified error description.
1010   *
1011   * @param errorDescription The description of the validation error.
1012   * @param mediaType The accept-type for SCIMRequest.
1013   * @return a Response instance.
1014   */
1015  private static Response invalidToken(final String errorDescription,
1016                                       final MediaType mediaType)
1017  {
1018    Response.ResponseBuilder builder = Response.status(401);
1019    String authHeaderValue = "Bearer realm=\"SCIM\", error=\"invalid_token\"";
1020    if (errorDescription != null && !errorDescription.isEmpty())
1021    {
1022      authHeaderValue += ", error_description=\"" + errorDescription + "\"";
1023    }
1024    builder.header("WWW-Authenticate", authHeaderValue);
1025
1026    SCIMException exception = new UnauthorizedException(errorDescription);
1027    setResponseEntity(builder, mediaType, exception);
1028
1029    return builder.build();
1030  }
1031
1032  /**
1033   * Creates an insufficient_scope Response with the specified error
1034   * description and scope.
1035   *
1036   * @param errorDescription The description of the validation error.
1037   * @param scope The OAuth scope required to access the target resource.
1038   * @param mediaType The accept-type for SCIMRequest.
1039   * @return a Response instance.
1040   */
1041  private static Response insufficientScope(final String scope,
1042                                            final String errorDescription,
1043                                            final MediaType mediaType)
1044  {
1045    Response.ResponseBuilder builder = Response.status(403);
1046    String authHeaderValue =
1047            "Bearer realm=\"SCIM\", error=\"insufficient_scope\"";
1048    if (errorDescription != null && !errorDescription.isEmpty())
1049    {
1050      authHeaderValue += ", error_description=\"" + errorDescription + "\"";
1051    }
1052    if (scope != null && !scope.isEmpty())
1053    {
1054      authHeaderValue += ", scope=\"" + scope + "\"";
1055    }
1056    builder.header("WWW-Authenticate", authHeaderValue);
1057
1058    SCIMException exception = new ForbiddenException(errorDescription);
1059    setResponseEntity(builder, mediaType, exception);
1060
1061    return builder.build();
1062  }
1063
1064  /**
1065   * Retrieves the backend that should service the provided endpoint.
1066   *
1067   * @param endpoint The endpoint requested.
1068   * @return The backend that should service the provided endpoint.
1069   */
1070  private SCIMBackend getBackend(final String endpoint)
1071  {
1072    if(endpoint.equals(RESOURCE_ENDPOINT_SCHEMAS))
1073    {
1074      return resourceSchemaBackend;
1075    }
1076
1077    return application.getBackend();
1078  }
1079
1080  /**
1081   * Returns a ResponseBuilder from the provided SCIMException and
1082   * RequestContext.
1083   *
1084   * @param e The SCIMException.
1085   * @param requestContext The request context.
1086   * @return The ResponseBuilder.
1087   */
1088  private Response.ResponseBuilder error(final SCIMException e,
1089                                         final RequestContext requestContext)
1090  {
1091    // Build the response.
1092    Response.ResponseBuilder responseBuilder =
1093        Response.status(e.getStatusCode());
1094    if(e instanceof NotModifiedException)
1095    {
1096      // cant use responsebuilder.tag ... it will quote the already
1097      // quoted string
1098      responseBuilder.header(HttpHeaders.ETAG,
1099          ((NotModifiedException) e).getVersion());
1100    }
1101    else
1102    {
1103      if(e instanceof PreconditionFailedException)
1104      {
1105        // cant use responsebuilder.tag ... it will quote the already
1106        // quoted string
1107        responseBuilder.header(HttpHeaders.ETAG,
1108            ((PreconditionFailedException) e).getVersion());
1109      }
1110      setResponseEntity(responseBuilder, requestContext.getProduceMediaType(),
1111          e);
1112    }
1113    return responseBuilder;
1114  }
1115
1116  /**
1117   * Parse a filter string.
1118   * @param filterString          The SCIM filter string.
1119   * @param resourceDescriptor    ResourceDescriptor for resource being
1120   *                              queried.
1121   * @return                      SCIMFilter object.
1122   * @throws SCIMException        If filter string violates SCIM syntax.
1123   */
1124  private SCIMFilter parseFilter(
1125      final String filterString,
1126      final ResourceDescriptor resourceDescriptor) throws SCIMException
1127  {
1128    SCIMFilter filter = null;
1129    if (filterString != null && !filterString.isEmpty())
1130    {
1131      if(resourceDescriptor.getSchema().equalsIgnoreCase(
1132          "urn:unboundid:schemas:scim:ldap:1.0"))
1133      {
1134        filter = SCIMFilter.parse(
1135            filterString, resourceDescriptor.getSchema());
1136      }
1137      else
1138      {
1139        filter = SCIMFilter.parse(filterString);
1140      }
1141    }
1142    return filter;
1143  }
1144}