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}