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 org.hl7.fhir.instance.model.api.IBaseResource;
028import org.hl7.fhir.instance.model.api.IIdType;
029
030import java.util.ArrayList;
031import java.util.Collection;
032import java.util.List;
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 BulkExportJobParameters.ExportStyle myWantExportStyle;
046        private Collection<String> myResourceTypes;
047        private boolean myWantAnyStyle;
048
049        RuleBulkExportImpl(String theRuleName) {
050                super(theRuleName);
051                myPatientIds = new ArrayList<>();
052        }
053
054        @Override
055        public AuthorizationInterceptor.Verdict applyRule(
056                        RestOperationTypeEnum theOperation,
057                        RequestDetails theRequestDetails,
058                        IBaseResource theInputResource,
059                        IIdType theInputResourceId,
060                        IBaseResource theOutputResource,
061                        IRuleApplier theRuleApplier,
062                        Set<AuthorizationFlagsEnum> theFlags,
063                        Pointcut thePointcut) {
064                if (thePointcut != Pointcut.STORAGE_INITIATE_BULK_EXPORT) {
065                        return null;
066                }
067
068                if (theRequestDetails == null) {
069                        return null;
070                }
071
072                BulkExportJobParameters options = (BulkExportJobParameters)
073                                theRequestDetails.getAttribute(AuthorizationInterceptor.REQUEST_ATTRIBUTE_BULK_DATA_EXPORT_OPTIONS);
074
075                if (!myWantAnyStyle && options.getExportStyle() != myWantExportStyle) {
076                        return null;
077                }
078
079                if (isNotEmpty(myResourceTypes)) {
080                        if (isEmpty(options.getResourceTypes())) {
081                                return null;
082                        }
083                        for (String next : options.getResourceTypes()) {
084                                if (!myResourceTypes.contains(next)) {
085                                        return new AuthorizationInterceptor.Verdict(PolicyEnum.DENY, this);
086                                }
087                        }
088                }
089
090                if (myWantAnyStyle || myWantExportStyle == BulkExportJobParameters.ExportStyle.SYSTEM) {
091                        return newVerdict(
092                                        theOperation,
093                                        theRequestDetails,
094                                        theInputResource,
095                                        theInputResourceId,
096                                        theOutputResource,
097                                        theRuleApplier);
098                }
099
100                if (isNotBlank(myGroupId) && options.getGroupId() != null) {
101                        String expectedGroupId =
102                                        new IdDt(myGroupId).toUnqualifiedVersionless().getValue();
103                        String actualGroupId =
104                                        new IdDt(options.getGroupId()).toUnqualifiedVersionless().getValue();
105                        if (Objects.equals(expectedGroupId, actualGroupId)) {
106                                return newVerdict(
107                                                theOperation,
108                                                theRequestDetails,
109                                                theInputResource,
110                                                theInputResourceId,
111                                                theOutputResource,
112                                                theRuleApplier);
113                        }
114                }
115
116                // 1. If each of the requested resource IDs in the parameters are present in the users permissions, Approve
117                // 2. If any requested ID is not present in the users permissions, Deny.
118                if (myWantExportStyle == BulkExportJobParameters.ExportStyle.PATIENT && isNotEmpty(myPatientIds)) {
119                        List<String> permittedPatientIds = myPatientIds.stream()
120                                        .map(id -> new IdDt(id).toUnqualifiedVersionless().getValue())
121                                        .collect(Collectors.toList());
122                        if (!options.getPatientIds().isEmpty()) {
123                                ourLog.debug("options.getPatientIds() != null");
124                                List<String> requestedPatientIds = options.getPatientIds().stream()
125                                                .map(t -> new IdDt(t).toUnqualifiedVersionless().getValue())
126                                                .collect(Collectors.toList());
127                                boolean requestedPatientsPermitted = true;
128                                for (String requestedPatientId : requestedPatientIds) {
129                                        if (!permittedPatientIds.contains(requestedPatientId)) {
130                                                requestedPatientsPermitted = false;
131                                                break;
132                                        }
133                                }
134                                if (requestedPatientsPermitted) {
135                                        return newVerdict(
136                                                        theOperation,
137                                                        theRequestDetails,
138                                                        theInputResource,
139                                                        theInputResourceId,
140                                                        theOutputResource,
141                                                        theRuleApplier);
142                                }
143
144                                return new AuthorizationInterceptor.Verdict(PolicyEnum.DENY, this);
145                        }
146
147                        final List<String> filters = options.getFilters();
148
149                        if (!filters.isEmpty()) {
150                                ourLog.debug("filters not empty");
151                                final Set<String> patientIdsInFilters = filters.stream()
152                                                .filter(filter -> filter.startsWith("Patient?_id="))
153                                                .map(filter -> filter.replace("?_id=", "/"))
154                                                .collect(Collectors.toUnmodifiableSet());
155
156                                boolean filteredPatientIdsPermitted = true;
157                                for (String patientIdInFilters : patientIdsInFilters) {
158                                        if (!permittedPatientIds.contains(patientIdInFilters)) {
159                                                filteredPatientIdsPermitted = false;
160                                                break;
161                                        }
162                                }
163
164                                if (filteredPatientIdsPermitted) {
165                                        return newVerdict(
166                                                        theOperation,
167                                                        theRequestDetails,
168                                                        theInputResource,
169                                                        theInputResourceId,
170                                                        theOutputResource,
171                                                        theRuleApplier);
172                                }
173
174                                return new AuthorizationInterceptor.Verdict(PolicyEnum.DENY, this);
175                        }
176                        ourLog.debug("patientIds and filters both empty");
177                }
178                return null;
179        }
180
181        public void setAppliesToGroupExportOnGroup(String theGroupId) {
182                myWantExportStyle = BulkExportJobParameters.ExportStyle.GROUP;
183                myGroupId = theGroupId;
184        }
185
186        public void setAppliesToPatientExportOnGroup(String theGroupId) {
187                myWantExportStyle = BulkExportJobParameters.ExportStyle.PATIENT;
188                myGroupId = theGroupId;
189        }
190
191        public void setAppliesToPatientExport(String thePatientId) {
192                myWantExportStyle = BulkExportJobParameters.ExportStyle.PATIENT;
193                myPatientIds.add(thePatientId);
194        }
195
196        public void setAppliesToSystem() {
197                myWantExportStyle = BulkExportJobParameters.ExportStyle.SYSTEM;
198        }
199
200        public void setResourceTypes(Collection<String> theResourceTypes) {
201                myResourceTypes = theResourceTypes;
202        }
203
204        public void setAppliesToAny() {
205                myWantAnyStyle = true;
206        }
207
208        String getGroupId() {
209                return myGroupId;
210        }
211
212        BulkExportJobParameters.ExportStyle getWantExportStyle() {
213                return myWantExportStyle;
214        }
215}