
001/*- 002 * #%L 003 * HAPI FHIR - Server Framework 004 * %% 005 * Copyright (C) 2014 - 2025 Smile CDR, Inc. 006 * %% 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, software 014 * distributed under the License is distributed on an "AS IS" BASIS, 015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 016 * See the License for the specific language governing permissions and 017 * limitations under the License. 018 * #L% 019 */ 020package ca.uhn.fhir.rest.server.interceptor.auth; 021 022import ca.uhn.fhir.interceptor.api.Pointcut; 023import ca.uhn.fhir.model.primitive.IdDt; 024import ca.uhn.fhir.rest.api.RestOperationTypeEnum; 025import ca.uhn.fhir.rest.api.server.RequestDetails; 026import ca.uhn.fhir.rest.api.server.bulk.BulkExportJobParameters; 027import com.google.common.annotations.VisibleForTesting; 028import org.hl7.fhir.instance.model.api.IBaseResource; 029import org.hl7.fhir.instance.model.api.IIdType; 030 031import java.util.ArrayList; 032import java.util.Collection; 033import java.util.Objects; 034import java.util.Set; 035import java.util.stream.Collectors; 036 037import static org.apache.commons.collections4.CollectionUtils.isEmpty; 038import static org.apache.commons.collections4.CollectionUtils.isNotEmpty; 039import static org.apache.commons.lang3.StringUtils.isNotBlank; 040 041public class RuleBulkExportImpl extends BaseRule { 042 private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(RuleBulkExportImpl.class); 043 private String myGroupId; 044 private final Collection<String> myPatientIds; 045 private boolean myAppliesToAllPatients; 046 private BulkExportJobParameters.ExportStyle myWantExportStyle; 047 private Collection<String> myResourceTypes; 048 private boolean myWantAnyStyle; 049 050 RuleBulkExportImpl(String theRuleName) { 051 super(theRuleName); 052 myPatientIds = new ArrayList<>(); 053 } 054 055 @Override 056 public AuthorizationInterceptor.Verdict applyRule( 057 RestOperationTypeEnum theOperation, 058 RequestDetails theRequestDetails, 059 IBaseResource theInputResource, 060 IIdType theInputResourceId, 061 IBaseResource theOutputResource, 062 IRuleApplier theRuleApplier, 063 Set<AuthorizationFlagsEnum> theFlags, 064 Pointcut thePointcut) { 065 if (thePointcut != Pointcut.STORAGE_INITIATE_BULK_EXPORT) { 066 return null; 067 } 068 069 if (theRequestDetails == null) { 070 return null; 071 } 072 073 BulkExportJobParameters inboundBulkExportRequestOptions = (BulkExportJobParameters) theRequestDetails 074 .getUserData() 075 .get(AuthorizationInterceptor.REQUEST_ATTRIBUTE_BULK_DATA_EXPORT_OPTIONS); 076 // if style doesn't match - abstain 077 if (!myWantAnyStyle && inboundBulkExportRequestOptions.getExportStyle() != myWantExportStyle) { 078 return null; 079 } 080 081 // Do we only authorize some types? If so, make sure requested types are a subset 082 if (isNotEmpty(myResourceTypes)) { 083 if (isEmpty(inboundBulkExportRequestOptions.getResourceTypes())) { 084 return new AuthorizationInterceptor.Verdict(PolicyEnum.DENY, this); 085 } 086 if (!myResourceTypes.containsAll(inboundBulkExportRequestOptions.getResourceTypes())) { 087 return new AuthorizationInterceptor.Verdict(PolicyEnum.DENY, this); 088 } 089 } 090 091 // system only supports filtering by resource type. So if we are system, or any(), then allow, since we have 092 // done resource type checking 093 // above 094 AuthorizationInterceptor.Verdict allowVerdict = newVerdict( 095 theOperation, 096 theRequestDetails, 097 theInputResource, 098 theInputResourceId, 099 theOutputResource, 100 theRuleApplier); 101 102 if (myWantAnyStyle || myWantExportStyle == BulkExportJobParameters.ExportStyle.SYSTEM) { 103 return allowVerdict; 104 } 105 106 // assume myGroupId not empty->myStyle is group. If target group matches, then allow. 107 if (isNotBlank(myGroupId) && inboundBulkExportRequestOptions.getGroupId() != null) { 108 String expectedGroupId = 109 new IdDt(myGroupId).toUnqualifiedVersionless().getValue(); 110 String actualGroupId = new IdDt(inboundBulkExportRequestOptions.getGroupId()) 111 .toUnqualifiedVersionless() 112 .getValue(); 113 if (Objects.equals(expectedGroupId, actualGroupId)) { 114 return allowVerdict; 115 } 116 } 117 // patient export mode - instance or type. type can have 0..n patient ids. 118 // myPatientIds == the rules built by the auth interceptor rule builder 119 // options.getPatientIds() == the requested IDs in the export job. 120 121 // 1. If each of the requested resource IDs in the parameters are present in the users permissions, Approve 122 // 2. If any requested ID is not present in the users permissions, Deny. 123 if (myWantExportStyle == BulkExportJobParameters.ExportStyle.PATIENT) 124 // Unfiltered Type Level 125 if (myAppliesToAllPatients) { 126 return allowVerdict; 127 } 128 129 // Instance level, or filtered type level 130 if (isNotEmpty(myPatientIds)) { 131 // If bulk export options defines no patient IDs, return null. 132 if (inboundBulkExportRequestOptions.getPatientIds().isEmpty()) { 133 return null; 134 } else { 135 ourLog.debug("options.getPatientIds() != null"); 136 Set<String> requestedPatientIds = sanitizeIds(inboundBulkExportRequestOptions.getPatientIds()); 137 Set<String> permittedPatientIds = sanitizeIds(myPatientIds); 138 if (permittedPatientIds.containsAll(requestedPatientIds)) { 139 return allowVerdict; 140 } else { 141 return new AuthorizationInterceptor.Verdict(PolicyEnum.DENY, this); 142 } 143 } 144 } 145 return null; 146 } 147 148 private Set<String> sanitizeIds(Collection<String> myPatientIds) { 149 return myPatientIds.stream() 150 .map(id -> new IdDt(id).toUnqualifiedVersionless().getValue()) 151 .collect(Collectors.toSet()); 152 } 153 154 public void setAppliesToGroupExportOnGroup(String theGroupId) { 155 myWantExportStyle = BulkExportJobParameters.ExportStyle.GROUP; 156 myGroupId = theGroupId; 157 } 158 159 public void setAppliesToPatientExportOnGroup(String theGroupId) { 160 myWantExportStyle = BulkExportJobParameters.ExportStyle.PATIENT; 161 myGroupId = theGroupId; 162 } 163 164 public void setAppliesToPatientExport(String thePatientId) { 165 myWantExportStyle = BulkExportJobParameters.ExportStyle.PATIENT; 166 myPatientIds.add(thePatientId); 167 } 168 169 public void setAppliesToPatientExport(Collection<String> thePatientIds) { 170 myWantExportStyle = BulkExportJobParameters.ExportStyle.PATIENT; 171 myPatientIds.addAll(thePatientIds); 172 } 173 174 public void setAppliesToPatientExportAllPatients() { 175 myWantExportStyle = BulkExportJobParameters.ExportStyle.PATIENT; 176 myAppliesToAllPatients = true; 177 } 178 179 public void setAppliesToSystem() { 180 myWantExportStyle = BulkExportJobParameters.ExportStyle.SYSTEM; 181 } 182 183 public void setResourceTypes(Collection<String> theResourceTypes) { 184 myResourceTypes = theResourceTypes; 185 } 186 187 public void setAppliesToAny() { 188 myWantAnyStyle = true; 189 } 190 191 String getGroupId() { 192 return myGroupId; 193 } 194 195 BulkExportJobParameters.ExportStyle getWantExportStyle() { 196 return myWantExportStyle; 197 } 198 199 @VisibleForTesting 200 Collection<String> getPatientIds() { 201 return myPatientIds; 202 } 203 204 @VisibleForTesting 205 Collection<String> getResourceTypes() { 206 return myResourceTypes; 207 } 208}