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}