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}