001/*- 002 * #%L 003 * HAPI FHIR - Server Framework 004 * %% 005 * Copyright (C) 2014 - 2024 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) 074 theRequestDetails.getAttribute(AuthorizationInterceptor.REQUEST_ATTRIBUTE_BULK_DATA_EXPORT_OPTIONS); 075 // if style doesn't match - abstain 076 if (!myWantAnyStyle && inboundBulkExportRequestOptions.getExportStyle() != myWantExportStyle) { 077 return null; 078 } 079 080 // Do we only authorize some types? If so, make sure requested types are a subset 081 if (isNotEmpty(myResourceTypes)) { 082 if (isEmpty(inboundBulkExportRequestOptions.getResourceTypes())) { 083 return new AuthorizationInterceptor.Verdict(PolicyEnum.DENY, this); 084 } 085 if (!myResourceTypes.containsAll(inboundBulkExportRequestOptions.getResourceTypes())) { 086 return new AuthorizationInterceptor.Verdict(PolicyEnum.DENY, this); 087 } 088 } 089 090 // system only supports filtering by resource type. So if we are system, or any(), then allow, since we have 091 // done resource type checking 092 // above 093 AuthorizationInterceptor.Verdict allowVerdict = newVerdict( 094 theOperation, 095 theRequestDetails, 096 theInputResource, 097 theInputResourceId, 098 theOutputResource, 099 theRuleApplier); 100 101 if (myWantAnyStyle || myWantExportStyle == BulkExportJobParameters.ExportStyle.SYSTEM) { 102 return allowVerdict; 103 } 104 105 // assume myGroupId not empty->myStyle is group. If target group matches, then allow. 106 if (isNotBlank(myGroupId) && inboundBulkExportRequestOptions.getGroupId() != null) { 107 String expectedGroupId = 108 new IdDt(myGroupId).toUnqualifiedVersionless().getValue(); 109 String actualGroupId = new IdDt(inboundBulkExportRequestOptions.getGroupId()) 110 .toUnqualifiedVersionless() 111 .getValue(); 112 if (Objects.equals(expectedGroupId, actualGroupId)) { 113 return allowVerdict; 114 } 115 } 116 // patient export mode - instance or type. type can have 0..n patient ids. 117 // myPatientIds == the rules built by the auth interceptor rule builder 118 // options.getPatientIds() == the requested IDs in the export job. 119 120 // 1. If each of the requested resource IDs in the parameters are present in the users permissions, Approve 121 // 2. If any requested ID is not present in the users permissions, Deny. 122 if (myWantExportStyle == BulkExportJobParameters.ExportStyle.PATIENT) 123 // Unfiltered Type Level 124 if (myAppliesToAllPatients) { 125 return allowVerdict; 126 } 127 128 // Instance level, or filtered type level 129 if (isNotEmpty(myPatientIds)) { 130 // If bulk export options defines no patient IDs, return null. 131 if (inboundBulkExportRequestOptions.getPatientIds().isEmpty()) { 132 return null; 133 } else { 134 ourLog.debug("options.getPatientIds() != null"); 135 Set<String> requestedPatientIds = sanitizeIds(inboundBulkExportRequestOptions.getPatientIds()); 136 Set<String> permittedPatientIds = sanitizeIds(myPatientIds); 137 if (permittedPatientIds.containsAll(requestedPatientIds)) { 138 return allowVerdict; 139 } else { 140 return new AuthorizationInterceptor.Verdict(PolicyEnum.DENY, this); 141 } 142 } 143 } 144 return null; 145 } 146 147 private Set<String> sanitizeIds(Collection<String> myPatientIds) { 148 return myPatientIds.stream() 149 .map(id -> new IdDt(id).toUnqualifiedVersionless().getValue()) 150 .collect(Collectors.toSet()); 151 } 152 153 public void setAppliesToGroupExportOnGroup(String theGroupId) { 154 myWantExportStyle = BulkExportJobParameters.ExportStyle.GROUP; 155 myGroupId = theGroupId; 156 } 157 158 public void setAppliesToPatientExportOnGroup(String theGroupId) { 159 myWantExportStyle = BulkExportJobParameters.ExportStyle.PATIENT; 160 myGroupId = theGroupId; 161 } 162 163 public void setAppliesToPatientExport(String thePatientId) { 164 myWantExportStyle = BulkExportJobParameters.ExportStyle.PATIENT; 165 myPatientIds.add(thePatientId); 166 } 167 168 public void setAppliesToPatientExport(Collection<String> thePatientIds) { 169 myWantExportStyle = BulkExportJobParameters.ExportStyle.PATIENT; 170 myPatientIds.addAll(thePatientIds); 171 } 172 173 public void setAppliesToPatientExportAllPatients() { 174 myWantExportStyle = BulkExportJobParameters.ExportStyle.PATIENT; 175 myAppliesToAllPatients = true; 176 } 177 178 public void setAppliesToSystem() { 179 myWantExportStyle = BulkExportJobParameters.ExportStyle.SYSTEM; 180 } 181 182 public void setResourceTypes(Collection<String> theResourceTypes) { 183 myResourceTypes = theResourceTypes; 184 } 185 186 public void setAppliesToAny() { 187 myWantAnyStyle = true; 188 } 189 190 String getGroupId() { 191 return myGroupId; 192 } 193 194 BulkExportJobParameters.ExportStyle getWantExportStyle() { 195 return myWantExportStyle; 196 } 197 198 @VisibleForTesting 199 Collection<String> getPatientIds() { 200 return myPatientIds; 201 } 202 203 @VisibleForTesting 204 Collection<String> getResourceTypes() { 205 return myResourceTypes; 206 } 207}