001/*-
002 * #%L
003 * Smile CDR - CDR
004 * %%
005 * Copyright (C) 2016 - 2025 Smile CDR, Inc.
006 * %%
007 * All rights reserved.
008 * #L%
009 */
010package ca.cdr.api.security;
011
012import ca.cdr.api.log.Logs;
013import jakarta.annotation.Nonnull;
014import org.apache.commons.lang3.StringUtils;
015
016import java.util.Arrays;
017import java.util.Optional;
018import java.util.Set;
019import java.util.stream.Collectors;
020
021import static org.apache.commons.lang3.StringUtils.defaultString;
022
023public class ScopeValidation {
024
025        /**
026         * Returns true if {@literal theClientAllowedScopes} contains {@literal theRequestedScope} or if
027         * {@literal theClientAllowedScopes} contains a scope that is more broad, such as <code>patient/*.read</code>
028         * and {@literal theRequestedScope} contains a narrower version such as <code>patient/Observation.read</code>.
029         */
030        public static boolean isScopeAllowedDirectlyOrIndirectly(
031                        Set<String> theClientAllowedScopes, String theRequestedScope) {
032                boolean isAllowable = false;
033                if (theClientAllowedScopes.contains(theRequestedScope)) {
034                        isAllowable = true;
035                } else {
036                        // do we have a wildcard that covers?
037                        Optional<SmartClinicalScope> maybeParsesScope = SmartClinicalScope.fromScopeString(theRequestedScope);
038                        if (maybeParsesScope.isPresent()) {
039                                SmartClinicalScope parsedRequestScope = maybeParsesScope.get();
040                                String wildcardExpression =
041                                                parsedRequestScope.getPrefix() + "/*." + parsedRequestScope.getPermissions();
042                                if (theClientAllowedScopes.contains(wildcardExpression)) {
043                                        Logs.getSecurityTroubleshootingLog()
044                                                        .debug(
045                                                                        "Auto-granting scope {} because the client is approved for {}",
046                                                                        theRequestedScope,
047                                                                        wildcardExpression);
048                                        isAllowable = true;
049                                } else {
050                                        // check the more general case
051                                        isAllowable = theClientAllowedScopes.stream()
052                                                        .map(SmartClinicalScope::fromScopeString)
053                                                        .flatMap(Optional::stream)
054                                                        .anyMatch(parsedRequestScope::isImpliedBy);
055                                }
056                        }
057                }
058                return isAllowable;
059        }
060
061        /**
062         * Turn patient/Observation.write into patient/*.write.
063         * Returns empty if theScope isn't a resource scope. - e.g. offline_access
064         *
065         * @return a wildcard scope covering the resource scope
066         */
067        static Optional<String> maybeBuildCoveringWildcard(String theScope) {
068                int slashIdx = theScope.indexOf('/');
069                int dotIdx = theScope.indexOf('.');
070                if (slashIdx >= 0 && dotIdx >= 0 && slashIdx < dotIdx) {
071
072                        String scopePrefix = theScope.substring(0, slashIdx + 1);
073                        String subjectNoun = theScope.substring(slashIdx + 1, dotIdx);
074                        String operationPermission = theScope.substring(dotIdx + 1);
075
076                        if (ScopeConstants.SUPPORTED_SCOPE_PREFIXES.contains(scopePrefix)
077                                        && StringUtils.isAlphanumeric(subjectNoun)
078                                        && ScopeConstants.SUPPORTED_RESOURCE_OPERATIONS.contains(operationPermission)) {
079                                String equivalentStarScope = scopePrefix + "*." + operationPermission;
080                                return Optional.of(equivalentStarScope);
081                        }
082                }
083                return Optional.empty();
084        }
085
086        @Nonnull
087        public static Set<String> scopesFromString(String scopeString) {
088                String[] split = defaultString(scopeString).split(" +");
089                return Arrays.stream(split).filter(s -> !s.isEmpty()).collect(Collectors.toSet());
090        }
091}