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}