Add MultiTenantAuthenticationManagerResolver

A class with a number of handy request-based implementations of
AuthenticationManagerResolver targeted at common multi-tenancy
scenarios.

Fixes: gh-6976
This commit is contained in:
Josh Cummings
2019-06-11 21:44:51 -06:00
parent ecb13aa8cc
commit f5da63118e
3 changed files with 338 additions and 10 deletions

View File

@@ -0,0 +1,174 @@
/*
* Copyright 2002-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.authentication;
import java.util.Optional;
import javax.servlet.http.HttpServletRequest;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationManagerResolver;
import org.springframework.util.Assert;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
/**
* An implementation of {@link AuthenticationManagerResolver} that separates the tasks of
* extracting the request's tenant identifier and looking up an {@link AuthenticationManager}
* by that tenant identifier.
*
* @author Josh Cummings
* @since 5.2
* @see AuthenticationManagerResolver
*/
public final class MultiTenantAuthenticationManagerResolver<T> implements AuthenticationManagerResolver<HttpServletRequest> {
private final Converter<HttpServletRequest, AuthenticationManager> authenticationManagerResolver;
/**
* Constructs a {@link MultiTenantAuthenticationManagerResolver} with the provided parameters
*
* @param tenantResolver
* @param authenticationManagerResolver
*/
public MultiTenantAuthenticationManagerResolver
(Converter<HttpServletRequest, T> tenantResolver,
Converter<T, AuthenticationManager> authenticationManagerResolver) {
Assert.notNull(tenantResolver, "tenantResolver cannot be null");
Assert.notNull(authenticationManagerResolver, "authenticationManagerResolver cannot be null");
this.authenticationManagerResolver = request -> {
Optional<T> context = Optional.ofNullable(tenantResolver.convert(request));
return context.map(authenticationManagerResolver::convert)
.orElseThrow(() -> new IllegalArgumentException
("Could not resolve AuthenticationManager by reference " + context.orElse(null)));
};
}
@Override
public AuthenticationManager resolve(HttpServletRequest context) {
return this.authenticationManagerResolver.convert(context);
}
/**
* Creates an {@link AuthenticationManagerResolver} that will use a hostname's first label as
* the resolution key for the underlying {@link AuthenticationManagerResolver}.
*
* For example, you might have a set of {@link AuthenticationManager}s defined like so:
*
* <pre>
* Map<String, AuthenticationManager> authenticationManagers = new HashMap<>();
* authenticationManagers.put("tenantOne", managerOne());
* authenticationManagers.put("tenantTwo", managerTwo());
* </pre>
*
* And that your system serves hostnames like <pre>https://tenantOne.example.org</pre>.
*
* Then, you could create an {@link AuthenticationManagerResolver} that uses the "tenantOne" value from
* the hostname to resolve Tenant One's {@link AuthenticationManager} like so:
*
* <pre>
* AuthenticationManagerResolver<HttpServletRequest> resolver =
* resolveFromSubdomain(authenticationManagers::get);
* </pre>
*
* {@link HttpServletRequest}
* @param resolver A {@link String}-resolving {@link AuthenticationManagerResolver}
* @return A hostname-resolving {@link AuthenticationManagerResolver}
*/
public static AuthenticationManagerResolver<HttpServletRequest>
resolveFromSubdomain(Converter<String, AuthenticationManager> resolver) {
return new MultiTenantAuthenticationManagerResolver<>(request ->
Optional.ofNullable(request.getServerName())
.map(host -> host.split("\\."))
.filter(segments -> segments.length > 0)
.map(segments -> segments[0]).orElse(null), resolver);
}
/**
* Creates an {@link AuthenticationManagerResolver} that will use a request path's first segment as
* the resolution key for the underlying {@link AuthenticationManagerResolver}.
*
* For example, you might have a set of {@link AuthenticationManager}s defined like so:
*
* <pre>
* Map<String, AuthenticationManager> authenticationManagers = new HashMap<>();
* authenticationManagers.put("tenantOne", managerOne());
* authenticationManagers.put("tenantTwo", managerTwo());
* </pre>
*
* And that your system serves requests like <pre>https://example.org/tenantOne</pre>.
*
* Then, you could create an {@link AuthenticationManagerResolver} that uses the "tenantOne" value from
* the request to resolve Tenant One's {@link AuthenticationManager} like so:
*
* <pre>
* AuthenticationManagerResolver<HttpServletRequest> resolver =
* resolveFromPath(authenticationManagers::get);
* </pre>
*
* {@link HttpServletRequest}
* @param resolver A {@link String}-resolving {@link AuthenticationManagerResolver}
* @return A path-resolving {@link AuthenticationManagerResolver}
*/
public static AuthenticationManagerResolver<HttpServletRequest>
resolveFromPath(Converter<String, AuthenticationManager> resolver) {
return new MultiTenantAuthenticationManagerResolver<>(request ->
Optional.ofNullable(request.getRequestURI())
.map(UriComponentsBuilder::fromUriString)
.map(UriComponentsBuilder::build)
.map(UriComponents::getPathSegments)
.filter(segments -> !segments.isEmpty())
.map(segments -> segments.get(0)).orElse(null), resolver);
}
/**
* Creates an {@link AuthenticationManagerResolver} that will use a request headers's value as
* the resolution key for the underlying {@link AuthenticationManagerResolver}.
*
* For example, you might have a set of {@link AuthenticationManager}s defined like so:
*
* <pre>
* Map<String, AuthenticationManager> authenticationManagers = new HashMap<>();
* authenticationManagers.put("tenantOne", managerOne());
* authenticationManagers.put("tenantTwo", managerTwo());
* </pre>
*
* And that your system serves requests with a header like <pre>X-Tenant-Id: tenantOne</pre>.
*
* Then, you could create an {@link AuthenticationManagerResolver} that uses the "tenantOne" value from
* the request to resolve Tenant One's {@link AuthenticationManager} like so:
*
* <pre>
* AuthenticationManagerResolver<HttpServletRequest> resolver =
* resolveFromHeader("X-Tenant-Id", authenticationManagers::get);
* </pre>
*
* {@link HttpServletRequest}
* @param resolver A {@link String}-resolving {@link AuthenticationManagerResolver}
* @return A header-resolving {@link AuthenticationManagerResolver}
*/
public static AuthenticationManagerResolver<HttpServletRequest>
resolveFromHeader(String headerName, Converter<String, AuthenticationManager> resolver) {
return new MultiTenantAuthenticationManagerResolver<>
(request -> request.getHeader(headerName), resolver);
}
}