001package ca.uhn.fhir.rest.server.interceptor.consent;
002
003/*-
004 * #%L
005 * HAPI FHIR - Server Framework
006 * %%
007 * Copyright (C) 2014 - 2019 University Health Network
008 * %%
009 * Licensed under the Apache License, Version 2.0 (the "License");
010 * you may not use this file except in compliance with the License.
011 * You may obtain a copy of the License at
012 *
013 *      http://www.apache.org/licenses/LICENSE-2.0
014 *
015 * Unless required by applicable law or agreed to in writing, software
016 * distributed under the License is distributed on an "AS IS" BASIS,
017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
018 * See the License for the specific language governing permissions and
019 * limitations under the License.
020 * #L%
021 */
022
023import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
024import ca.uhn.fhir.context.BaseRuntimeElementDefinition;
025import ca.uhn.fhir.context.FhirContext;
026import ca.uhn.fhir.interceptor.api.Hook;
027import ca.uhn.fhir.interceptor.api.Interceptor;
028import ca.uhn.fhir.interceptor.api.Pointcut;
029import ca.uhn.fhir.rest.api.Constants;
030import ca.uhn.fhir.rest.api.server.IPreResourceAccessDetails;
031import ca.uhn.fhir.rest.api.server.IPreResourceShowDetails;
032import ca.uhn.fhir.rest.api.server.RequestDetails;
033import ca.uhn.fhir.rest.api.server.ResponseDetails;
034import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
035import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException;
036import ca.uhn.fhir.rest.server.util.ICachedSearchDetails;
037import ca.uhn.fhir.util.BundleUtil;
038import ca.uhn.fhir.util.IModelVisitor2;
039import org.apache.commons.lang3.Validate;
040import org.hl7.fhir.instance.model.api.*;
041
042import java.util.IdentityHashMap;
043import java.util.List;
044import java.util.Map;
045import java.util.concurrent.atomic.AtomicInteger;
046
047@Interceptor
048public class ConsentInterceptor {
049        private static final AtomicInteger ourInstanceCount = new AtomicInteger(0);
050        private final int myInstanceIndex = ourInstanceCount.incrementAndGet();
051        private final String myRequestAuthorizedKey = ConsentInterceptor.class.getName() + "_" + myInstanceIndex + "_AUTHORIZED";
052        private final String myRequestCompletedKey = ConsentInterceptor.class.getName() + "_" + myInstanceIndex + "_COMPLETED";
053        private final String myRequestSeenResourcesKey = ConsentInterceptor.class.getName() + "_" + myInstanceIndex + "_SEENRESOURCES";
054
055        private IConsentService myConsentService;
056        private IConsentContextServices myContextConsentServices;
057
058        /**
059         * Constructor
060         */
061        public ConsentInterceptor() {
062                super();
063        }
064
065        /**
066         * Constructor
067         *
068         * @param theConsentService         Must not be <code>null</code>
069         */
070        public ConsentInterceptor(IConsentService theConsentService) {
071                this(theConsentService, IConsentContextServices.NULL_IMPL);
072        }
073
074        /**
075         * Constructor
076         *
077         * @param theConsentService         Must not be <code>null</code>
078         * @param theContextConsentServices Must not be <code>null</code>
079         */
080        public ConsentInterceptor(IConsentService theConsentService, IConsentContextServices theContextConsentServices) {
081                setConsentService(theConsentService);
082                setContextConsentServices(theContextConsentServices);
083        }
084
085        public void setContextConsentServices(IConsentContextServices theContextConsentServices) {
086                Validate.notNull(theContextConsentServices, "theContextConsentServices must not be null");
087                myContextConsentServices = theContextConsentServices;
088        }
089
090        public void setConsentService(IConsentService theConsentService) {
091                Validate.notNull(theConsentService, "theConsentService must not be null");
092                myConsentService = theConsentService;
093        }
094
095        @Hook(value = Pointcut.SERVER_INCOMING_REQUEST_PRE_HANDLED)
096        public void interceptPreHandled(RequestDetails theRequestDetails) {
097                ConsentOutcome outcome = myConsentService.startOperation(theRequestDetails, myContextConsentServices);
098                Validate.notNull(outcome, "Consent service returned null outcome");
099
100                switch (outcome.getStatus()) {
101                        case REJECT:
102                                throw toForbiddenOperationException(outcome);
103                        case PROCEED:
104                                break;
105                        case AUTHORIZED:
106                                Map<Object, Object> userData = theRequestDetails.getUserData();
107                                userData.put(myRequestAuthorizedKey, Boolean.TRUE);
108                                break;
109                }
110        }
111
112        @Hook(value = Pointcut.STORAGE_PRECHECK_FOR_CACHED_SEARCH)
113        public boolean interceptPreCheckForCachedSearch(RequestDetails theRequestDetails) {
114                if (isRequestAuthorized(theRequestDetails)) {
115                        return true;
116                }
117                return false;
118        }
119
120        @Hook(value = Pointcut.STORAGE_PRESEARCH_REGISTERED)
121        public void interceptPreSearchRegistered(RequestDetails theRequestDetails, ICachedSearchDetails theCachedSearchDetails) {
122                if (!isRequestAuthorized(theRequestDetails)) {
123                        theCachedSearchDetails.setCannotBeReused();
124                }
125        }
126
127        @Hook(value = Pointcut.STORAGE_PREACCESS_RESOURCES)
128        public void interceptPreAccess(RequestDetails theRequestDetails, IPreResourceAccessDetails thePreResourceAccessDetails) {
129                if (isRequestAuthorized(theRequestDetails)) {
130                        return;
131                }
132
133                for (int i = 0; i < thePreResourceAccessDetails.size(); i++) {
134                        IBaseResource nextResource = thePreResourceAccessDetails.getResource(i);
135                        ConsentOutcome nextOutcome = myConsentService.canSeeResource(theRequestDetails, nextResource, myContextConsentServices);
136                        switch (nextOutcome.getStatus()) {
137                                case PROCEED:
138                                        break;
139                                case AUTHORIZED:
140                                        break;
141                                case REJECT:
142                                        thePreResourceAccessDetails.setDontReturnResourceAtIndex(i);
143                                        break;
144                        }
145                }
146        }
147
148        @Hook(value = Pointcut.STORAGE_PRESHOW_RESOURCES)
149        public void interceptPreShow(RequestDetails theRequestDetails, IPreResourceShowDetails thePreResourceShowDetails) {
150                if (isRequestAuthorized(theRequestDetails)) {
151                        return;
152                }
153                IdentityHashMap<IBaseResource, Boolean> alreadySeenResources = getAlreadySeenResourcesMap(theRequestDetails);
154
155                for (int i = 0; i < thePreResourceShowDetails.size(); i++) {
156                        IBaseResource nextResource = thePreResourceShowDetails.getResource(i);
157                        if (alreadySeenResources.putIfAbsent(nextResource, Boolean.TRUE) != null) {
158                                continue;
159                        }
160
161                        ConsentOutcome nextOutcome = myConsentService.willSeeResource(theRequestDetails, nextResource, myContextConsentServices);
162                        switch (nextOutcome.getStatus()) {
163                                case PROCEED:
164                                        if (nextOutcome.getResource() != null) {
165                                                thePreResourceShowDetails.setResource(i, nextOutcome.getResource());
166                                        }
167                                        break;
168                                case AUTHORIZED:
169                                        break;
170                                case REJECT:
171                                        if (nextOutcome.getResource() != null) {
172                                                IBaseResource newResource = nextOutcome.getResource();
173                                                thePreResourceShowDetails.setResource(i, newResource);
174                                                alreadySeenResources.put(newResource, true);
175                                        } else if (nextOutcome.getOperationOutcome() != null) {
176                                                IBaseOperationOutcome newOperationOutcome = nextOutcome.getOperationOutcome();
177                                                thePreResourceShowDetails.setResource(i, newOperationOutcome);
178                                                alreadySeenResources.put(newOperationOutcome, true);
179                                        } else {
180                                                String resourceId = nextResource.getIdElement().getValue();
181                                                thePreResourceShowDetails.setResource(i, null);
182                                                nextResource.setId(resourceId);
183                                        }
184                                        break;
185                        }
186                }
187        }
188
189        private IdentityHashMap<IBaseResource, Boolean> getAlreadySeenResourcesMap(RequestDetails theRequestDetails) {
190                return getAlreadySeenResourcesMap(theRequestDetails, myRequestSeenResourcesKey);
191        }
192
193        @Hook(value = Pointcut.SERVER_OUTGOING_RESPONSE)
194        public void interceptOutgoingResponse(RequestDetails theRequestDetails, ResponseDetails theResource) {
195                if (theResource.getResponseResource() == null) {
196                        return;
197                }
198                if (isRequestAuthorized(theRequestDetails)) {
199                        return;
200                }
201
202                IdentityHashMap<IBaseResource, Boolean> alreadySeenResources = getAlreadySeenResourcesMap(theRequestDetails);
203
204                // See outer resource
205                if (alreadySeenResources.putIfAbsent(theResource.getResponseResource(), Boolean.TRUE) == null) {
206                        final ConsentOutcome outcome = myConsentService.willSeeResource(theRequestDetails, theResource.getResponseResource(), myContextConsentServices);
207                        if (outcome.getResource() != null) {
208                                theResource.setResponseResource(outcome.getResource());
209                        }
210
211                        switch (outcome.getStatus()) {
212                                case REJECT:
213                                        if (outcome.getOperationOutcome() != null) {
214                                                theResource.setResponseResource(outcome.getOperationOutcome());
215                                        } else {
216                                                theResource.setResponseResource(null);
217                                                theResource.setResponseCode(Constants.STATUS_HTTP_204_NO_CONTENT);
218                                        }
219                                        return;
220                                case AUTHORIZED:
221                                        // Don't check children
222                                        return;
223                                case PROCEED:
224                                        // Check children
225                                        break;
226                        }
227                }
228
229                // See child resources
230                IBaseResource outerResource = theResource.getResponseResource();
231                FhirContext ctx = theRequestDetails.getServer().getFhirContext();
232                IModelVisitor2 visitor = new IModelVisitor2() {
233                        @Override
234                        public boolean acceptElement(IBase theElement, List<IBase> theContainingElementPath, List<BaseRuntimeChildDefinition> theChildDefinitionPath, List<BaseRuntimeElementDefinition<?>> theElementDefinitionPath) {
235
236                                // Clear the total
237                                if (theElement instanceof IBaseBundle) {
238                                        BundleUtil.setTotal(theRequestDetails.getFhirContext(), (IBaseBundle) theElement, null);
239                                }
240
241                                if (theElement == outerResource) {
242                                        return true;
243                                }
244                                if (theElement instanceof IBaseResource) {
245                                        if (alreadySeenResources.putIfAbsent((IBaseResource) theElement, Boolean.TRUE) != null) {
246                                                return true;
247                                        }
248                                        ConsentOutcome childOutcome = myConsentService.willSeeResource(theRequestDetails, (IBaseResource) theElement, myContextConsentServices);
249
250                                        IBaseResource replacementResource = null;
251                                        boolean shouldReplaceResource = false;
252                                        boolean shouldCheckChildren = false;
253
254                                        switch (childOutcome.getStatus()) {
255                                                case REJECT:
256                                                        replacementResource = childOutcome.getOperationOutcome();
257                                                        shouldReplaceResource = true;
258                                                        break;
259                                                case PROCEED:
260                                                case AUTHORIZED:
261                                                        replacementResource = childOutcome.getResource();
262                                                        shouldReplaceResource = replacementResource != null;
263                                                        shouldCheckChildren = childOutcome.getStatus() == ConsentOperationStatusEnum.PROCEED;
264                                                        break;
265                                        }
266
267                                        if (shouldReplaceResource) {
268                                                IBase container = theContainingElementPath.get(theContainingElementPath.size() - 2);
269                                                BaseRuntimeChildDefinition containerChildElement = theChildDefinitionPath.get(theChildDefinitionPath.size() - 1);
270                                                containerChildElement.getMutator().setValue(container, replacementResource);
271                                        }
272
273                                        return shouldCheckChildren;
274                                }
275
276                                return true;
277                        }
278
279                        @Override
280                        public boolean acceptUndeclaredExtension(IBaseExtension<?, ?> theNextExt, List<IBase> theContainingElementPath, List<BaseRuntimeChildDefinition> theChildDefinitionPath, List<BaseRuntimeElementDefinition<?>> theElementDefinitionPath) {
281                                return true;
282                        }
283                };
284                ctx.newTerser().visit(outerResource, visitor);
285
286        }
287
288        @Hook(value = Pointcut.SERVER_HANDLE_EXCEPTION)
289        public void requestFailed(RequestDetails theRequest, BaseServerResponseException theException) {
290                theRequest.getUserData().put(myRequestCompletedKey, Boolean.TRUE);
291                myConsentService.completeOperationFailure(theRequest, theException, myContextConsentServices);
292        }
293
294        @Hook(value = Pointcut.SERVER_PROCESSING_COMPLETED_NORMALLY)
295        public void requestSucceeded(RequestDetails theRequest) {
296                if (Boolean.TRUE.equals(theRequest.getUserData().get(myRequestCompletedKey))) {
297                        return;
298                }
299                myConsentService.completeOperationSuccess(theRequest, myContextConsentServices);
300        }
301
302        private boolean isRequestAuthorized(RequestDetails theRequestDetails) {
303                boolean retVal = false;
304                if (theRequestDetails != null) {
305                        Object authorizedObj = theRequestDetails.getUserData().get(myRequestAuthorizedKey);
306                        retVal = Boolean.TRUE.equals(authorizedObj);
307                }
308                return retVal;
309        }
310
311        @SuppressWarnings("unchecked")
312        public static IdentityHashMap<IBaseResource, Boolean> getAlreadySeenResourcesMap(RequestDetails theRequestDetails, String theKey) {
313                IdentityHashMap<IBaseResource, Boolean> alreadySeenResources = (IdentityHashMap<IBaseResource, Boolean>) theRequestDetails.getUserData().get(theKey);
314                if (alreadySeenResources == null) {
315                        alreadySeenResources = new IdentityHashMap<>();
316                        theRequestDetails.getUserData().put(theKey, alreadySeenResources);
317                }
318                return alreadySeenResources;
319        }
320
321        private static ForbiddenOperationException toForbiddenOperationException(ConsentOutcome theOutcome) {
322                IBaseOperationOutcome operationOutcome = null;
323                if (theOutcome.getOperationOutcome() != null) {
324                        operationOutcome = theOutcome.getOperationOutcome();
325                }
326                return new ForbiddenOperationException("Rejected by consent service", operationOutcome);
327        }
328}