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}