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.binary;
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.rest.api.server.IPreResourceShowDetails;
028import ca.uhn.fhir.rest.api.server.RequestDetails;
029import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
030import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException;
031import ca.uhn.fhir.rest.server.interceptor.auth.AuthorizationConstants;
032import ca.uhn.fhir.util.FhirTerser;
033import org.apache.commons.lang3.Validate;
034import org.hl7.fhir.instance.model.api.IBaseBinary;
035import org.hl7.fhir.instance.model.api.IBaseResource;
036
037import static org.apache.commons.lang3.StringUtils.isNotBlank;
038
039/**
040 * This security interceptor checks any Binary resources that are being exposed to
041 * a user and can forbid the user from accessing them based on the security context
042 * found in <code>Binary.securityContext.identifier</code>.
043 * <p>
044 * This interceptor is intended to be subclassed. The default implementation if it
045 * is not subclassed will reject any access to a Binary resource unless the
046 * request is a system request (using {@link SystemRequestDetails} or the Binary
047 * resource has no value in <code>Binary.securityContext.identifier</code>.
048 * </p>
049 * <p>
050 * Override {@link #securityContextIdentifierAllowed(String, String, RequestDetails)} in order
051 * to allow the user to access specific context values.
052 * </p>
053 *
054 * @since 6.8.0
055 */
056@SuppressWarnings("unused")
057@Interceptor(order = AuthorizationConstants.ORDER_BINARY_SECURITY_INTERCEPTOR)
058public class BinarySecurityContextInterceptor {
059
060        private final FhirContext myFhirContext;
061
062        /**
063         * Constructor
064         *
065         * @param theFhirContext The FHIR context
066         */
067        public BinarySecurityContextInterceptor(FhirContext theFhirContext) {
068                Validate.notNull(theFhirContext, "theFhirContext must not be null");
069                myFhirContext = theFhirContext;
070        }
071
072        /**
073         * Interceptor hook method. Do not call this method directly.
074         */
075        @Hook(Pointcut.STORAGE_PRESHOW_RESOURCES)
076        public void preShowResources(IPreResourceShowDetails theShowDetails, RequestDetails theRequestDetails) {
077                for (IBaseResource nextResource : theShowDetails.getAllResources()) {
078                        if (nextResource instanceof IBaseBinary) {
079                                applyAccessControl((IBaseBinary) nextResource, theRequestDetails);
080                        }
081                }
082        }
083
084        /**
085         * Interceptor hook method. Do not call this method directly.
086         */
087        @Hook(Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED)
088        public void preShowResources(
089                        IBaseResource theOldValue, IBaseResource theNewValue, RequestDetails theRequestDetails) {
090                if (theOldValue instanceof IBaseBinary) {
091                        applyAccessControl((IBaseBinary) theOldValue, theRequestDetails);
092                }
093        }
094
095        /**
096         * This method applies security to a given Binary resource. It is not typically
097         * overridden but you could override it if you wanted to completely replace the
098         * security logic in this interceptor.
099         *
100         * @param theBinary         The Binary resource being checked
101         * @param theRequestDetails The request details associated with this request
102         */
103        protected void applyAccessControl(IBaseBinary theBinary, RequestDetails theRequestDetails) {
104                FhirTerser terser = myFhirContext.newTerser();
105                String securityContextSystem =
106                                terser.getSinglePrimitiveValueOrNull(theBinary, "securityContext.identifier.system");
107                String securityContextValue =
108                                terser.getSinglePrimitiveValueOrNull(theBinary, "securityContext.identifier.value");
109
110                if (isNotBlank(securityContextSystem) || isNotBlank(securityContextValue)) {
111                        applyAccessControl(theBinary, securityContextSystem, securityContextValue, theRequestDetails);
112                }
113        }
114
115        /**
116         * This method applies access controls to a Binary resource containing the
117         * given identifier system and value in the Binary.securityContext element.
118         *
119         * @param theBinary                The binary resource
120         * @param theSecurityContextSystem The identifier system
121         * @param theSecurityContextValue  The identifier value
122         * @param theRequestDetails        The request details
123         */
124        protected void applyAccessControl(
125                        IBaseBinary theBinary,
126                        String theSecurityContextSystem,
127                        String theSecurityContextValue,
128                        RequestDetails theRequestDetails) {
129                if (theRequestDetails instanceof SystemRequestDetails) {
130                        return;
131                }
132                if (securityContextIdentifierAllowed(theSecurityContextSystem, theSecurityContextValue, theRequestDetails)) {
133                        return;
134                }
135
136                handleForbidden(theBinary);
137        }
138
139        /**
140         * Handles a non-permitted operation. This method throws a {@link ForbiddenOperationException}
141         * but you could override it to change that behaviour.
142         */
143        protected void handleForbidden(IBaseBinary theBinary) {
144                throw new ForbiddenOperationException(Msg.code(2369) + "Security context not permitted");
145        }
146
147        /**
148         * Determines whether the current user has access to the given security
149         * context identifier. This method is intended to be overridden, the default
150         * implementation simply always returns <code>false</code>.
151         *
152         * @param theSecurityContextSystem The <code>Binary.securityContext.identifier.system</code> value
153         * @param theSecurityContextValue  The <code>Binary.securityContext.identifier.value</code> value
154         * @param theRequestDetails        The request details associated with this request
155         * @return Returns <code>true</code> if the request should be permitted, and <code>false</code> otherwise
156         */
157        protected boolean securityContextIdentifierAllowed(
158                        String theSecurityContextSystem, String theSecurityContextValue, RequestDetails theRequestDetails) {
159                return false;
160        }
161}