001package ca.uhn.fhir.jpa.subscription.submit.interceptor;
002
003/*-
004 * #%L
005 * HAPI FHIR Subscription Server
006 * %%
007 * Copyright (C) 2014 - 2022 Smile CDR, Inc.
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.FhirContext;
024import ca.uhn.fhir.i18n.Msg;
025import ca.uhn.fhir.interceptor.api.Hook;
026import ca.uhn.fhir.interceptor.api.Interceptor;
027import ca.uhn.fhir.interceptor.api.Pointcut;
028import ca.uhn.fhir.interceptor.model.RequestPartitionId;
029import ca.uhn.fhir.jpa.api.config.DaoConfig;
030import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
031import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc;
032import ca.uhn.fhir.jpa.partition.SystemRequestDetails;
033import ca.uhn.fhir.jpa.subscription.match.matcher.matching.SubscriptionMatchingStrategy;
034import ca.uhn.fhir.jpa.subscription.match.matcher.matching.SubscriptionStrategyEvaluator;
035import ca.uhn.fhir.jpa.subscription.match.matcher.subscriber.SubscriptionCriteriaParser;
036import ca.uhn.fhir.jpa.subscription.match.registry.SubscriptionCanonicalizer;
037import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscription;
038import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscriptionChannelType;
039import ca.uhn.fhir.parser.DataFormatException;
040import ca.uhn.fhir.rest.api.EncodingEnum;
041import ca.uhn.fhir.rest.api.server.RequestDetails;
042import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
043import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
044import ca.uhn.fhir.util.HapiExtensions;
045import ca.uhn.fhir.util.SubscriptionUtil;
046import com.google.common.annotations.VisibleForTesting;
047import org.hl7.fhir.instance.model.api.IBaseResource;
048import org.springframework.beans.factory.annotation.Autowired;
049
050import java.net.URI;
051import java.net.URISyntaxException;
052import java.util.Objects;
053
054import static org.apache.commons.lang3.StringUtils.isBlank;
055@Interceptor
056public class SubscriptionValidatingInterceptor {
057
058        @Autowired
059        private SubscriptionCanonicalizer mySubscriptionCanonicalizer;
060        @Autowired
061        private DaoRegistry myDaoRegistry;
062        @Autowired
063        private DaoConfig myDaoConfig;
064        @Autowired
065        private SubscriptionStrategyEvaluator mySubscriptionStrategyEvaluator;
066        @Autowired
067        private FhirContext myFhirContext;
068        @Autowired
069        private IRequestPartitionHelperSvc myRequestPartitionHelperSvc;
070
071        @Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED)
072        public void resourcePreCreate(IBaseResource theResource, RequestDetails theRequestDetails) {
073                validateSubmittedSubscription(theResource, theRequestDetails);
074        }
075
076        @Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED)
077        public void resourcePreCreate(IBaseResource theOldResource, IBaseResource theResource, RequestDetails theRequestDetails) {
078                validateSubmittedSubscription(theResource, theRequestDetails);
079        }
080
081        @VisibleForTesting
082        public void setFhirContextForUnitTest(FhirContext theFhirContext) {
083                myFhirContext = theFhirContext;
084        }
085
086        @Deprecated
087        public void validateSubmittedSubscription(IBaseResource theSubscription) {
088                validateSubmittedSubscription(theSubscription, null);
089        }
090
091        public void validateSubmittedSubscription(IBaseResource theSubscription, RequestDetails theRequestDetails) {
092                if (!"Subscription".equals(myFhirContext.getResourceType(theSubscription))) {
093                        return;
094                }
095
096                CanonicalSubscription subscription = mySubscriptionCanonicalizer.canonicalize(theSubscription);
097                boolean finished = false;
098                if (subscription.getStatus() == null) {
099                        throw new UnprocessableEntityException(Msg.code(8) + "Can not process submitted Subscription - Subscription.status must be populated on this server");
100                }
101
102                switch (subscription.getStatus()) {
103                        case REQUESTED:
104                        case ACTIVE:
105                                break;
106                        case ERROR:
107                        case OFF:
108                        case NULL:
109                                finished = true;
110                                break;
111                }
112
113                // If the subscription has the cross partition tag &&
114                if (SubscriptionUtil.isCrossPartition(theSubscription) && !(theRequestDetails instanceof SystemRequestDetails)) {
115                        if (!myDaoConfig.isCrossPartitionSubscription()){
116                                throw new UnprocessableEntityException(Msg.code(2009) + "Cross partition subscription is not enabled on this server");
117                        }
118
119                        if (!determinePartition(theRequestDetails, theSubscription).isDefaultPartition()) {
120                                throw new UnprocessableEntityException(Msg.code(2010) + "Cross partition subscription must be created on the default partition");
121                        }
122                }
123
124                mySubscriptionCanonicalizer.setMatchingStrategyTag(theSubscription, null);
125
126                if (!finished) {
127
128                        validateQuery(subscription.getCriteriaString(), "Subscription.criteria");
129
130                        if (subscription.getPayloadSearchCriteria() != null) {
131                                validateQuery(subscription.getPayloadSearchCriteria(), "Subscription.extension(url='" + HapiExtensions.EXT_SUBSCRIPTION_PAYLOAD_SEARCH_CRITERIA + "')");
132                        }
133
134                        validateChannelType(subscription);
135
136                        try {
137                                SubscriptionMatchingStrategy strategy = mySubscriptionStrategyEvaluator.determineStrategy(subscription.getCriteriaString());
138                                mySubscriptionCanonicalizer.setMatchingStrategyTag(theSubscription, strategy);
139                        } catch (InvalidRequestException | DataFormatException e) {
140                                throw new UnprocessableEntityException(Msg.code(9) + "Invalid subscription criteria submitted: " + subscription.getCriteriaString() + " " + e.getMessage());
141                        }
142
143                        if (subscription.getChannelType() == null) {
144                                throw new UnprocessableEntityException(Msg.code(10) + "Subscription.channel.type must be populated on this server");
145                        } else if (subscription.getChannelType() == CanonicalSubscriptionChannelType.MESSAGE) {
146                                validateMessageSubscriptionEndpoint(subscription.getEndpointUrl());
147                        }
148
149
150                }
151        }
152
153        private RequestPartitionId determinePartition(RequestDetails theRequestDetails, IBaseResource theResource) {
154                switch (theRequestDetails.getRestOperationType()) {
155                        case CREATE:
156                                return myRequestPartitionHelperSvc.determineCreatePartitionForRequest(theRequestDetails, theResource, "Subscription");
157                        case UPDATE:
158                                return myRequestPartitionHelperSvc.determineReadPartitionForRequestForRead(theRequestDetails, "Subscription", theResource.getIdElement());
159                        default:
160                                return null;
161                }
162        }
163
164        public void validateQuery(String theQuery, String theFieldName) {
165                if (isBlank(theQuery)) {
166                        throw new UnprocessableEntityException(Msg.code(11) + theFieldName + " must be populated");
167                }
168
169                SubscriptionCriteriaParser.SubscriptionCriteria parsedCriteria = SubscriptionCriteriaParser.parse(theQuery);
170                if (parsedCriteria == null) {
171                        throw new UnprocessableEntityException(Msg.code(12) + theFieldName + " can not be parsed");
172                }
173
174                if (parsedCriteria.getType() == SubscriptionCriteriaParser.TypeEnum.STARTYPE_EXPRESSION) {
175                        return;
176                }
177
178                for (String next : parsedCriteria.getApplicableResourceTypes()) {
179                        if (!myDaoRegistry.isResourceTypeSupported(next)) {
180                                throw new UnprocessableEntityException(Msg.code(13) + theFieldName + " contains invalid/unsupported resource type: " + next);
181                        }
182                }
183
184                if (parsedCriteria.getType() != SubscriptionCriteriaParser.TypeEnum.SEARCH_EXPRESSION) {
185                        return;
186                }
187
188                int sep = theQuery.indexOf('?');
189                if (sep <= 1) {
190                        throw new UnprocessableEntityException(Msg.code(14) + theFieldName + " must be in the form \"{Resource Type}?[params]\"");
191                }
192
193                String resType = theQuery.substring(0, sep);
194                if (resType.contains("/")) {
195                        throw new UnprocessableEntityException(Msg.code(15) + theFieldName + " must be in the form \"{Resource Type}?[params]\"");
196                }
197
198        }
199
200        public void validateMessageSubscriptionEndpoint(String theEndpointUrl) {
201                if (theEndpointUrl == null) {
202                        throw new UnprocessableEntityException(Msg.code(16) + "No endpoint defined for message subscription");
203                }
204
205                try {
206                        URI uri = new URI(theEndpointUrl);
207
208                        if (!"channel".equals(uri.getScheme())) {
209                                throw new UnprocessableEntityException(Msg.code(17) + "Only 'channel' protocol is supported for Subscriptions with channel type 'message'");
210                        }
211                        String channelName = uri.getSchemeSpecificPart();
212                        if (isBlank(channelName)) {
213                                throw new UnprocessableEntityException(Msg.code(18) + "A channel name must appear after channel: in a message Subscription endpoint");
214                        }
215                } catch (URISyntaxException e) {
216                        throw new UnprocessableEntityException(Msg.code(19) + "Invalid subscription endpoint uri " + theEndpointUrl, e);
217                }
218        }
219
220        @SuppressWarnings("WeakerAccess")
221        protected void validateChannelType(CanonicalSubscription theSubscription) {
222                if (theSubscription.getChannelType() == null) {
223                        throw new UnprocessableEntityException(Msg.code(20) + "Subscription.channel.type must be populated");
224                } else if (theSubscription.getChannelType() == CanonicalSubscriptionChannelType.RESTHOOK) {
225                        validateChannelPayload(theSubscription);
226                        validateChannelEndpoint(theSubscription);
227                }
228        }
229
230        @SuppressWarnings("WeakerAccess")
231        protected void validateChannelEndpoint(CanonicalSubscription theResource) {
232                if (isBlank(theResource.getEndpointUrl())) {
233                        throw new UnprocessableEntityException(Msg.code(21) + "Rest-hook subscriptions must have Subscription.channel.endpoint defined");
234                }
235        }
236
237        @SuppressWarnings("WeakerAccess")
238        protected void validateChannelPayload(CanonicalSubscription theResource) {
239                if (!isBlank(theResource.getPayloadString()) && EncodingEnum.forContentType(theResource.getPayloadString()) == null) {
240                        throw new UnprocessableEntityException(Msg.code(1985) + "Invalid value for Subscription.channel.payload: " + theResource.getPayloadString());
241                }
242        }
243
244        @SuppressWarnings("WeakerAccess")
245        @VisibleForTesting
246        public void setSubscriptionCanonicalizerForUnitTest(SubscriptionCanonicalizer theSubscriptionCanonicalizer) {
247                mySubscriptionCanonicalizer = theSubscriptionCanonicalizer;
248        }
249
250        @SuppressWarnings("WeakerAccess")
251        @VisibleForTesting
252        public void setDaoRegistryForUnitTest(DaoRegistry theDaoRegistry) {
253                myDaoRegistry = theDaoRegistry;
254        }
255
256        @VisibleForTesting
257        public void setDaoConfigForUnitTest(DaoConfig theDaoConfig) {
258                myDaoConfig = theDaoConfig;
259        }
260
261        @VisibleForTesting
262        public void setRequestPartitionHelperSvcForUnitTest(IRequestPartitionHelperSvc theRequestPartitionHelperSvc) {
263                myRequestPartitionHelperSvc = theRequestPartitionHelperSvc;
264        }
265
266
267        @VisibleForTesting
268        @SuppressWarnings("WeakerAccess")
269        public void setSubscriptionStrategyEvaluatorForUnitTest(SubscriptionStrategyEvaluator theSubscriptionStrategyEvaluator) {
270                mySubscriptionStrategyEvaluator = theSubscriptionStrategyEvaluator;
271        }
272
273}