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.tenant;
021
022import ca.uhn.fhir.i18n.HapiLocalizer;
023import ca.uhn.fhir.i18n.Msg;
024import ca.uhn.fhir.rest.api.server.RequestDetails;
025import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
026import ca.uhn.fhir.rest.server.RestfulServer;
027import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
028import ca.uhn.fhir.util.UrlPathTokenizer;
029import org.slf4j.Logger;
030import org.slf4j.LoggerFactory;
031
032import static org.apache.commons.lang3.StringUtils.defaultIfBlank;
033
034/**
035 * This class is a tenant identification strategy which assumes that a single path
036 * element will be present between the server base URL and individual request.
037 * <p>
038 * For example,
039 * with this strategy enabled, given the following URL on a server with base URL <code>http://example.com/base</code>,
040 * the server will extract the <code>TENANT-A</code> portion of the URL and use it as the tenant identifier. The
041 * request will then proceed to read the resource with ID <code>Patient/123</code>.
042 * </p>
043 * <p>
044 * GET http://example.com/base/TENANT-A/Patient/123
045 * </p>
046 */
047public class UrlBaseTenantIdentificationStrategy implements ITenantIdentificationStrategy {
048
049        private static final Logger ourLog = LoggerFactory.getLogger(UrlBaseTenantIdentificationStrategy.class);
050
051        @Override
052        public void extractTenant(UrlPathTokenizer theUrlPathTokenizer, RequestDetails theRequestDetails) {
053                String tenantId = null;
054                boolean isSystemRequest = (theRequestDetails instanceof SystemRequestDetails);
055
056                // If we were given no partition for a system request, use DEFAULT:
057                if (!theUrlPathTokenizer.hasMoreTokens()) {
058                        if (isSystemRequest) {
059                                tenantId = "DEFAULT";
060                                theRequestDetails.setTenantId(tenantId);
061                                ourLog.trace("No tenant ID found for system request; using DEFAULT.");
062                        }
063                }
064
065                // We were given at least one URL token:
066                else {
067
068                        // peek() won't consume this token:
069                        tenantId = defaultIfBlank(theUrlPathTokenizer.peek(), null);
070
071                        // If it's "metadata" or starts with "$", use DEFAULT partition and don't consume this token:
072                        if (tenantId != null && (tenantId.equals("metadata") || isOperation(tenantId))) {
073                                tenantId = "DEFAULT";
074                                theRequestDetails.setTenantId(tenantId);
075                                ourLog.trace("No tenant ID found for metadata or system request; using DEFAULT.");
076                        }
077
078                        // It isn't metadata or $, so assume that this first token is the partition name and consume it:
079                        else {
080                                tenantId = defaultIfBlank(theUrlPathTokenizer.nextTokenUnescapedAndSanitized(), null);
081                                if (tenantId != null) {
082                                        theRequestDetails.setTenantId(tenantId);
083                                        ourLog.trace("Found tenant ID {} in request string", tenantId);
084                                }
085                        }
086                }
087
088                // If we get to this point without a tenant, it's an invalid request:
089                if (tenantId == null) {
090                        HapiLocalizer localizer =
091                                        theRequestDetails.getServer().getFhirContext().getLocalizer();
092                        throw new InvalidRequestException(
093                                        Msg.code(307) + localizer.getMessage(RestfulServer.class, "rootRequest.multitenant"));
094                }
095        }
096
097        private boolean isOperation(String theToken) {
098                return theToken.startsWith("$");
099        }
100
101        @Override
102        public String massageServerBaseUrl(String theFhirServerBase, RequestDetails theRequestDetails) {
103                String result = theFhirServerBase;
104                if (theRequestDetails.getTenantId() != null) {
105                        result += "/" + theRequestDetails.getTenantId();
106                }
107                return result;
108        }
109
110        @Override
111        public String resolveRelativeUrl(String theRelativeUrl, RequestDetails theRequestDetails) {
112                UrlPathTokenizer tokenizer = new UrlPathTokenizer(theRelativeUrl);
113                // there is no more tokens in the URL - skip url resolution
114                if (!tokenizer.hasMoreTokens() || tokenizer.peek() == null) {
115                        return theRelativeUrl;
116                }
117                String nextToken = tokenizer.peek();
118                // there is no tenant ID in parent request details or tenant ID is already present in URL - skip url resolution
119                if (theRequestDetails.getTenantId() == null || nextToken.equals(theRequestDetails.getTenantId())) {
120                        return theRelativeUrl;
121                }
122
123                // token is Resource type or operation - adding tenant ID from parent request details
124                if (isResourceType(nextToken, theRequestDetails) || isOperation(nextToken)) {
125                        return theRequestDetails.getTenantId() + "/" + theRelativeUrl;
126                } else {
127                        return theRelativeUrl;
128                }
129        }
130
131        private boolean isResourceType(String token, RequestDetails theRequestDetails) {
132                return theRequestDetails.getFhirContext().getResourceTypes().stream().anyMatch(type -> type.equals(token));
133        }
134}