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}