001/*-
002 * #%L
003 * HAPI FHIR Storage api
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.jpa.interceptor;
021
022import ca.uhn.fhir.context.FhirContext;
023import ca.uhn.fhir.i18n.Msg;
024import ca.uhn.fhir.interceptor.api.Hook;
025import ca.uhn.fhir.interceptor.api.Interceptor;
026import ca.uhn.fhir.interceptor.api.Pointcut;
027import ca.uhn.fhir.jpa.searchparam.extractor.ISearchParamExtractor;
028import ca.uhn.fhir.jpa.util.ResourceCompartmentUtil;
029import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
030import ca.uhn.fhir.util.StopWatch;
031import org.apache.commons.lang3.StringUtils;
032import org.hl7.fhir.instance.model.api.IBaseResource;
033import org.slf4j.Logger;
034import org.slf4j.LoggerFactory;
035
036import static org.apache.commons.lang3.StringUtils.EMPTY;
037
038/**
039 * This interceptor can be used to block resource updates which would make resource patient compartment change.
040 * <p/>
041 * This could be used when the JPA server has partitioning enabled, and Tenant Identification Strategy is PATIENT_ID.
042 */
043@Interceptor
044public class PatientCompartmentEnforcingInterceptor {
045        private static final Logger ourLog = LoggerFactory.getLogger(PatientCompartmentEnforcingInterceptor.class);
046
047        private final FhirContext myFhirContext;
048        private final ISearchParamExtractor mySearchParamExtractor;
049
050        public PatientCompartmentEnforcingInterceptor(
051                        FhirContext theFhirContext, ISearchParamExtractor theSearchParamExtractor) {
052                myFhirContext = theFhirContext;
053                mySearchParamExtractor = theSearchParamExtractor;
054        }
055
056        /**
057         * Blocks resource updates which would make the resource change Patient Compartment.
058         * @param theOldResource the original resource state
059         * @param theResource the updated resource state
060         */
061        @Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED)
062        public void storagePreStorageResourceUpdated(IBaseResource theOldResource, IBaseResource theResource) {
063
064                ourLog.info("Interceptor STORAGE_PRESTORAGE_RESOURCE_UPDATED - started");
065                StopWatch stopWatch = new StopWatch();
066                try {
067                        String patientCompartmentOld = ResourceCompartmentUtil.getPatientCompartmentIdentity(
068                                                        theOldResource, myFhirContext, mySearchParamExtractor)
069                                        .orElse(EMPTY);
070                        String patientCompartmentCurrent = ResourceCompartmentUtil.getPatientCompartmentIdentity(
071                                                        theResource, myFhirContext, mySearchParamExtractor)
072                                        .orElse(EMPTY);
073
074                        if (!StringUtils.equals(patientCompartmentOld, patientCompartmentCurrent)) {
075                                // Avoid disclosing compartments in message, which could have security implications
076                                throw new InternalErrorException(
077                                                Msg.code(2476) + "Resource compartment changed. Was a referenced Patient changed?");
078                        }
079
080                } finally {
081                        ourLog.info("Interceptor STORAGE_PRESTORAGE_RESOURCE_UPDATED - ended, execution took {}", stopWatch);
082                }
083        }
084}