001package ca.cdr.api.model.json; 002 003/*- 004 * #%L 005 * Smile CDR - CDR 006 * %% 007 * Copyright (C) 2016 - 2025 Smile CDR, Inc. 008 * %% 009 * All rights reserved. 010 * #L% 011 */ 012 013import ca.cdr.api.model.enm.OAuth2AuthorizedGrantTypeEnum; 014import ca.cdr.api.model.enm.PermissionEnum; 015import ca.cdr.api.security.ScopeValidation; 016import com.fasterxml.jackson.annotation.JsonProperty; 017import io.swagger.v3.oas.annotations.media.Schema; 018import jakarta.annotation.Nonnull; 019import jakarta.validation.constraints.NotBlank; 020import jakarta.validation.constraints.Pattern; 021import org.apache.commons.lang3.Validate; 022import org.springframework.security.core.GrantedAuthority; 023import org.springframework.security.oauth2.provider.ClientDetails; 024 025import java.util.ArrayList; 026import java.util.Collection; 027import java.util.Collections; 028import java.util.Date; 029import java.util.HashMap; 030import java.util.Iterator; 031import java.util.List; 032import java.util.Map; 033import java.util.Optional; 034import java.util.Set; 035import java.util.TreeSet; 036import java.util.stream.Collectors; 037 038import static org.apache.commons.lang3.StringUtils.defaultString; 039import static org.apache.commons.lang3.StringUtils.isNotBlank; 040import static org.apache.commons.lang3.StringUtils.join; 041 042public class OAuth2WritableClientDetailsJson implements ClientDetails, IHasAuthorities, IModelJson { 043 044 public static final String CLIENTID_PATTERN = "[^ ]+"; 045 public static final String CLIENT_ID = "clientId"; 046 public static final String CLIENT_NAME = "clientName"; 047 public static final String ENABLED = "enabled"; 048 public static final String PID = "pid"; 049 private static final long serialVersionUID = 1L; 050 051 public static final String CLIENT_SECRET_JOINED_DELIMITER = " "; 052 053 @Schema(accessMode = Schema.AccessMode.READ_ONLY, description = "The internal ID for this client.") 054 @JsonProperty(PID) 055 private Long myPid; 056 057 @JsonProperty("accessTokenValiditySeconds") 058 @Schema(description = "The number of seconds that an access token should be valid once it has been created.") 059 private Integer myAccessTokenValiditySeconds; 060 061 @JsonProperty("allowedGrantTypes") 062 @Schema( 063 description = 064 "The grant types that this client is permitted to perform. See [Authorization Flows](/docs/smart/smart_on_fhir_authorization_flows.html) for a description of the possible flows.") 065 private TreeSet<OAuth2AuthorizedGrantTypeEnum> myAllowedGrantTypes; 066 067 @JsonProperty("autoApproveScopes") 068 @Schema( 069 description = 070 "Scopes listed here will be automatically approved if requested by the client during the initial authorization request, without requiring the user to explicitly accept them.") 071 private TreeSet<String> myAutoApproveScopes; 072 073 @JsonProperty("autoGrantScopes") 074 @Schema( 075 description = 076 "Scopes listed here will be automatically granted during every successful authorization by this client. These scopes do not have to be explicitly requested by the client during the initial authorization request.") 077 private TreeSet<String> myAutoGrantScopes; 078 079 @NotBlank 080 @JsonProperty(CLIENT_ID) 081 @Schema(description = "The Client ID (corresponds to the `iss` field in many OAuth2 exchanges).") 082 @Pattern(regexp = CLIENTID_PATTERN, message = "module_validation.invalid_client_id") 083 private String myClientId; 084 085 @JsonProperty(CLIENT_NAME) 086 @Schema(description = "A human friendly description/name for the client.") 087 private String myClientName; 088 089 @JsonProperty("clientSecrets") 090 @Schema(description = "Optionally contains client secrets to be used by the client in some grant types.") 091 private List<OAuth2ClientSecretJson> myClientSecrets; 092 093 @JsonProperty("fixedScope") 094 @Schema( 095 description = 096 "Is this client fixed scope? When authorizing a fixed scope client, the list of scopes requested in the initial authorization request will be ignored, and the complete list of scopes in the Scope property will be assumed. If these scopes are not listed as Auto-Approve, the user will still be required to approve them.") 097 private boolean myFixedScope; 098 099 @JsonProperty("refreshTokenValiditySeconds") 100 @Schema(description = "The number of seconds that a refresh token will be valid for.") 101 private Integer myRefreshTokenValiditySeconds; 102 103 @JsonProperty("registeredRedirectUris") 104 @Schema(description = "The allowable redirect URIs that may be requested.") 105 private TreeSet<String> myRegisteredRedirectUris; 106 107 @JsonProperty("scopes") 108 @Schema(description = "A list of OAuth2 scopes that the client is allowed to request user approval for.") 109 private TreeSet<String> myScope; 110 111 @JsonProperty("secretRequired") 112 @Schema(description = "Is the client secret required in order to authenticate this client?") 113 private boolean mySecretRequired; 114 115 @JsonProperty("secretClientCanChange") 116 @Schema(description = "Can the client change their own secret?") 117 private boolean mySecretClientCanChange; 118 119 @JsonProperty(ENABLED) 120 @Schema(description = "Is the client enabled?") 121 private boolean myEnabled; 122 123 @JsonProperty("canIntrospectOwnTokens") 124 @Schema(description = "Can this client perform token introspection on tokens that it issued?") 125 private boolean myCanIntrospectOwnTokens; 126 127 @JsonProperty("canIntrospectAnyTokens") 128 @Schema( 129 description = 130 "Can this client perform token introspecton on any tokens issued by the security module it is registered against?") 131 private boolean myCanIntrospectAnyTokens; 132 133 @JsonProperty("alwaysRequireApproval") 134 @Schema( 135 description = 136 "Should the user approval page be displayed even if the client has not requested any scopes that require user approval?") 137 private boolean myAlwaysRequireApproval; 138 139 @JsonProperty("canReissueTokens") 140 @Schema( 141 description = 142 "Can the OAuth2 server reissue tokens that have been previously issued for this client, if the token request is the same (e.g. for the same user, requesting the same scopes, etc.) and the token is not close to expiry?") 143 private boolean myCanReissueTokens; 144 145 @JsonProperty("permissions") 146 @Schema( 147 description = 148 "Any permission that should be granted directly to the client when it authenticates using the [Client Credentials Grant](/docs/smart/smart_on_fhir_authorization_flows.html#client-credentials-flow).") 149 private List<GrantedAuthorityJson> myPermissions; 150 151 @JsonProperty("rememberApprovedScopes") 152 @Schema( 153 description = 154 "When a user performs an OAuth2 authentication/authorization flow for this client, should their approved scopes be remembered the next time they authenticate?") 155 private boolean myRememberApprovedScopes; 156 157 @JsonProperty("attestationAccepted") 158 @Schema(description = "Has the client developer attested to the policy?") 159 private boolean myAttestationAccepted; 160 161 @JsonProperty("publicJwks") 162 @Schema( 163 description = 164 "The public JWKS Keystore for this client. Used when the client authenticated using a bearer token.") 165 private String myPublicJwks; 166 167 @JsonProperty("jwksUrl") 168 @Schema( 169 description = 170 "A public endpoint location of the JWK Set. If present, this will be used before any public JWKS on the client directly.") 171 private String myJwksUrl; 172 173 @JsonProperty("archivedAt") 174 @Schema(description = "The time at which this client was archived, if it has been.") 175 private Date myArchivedAt; 176 177 @JsonProperty("createdByAppSphere") 178 private boolean myCreatedByAppSphere; 179 180 /** 181 * Constructor 182 */ 183 public OAuth2WritableClientDetailsJson() { 184 super(); 185 } 186 187 public OAuth2WritableClientDetailsJson(OAuth2WritableClientDetailsJson theClientDetailsJson) { 188 myPid = theClientDetailsJson.myPid; 189 myAccessTokenValiditySeconds = theClientDetailsJson.myAccessTokenValiditySeconds; 190 myAllowedGrantTypes = nullSafeClone(theClientDetailsJson.myAllowedGrantTypes); 191 myAutoApproveScopes = nullSafeClone(theClientDetailsJson.myAutoApproveScopes); 192 myAutoGrantScopes = nullSafeClone(theClientDetailsJson.myAutoGrantScopes); 193 myClientId = theClientDetailsJson.myClientId; 194 myClientName = theClientDetailsJson.myClientName; 195 myClientSecrets = nullSafeClone(theClientDetailsJson.myClientSecrets); 196 myFixedScope = theClientDetailsJson.myFixedScope; 197 myRefreshTokenValiditySeconds = theClientDetailsJson.myRefreshTokenValiditySeconds; 198 myRegisteredRedirectUris = nullSafeClone(theClientDetailsJson.myRegisteredRedirectUris); 199 myScope = nullSafeClone(theClientDetailsJson.myScope); 200 mySecretRequired = theClientDetailsJson.mySecretRequired; 201 mySecretClientCanChange = theClientDetailsJson.mySecretClientCanChange; 202 myEnabled = theClientDetailsJson.myEnabled; 203 myCanIntrospectOwnTokens = theClientDetailsJson.myCanIntrospectOwnTokens; 204 myCanIntrospectAnyTokens = theClientDetailsJson.myCanIntrospectAnyTokens; 205 myAlwaysRequireApproval = theClientDetailsJson.myAlwaysRequireApproval; 206 myCanReissueTokens = theClientDetailsJson.myCanReissueTokens; 207 myPermissions = nullSafeClone(theClientDetailsJson.myPermissions); 208 myRememberApprovedScopes = theClientDetailsJson.myRememberApprovedScopes; 209 myAttestationAccepted = theClientDetailsJson.myAttestationAccepted; 210 myPublicJwks = theClientDetailsJson.myPublicJwks; 211 myJwksUrl = theClientDetailsJson.myJwksUrl; 212 myArchivedAt = theClientDetailsJson.myArchivedAt; 213 myCreatedByAppSphere = theClientDetailsJson.myCreatedByAppSphere; 214 } 215 216 public String getJwksUrl() { 217 return myJwksUrl; 218 } 219 220 public void setJwksUrl(String theJwksUrl) { 221 myJwksUrl = theJwksUrl; 222 } 223 224 public String getPublicJwks() { 225 return myPublicJwks; 226 } 227 228 public void setPublicJwks(String thePublicJwks) { 229 myPublicJwks = thePublicJwks; 230 } 231 232 @Override 233 public List<GrantedAuthorityJson> getPermissions() { 234 if (myPermissions == null) { 235 myPermissions = new ArrayList<>(); 236 } 237 return myPermissions; 238 } 239 240 public void setPermissions(List<GrantedAuthorityJson> thePermissions) { 241 myPermissions = thePermissions; 242 } 243 244 public void addAllowedGrantType(String theName) { 245 addAllowedGrantType(OAuth2AuthorizedGrantTypeEnum.valueOf(theName)); 246 } 247 248 public void addAllowedGrantType(@Nonnull OAuth2AuthorizedGrantTypeEnum theGrantType) { 249 getAllowedGrantTypes().add(theGrantType); 250 } 251 252 public void addAutoApproveScopeIfNotBlank(String theScope) { 253 if (isNotBlank(theScope)) { 254 getAutoApproveScopes().add(theScope.trim()); 255 } 256 } 257 258 public void addRedirectUrlIfNotBlank(String theRedirectUrl) { 259 if (isNotBlank(theRedirectUrl)) { 260 getRegisteredRedirectUri().add(theRedirectUrl.trim()); 261 } 262 } 263 264 public OAuth2WritableClientDetailsJson addScopeIfNotBlank(String theScope) { 265 if (isNotBlank(theScope)) { 266 getScope().add(theScope.trim()); 267 } 268 return this; 269 } 270 271 @Override 272 public Integer getAccessTokenValiditySeconds() { 273 return myAccessTokenValiditySeconds; 274 } 275 276 public void setAccessTokenValiditySeconds(Integer theAccessTokenValiditySeconds) { 277 myAccessTokenValiditySeconds = theAccessTokenValiditySeconds; 278 } 279 280 @Override 281 public Map<String, Object> getAdditionalInformation() { 282 return new HashMap<>(); 283 } 284 285 public TreeSet<OAuth2AuthorizedGrantTypeEnum> getAllowedGrantTypes() { 286 if (myAllowedGrantTypes == null) { 287 myAllowedGrantTypes = new TreeSet<>(); 288 } 289 return myAllowedGrantTypes; 290 } 291 292 public void setAllowedGrantTypes(TreeSet<OAuth2AuthorizedGrantTypeEnum> theAllowedGrantTypes) { 293 myAllowedGrantTypes = theAllowedGrantTypes; 294 } 295 296 /** 297 * This method is a part of the Spring Security framework, and returns 298 * an <b>unmodifiable</b> copy of the client authorities. Use 299 * {@link #getPermissions()} if you want to modify the actual 300 * values. 301 */ 302 @Override 303 public Collection<GrantedAuthority> getAuthorities() { 304 ArrayList<GrantedAuthorityJson> retVal = new ArrayList<>(); 305 retVal.addAll(getPermissions()); 306 return Collections.unmodifiableList(retVal); 307 } 308 309 @Override 310 public Set<String> getAuthorizedGrantTypes() { 311 TreeSet<String> retVal = 312 getAllowedGrantTypes().stream().map(t -> t.getCode()).collect(Collectors.toCollection(TreeSet::new)); 313 return Collections.unmodifiableSet(retVal); 314 } 315 316 public boolean hasAutoApproveScopes() { 317 return myAutoApproveScopes != null && !myAutoApproveScopes.isEmpty(); 318 } 319 320 public boolean hasAutoGrantScopes() { 321 return myAutoGrantScopes != null && !myAutoGrantScopes.isEmpty(); 322 } 323 324 @Nonnull 325 public TreeSet<String> getAutoApproveScopes() { 326 if (myAutoApproveScopes == null) { 327 myAutoApproveScopes = new TreeSet<>(); 328 } 329 return myAutoApproveScopes; 330 } 331 332 public void setAutoApproveScopes(TreeSet<String> theAutoApproveScopes) { 333 myAutoApproveScopes = theAutoApproveScopes; 334 } 335 336 @Nonnull 337 public TreeSet<String> getAutoGrantScopes() { 338 if (myAutoGrantScopes == null) { 339 myAutoGrantScopes = new TreeSet<>(); 340 } 341 return myAutoGrantScopes; 342 } 343 344 public void setAutoGrantScopes(TreeSet<String> theAutoGrantScopes) { 345 myAutoGrantScopes = theAutoGrantScopes; 346 } 347 348 public String getAutoApproveScopesSpaceSeparated() { 349 return defaultString(join(getAutoApproveScopes(), ' ')); 350 } 351 352 public String getAutoGrantScopesSpaceSeparated() { 353 return defaultString(join(getAutoGrantScopes(), ' ')); 354 } 355 356 @Override 357 public String getClientId() { 358 return myClientId; 359 } 360 361 public OAuth2WritableClientDetailsJson setClientId(String theClientId) { 362 myClientId = theClientId; 363 return this; 364 } 365 366 public String getClientName() { 367 return myClientName; 368 } 369 370 public void setClientName(String theClientName) { 371 myClientName = theClientName; 372 } 373 374 @Override 375 public String getClientSecret() { 376 // NB: If you ever decide to cache the return value of this method, note that 377 // we sometimes mask the secrets after generating the JSON entity (e.g. if the 378 // client def is going to be returned to a user) 379 return getClientSecrets().stream() 380 .filter(t -> t.getExpiration() == null || t.getExpiration().getTime() >= System.currentTimeMillis()) 381 .filter(t -> t.getActivation() == null || t.getActivation().getTime() <= System.currentTimeMillis()) 382 .map(t -> t.getSecret()) 383 .collect(Collectors.joining(CLIENT_SECRET_JOINED_DELIMITER)); 384 } 385 386 public List<OAuth2ClientSecretJson> getClientSecrets() { 387 if (myClientSecrets == null) { 388 myClientSecrets = new ArrayList<>(); 389 } 390 return myClientSecrets; 391 } 392 393 public void setClientSecrets(List<OAuth2ClientSecretJson> theClientSecrets) { 394 myClientSecrets = theClientSecrets; 395 } 396 397 public Long getPid() { 398 return myPid; 399 } 400 401 public OAuth2WritableClientDetailsJson setPid(Long myPid) { 402 this.myPid = myPid; 403 return this; 404 } 405 406 @Override 407 public Integer getRefreshTokenValiditySeconds() { 408 return myRefreshTokenValiditySeconds; 409 } 410 411 public void setRefreshTokenValiditySeconds(Integer theRefreshTokenValiditySeconds) { 412 myRefreshTokenValiditySeconds = theRefreshTokenValiditySeconds; 413 } 414 415 @Nonnull 416 @Override 417 public TreeSet<String> getRegisteredRedirectUri() { 418 if (myRegisteredRedirectUris == null) { 419 myRegisteredRedirectUris = new TreeSet<>(); 420 } 421 return myRegisteredRedirectUris; 422 } 423 424 public TreeSet<String> getRegisteredRedirectUris() { 425 return getRegisteredRedirectUri(); 426 } 427 428 public void setRegisteredRedirectUris(TreeSet<String> theRegisteredRedirectUris) { 429 myRegisteredRedirectUris = theRegisteredRedirectUris; 430 } 431 432 public void setRegisteredRedirectUris(Iterable<String> theRegisteredRedirectUri) { 433 myRegisteredRedirectUris = new TreeSet<>(); 434 if (theRegisteredRedirectUri != null) { 435 theRegisteredRedirectUri.forEach(t -> myRegisteredRedirectUris.add(t)); 436 } 437 } 438 439 public String getRegisteredRedirectUriOnePerLine() { 440 return defaultString(join(getRegisteredRedirectUri(), '\n')); 441 } 442 443 @Override 444 public Set<String> getResourceIds() { 445 return Collections.emptySet(); 446 } 447 448 public boolean hasScope() { 449 return myScope != null && !myScope.isEmpty(); 450 } 451 452 @Nonnull 453 @Override 454 public Set<String> getScope() { 455 if (myScope == null) { 456 myScope = new TreeSet<>(); 457 } 458 return new ScopeSetWrapper(myScope); 459 } 460 461 public void setScope(TreeSet<String> theScope) { 462 myScope = theScope; 463 } 464 465 public Set<String> getScopes() { 466 return getScope(); 467 } 468 469 public void setScopes(TreeSet<String> theScope) { 470 setScope(theScope); 471 } 472 473 public String getScopeSpaceSeparated() { 474 return defaultString(join(getScope(), ' ')); 475 } 476 477 public boolean isAlwaysRequireApproval() { 478 return myAlwaysRequireApproval; 479 } 480 481 public void setAlwaysRequireApproval(boolean theAlwaysRequireApproval) { 482 myAlwaysRequireApproval = theAlwaysRequireApproval; 483 } 484 485 @Override 486 public boolean isAutoApprove(String theScope) { 487 boolean retVal = getAutoApproveScopes().contains(theScope); 488 if (!retVal) { 489 retVal = ScopeValidation.isScopeAllowedDirectlyOrIndirectly(getAutoApproveScopes(), theScope); 490 } 491 return retVal; 492 } 493 494 public boolean isCanIntrospectAnyTokens() { 495 return myCanIntrospectAnyTokens; 496 } 497 498 public void setCanIntrospectAnyTokens(boolean theCanIntrospectAnyTokens) { 499 myCanIntrospectAnyTokens = theCanIntrospectAnyTokens; 500 } 501 502 public boolean isCanIntrospectOwnTokens() { 503 return myCanIntrospectOwnTokens; 504 } 505 506 public void setCanIntrospectOwnTokens(boolean theCanIntrospectOwnTokens) { 507 myCanIntrospectOwnTokens = theCanIntrospectOwnTokens; 508 } 509 510 public boolean isFixedScope() { 511 return myFixedScope; 512 } 513 514 public void setFixedScope(boolean theFixedScope) { 515 myFixedScope = theFixedScope; 516 } 517 518 @Override 519 public boolean isScoped() { 520 return myFixedScope; 521 } 522 523 @Override 524 public boolean isSecretRequired() { 525 return mySecretRequired; 526 } 527 528 public void setSecretRequired(boolean theSecretRequired) { 529 mySecretRequired = theSecretRequired; 530 } 531 532 public boolean isSecretClientCanChange() { 533 return mySecretClientCanChange; 534 } 535 536 public void setSecretClientCanChange(boolean theSecretClientCanChange) { 537 mySecretClientCanChange = theSecretClientCanChange; 538 } 539 540 public boolean isEnabled() { 541 return myEnabled; 542 } 543 544 public void setEnabled(boolean theEnabled) { 545 myEnabled = theEnabled; 546 } 547 548 public Optional<OAuth2ClientSecretJson> getClientSecret(Long thePid) { 549 Validate.notNull(thePid, "thePid must not be null"); 550 return getClientSecrets().stream() 551 .filter(t -> thePid.equals(t.getPid())) 552 .findFirst(); 553 } 554 555 public OAuth2WritableClientDetailsJson addClientSecret(OAuth2ClientSecretJson theSecret) { 556 Validate.notNull(theSecret, "theSecret must not be null"); 557 getClientSecrets().add(theSecret); 558 return this; 559 } 560 561 public OAuth2ClientSecretJson addClientSecret(String theSecret) { 562 Validate.notBlank(theSecret, "theSecret must not be null or blank"); 563 OAuth2ClientSecretJson secret = addClientSecret(); 564 secret.setSecret(theSecret); 565 return secret; 566 } 567 568 public OAuth2ClientSecretJson addClientSecret() { 569 OAuth2ClientSecretJson secret = new OAuth2ClientSecretJson(); 570 addClientSecret(secret); 571 return secret; 572 } 573 574 public OAuth2ClientSecretJson addClientSecret(String theSecret, Date theExpiration) { 575 return addClientSecret(theSecret).setExpiration(theExpiration); 576 } 577 578 public boolean isCanReissueTokens() { 579 return myCanReissueTokens; 580 } 581 582 public void setCanReissueTokens(boolean theCanReissueTokens) { 583 myCanReissueTokens = theCanReissueTokens; 584 } 585 586 public void addAutoGrantScopeIfNotBlank(String theScope) { 587 if (isNotBlank(theScope)) { 588 getAutoGrantScopes().add(theScope.trim()); 589 } 590 } 591 592 public void addPermission(GrantedAuthorityJson thePermission) { 593 Validate.notNull(thePermission); 594 getPermissions().add(thePermission); 595 } 596 597 public void addPermission(PermissionEnum thePermission, String theArgument) { 598 Validate.notNull(thePermission); 599 addPermission(new GrantedAuthorityJson(thePermission, theArgument)); 600 } 601 602 public void addPermission(PermissionEnum thePermission) { 603 Validate.notNull(thePermission); 604 addPermission(new GrantedAuthorityJson(thePermission)); 605 } 606 607 public void removePermission(PermissionEnum thePermission, String theArgument) { 608 Validate.notNull(thePermission); 609 removePermission(new GrantedAuthorityJson(thePermission, theArgument)); 610 } 611 612 public void removePermission(GrantedAuthorityJson thePermission) { 613 Validate.notNull(thePermission); 614 getPermissions().remove(thePermission); 615 } 616 617 public boolean isRememberApprovedScopes() { 618 return myRememberApprovedScopes; 619 } 620 621 public void setRememberApprovedScopes(boolean theRememberApprovedScopes) { 622 myRememberApprovedScopes = theRememberApprovedScopes; 623 } 624 625 public boolean isAttestationAccepted() { 626 return myAttestationAccepted; 627 } 628 629 public void setAttestationAccepted(boolean theAttestationAccepted) { 630 myAttestationAccepted = theAttestationAccepted; 631 } 632 633 public Date getArchivedAt() { 634 return myArchivedAt; 635 } 636 637 public void setArchivedAt(Date theArchivedAt) { 638 myArchivedAt = theArchivedAt; 639 } 640 641 public boolean isCreatedByAppSphere() { 642 return myCreatedByAppSphere; 643 } 644 645 public void setCreatedByAppSphere(boolean theCreatedByAppSphere) { 646 myCreatedByAppSphere = theCreatedByAppSphere; 647 } 648 649 private static <T> TreeSet<T> nullSafeClone(TreeSet<T> theTreeSet) { 650 return theTreeSet != null ? new TreeSet<>(theTreeSet) : null; 651 } 652 653 private static <T> List<T> nullSafeClone(List<T> theList) { 654 return theList != null ? new ArrayList<>(theList) : null; 655 } 656 657 /** 658 * This class provides a wrapper around the client scopes property 659 * (see {@link #getScope()}) but modifies the behaviour of the 660 * {@link #contains(Object)} method. Effectively, any tests for whether 661 * the {@link Set#contains(Object)} method contains a given scope will test 662 * the scopes, but will also account for {@link #getAutoGrantScopes()} and 663 * equivalent but narrower scopes (e.g. we allow <code>patient/Observation.read</code> 664 * if the client is allowed <code>patient/*.read</code>). 665 * <p> 666 * We use this clunky wrapper approach because Spring Security added a test in 667 * {@link org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationManager} 668 * that we can't modify. 669 * </p> 670 */ 671 private class ScopeSetWrapper implements Set<String> { 672 private final TreeSet<String> myWrap; 673 674 public ScopeSetWrapper(TreeSet<String> theWrap) { 675 myWrap = theWrap; 676 } 677 678 @Override 679 public int size() { 680 return myWrap.size(); 681 } 682 683 @Override 684 public boolean isEmpty() { 685 return myWrap.isEmpty(); 686 } 687 688 /** 689 * See class documentation for a description of what this method is about 690 */ 691 @Override 692 public boolean contains(Object theScope) { 693 if (!(theScope instanceof String scopeStr)) { 694 return false; 695 } else if (ScopeValidation.isScopeAllowedDirectlyOrIndirectly(myWrap, scopeStr)) { 696 return true; 697 } else { 698 return ScopeValidation.isScopeAllowedDirectlyOrIndirectly(getAutoGrantScopes(), scopeStr); 699 } 700 } 701 702 @Nonnull 703 @Override 704 public Iterator<String> iterator() { 705 return myWrap.iterator(); 706 } 707 708 @Nonnull 709 @Override 710 public Object[] toArray() { 711 return myWrap.toArray(); 712 } 713 714 @Nonnull 715 @Override 716 public <T> T[] toArray(@Nonnull T[] a) { 717 return myWrap.toArray(a); 718 } 719 720 @Override 721 public boolean add(String theScope) { 722 return myWrap.add(theScope); 723 } 724 725 @Override 726 public boolean remove(Object o) { 727 return myWrap.remove(o); 728 } 729 730 @Override 731 public boolean containsAll(@Nonnull Collection<?> c) { 732 return myWrap.containsAll(c); 733 } 734 735 @Override 736 public boolean addAll(@Nonnull Collection<? extends String> c) { 737 return myWrap.addAll(c); 738 } 739 740 @Override 741 public boolean retainAll(@Nonnull Collection<?> c) { 742 return myWrap.retainAll(c); 743 } 744 745 @Override 746 public boolean removeAll(@Nonnull Collection<?> c) { 747 return myWrap.removeAll(c); 748 } 749 750 @Override 751 public void clear() { 752 myWrap.clear(); 753 } 754 } 755}