2 * Copyright (c) 2017 Ericsson Inc. and others. All rights reserved.
4 * This program and the accompanying materials are made available under the
5 * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6 * and is available at http://www.eclipse.org/legal/epl-v10.html
8 package org.opendaylight.aaa.shiro.realm;
10 import static com.google.common.base.Verify.verifyNotNull;
11 import static java.util.Objects.requireNonNull;
13 import com.google.common.base.Throwables;
14 import com.google.common.cache.CacheBuilder;
15 import com.google.common.cache.CacheLoader;
16 import com.google.common.cache.LoadingCache;
17 import com.google.common.util.concurrent.UncheckedExecutionException;
18 import java.net.MalformedURLException;
20 import java.net.URISyntaxException;
23 import java.util.concurrent.TimeUnit;
24 import java.util.function.Supplier;
25 import java.util.stream.Collectors;
26 import javax.net.ssl.HostnameVerifier;
27 import javax.net.ssl.HttpsURLConnection;
28 import javax.net.ssl.SSLContext;
29 import javax.ws.rs.HttpMethod;
30 import javax.ws.rs.WebApplicationException;
31 import javax.ws.rs.client.ClientBuilder;
32 import javax.ws.rs.core.MediaType;
33 import org.apache.shiro.authc.AuthenticationException;
34 import org.apache.shiro.authc.AuthenticationInfo;
35 import org.apache.shiro.authc.AuthenticationToken;
36 import org.apache.shiro.authc.SimpleAuthenticationInfo;
37 import org.apache.shiro.authc.UsernamePasswordToken;
38 import org.apache.shiro.authz.AuthorizationInfo;
39 import org.apache.shiro.authz.SimpleAuthorizationInfo;
40 import org.apache.shiro.realm.AuthorizingRealm;
41 import org.apache.shiro.subject.PrincipalCollection;
42 import org.opendaylight.aaa.api.shiro.principal.ODLPrincipal;
43 import org.opendaylight.aaa.cert.api.ICertificateManager;
44 import org.opendaylight.aaa.provider.GsonProvider;
45 import org.opendaylight.aaa.shiro.keystone.domain.KeystoneAuth;
46 import org.opendaylight.aaa.shiro.keystone.domain.KeystoneToken;
47 import org.opendaylight.aaa.shiro.principal.ODLPrincipalImpl;
48 import org.opendaylight.aaa.shiro.realm.util.http.SimpleHttpClient;
49 import org.opendaylight.aaa.shiro.realm.util.http.SimpleHttpRequest;
50 import org.opendaylight.aaa.shiro.realm.util.http.UntrustedSSL;
51 import org.opendaylight.aaa.web.servlet.ServletSupport;
52 import org.opendaylight.yangtools.concepts.Registration;
53 import org.slf4j.Logger;
54 import org.slf4j.LoggerFactory;
57 * KeystoneAuthRealm is a Shiro Realm that authenticates users from OpenStack Keystone.
59 // Non-final for testing
60 public class KeystoneAuthRealm extends AuthorizingRealm {
61 private static final Logger LOG = LoggerFactory.getLogger(KeystoneAuthRealm.class);
63 private static final String NO_CATALOG_OPTION = "nocatalog";
64 private static final String DEFAULT_KEYSTONE_DOMAIN = "Default";
65 private static final String USERNAME_DOMAIN_SEPARATOR = "@";
66 private static final String FATAL_ERROR_BASIC_AUTH_ONLY = "{\"error\":\"Only basic authentication is supported\"}";
67 private static final String FATAL_ERROR_INVALID_URL = "{\"error\":\"Invalid URL to Keystone server\"}";
68 private static final String UNABLE_TO_AUTHENTICATE = "{\"error\":\"Could not authenticate\"}";
69 private static final String AUTH_PATH = "v3/auth/tokens";
71 private static final int CLIENT_EXPIRE_AFTER_ACCESS = 1;
72 private static final int CLIENT_EXPIRE_AFTER_WRITE = 10;
74 private static final ThreadLocal<ICertificateManager> CERT_MANAGER_TL = new ThreadLocal<>();
75 private static final ThreadLocal<Supplier<ClientBuilder>> SERVLET_SUPPORT_TL = new ThreadLocal<>();
77 private volatile URI serverUri = null;
78 private volatile boolean sslVerification = true;
79 private volatile String defaultDomain = DEFAULT_KEYSTONE_DOMAIN;
81 private final Supplier<ClientBuilder> clientBuilderFactory;
82 private final ICertificateManager certManager;
83 private final LoadingCache<Boolean, SimpleHttpClient> clientCache = CacheBuilder.newBuilder()
84 .expireAfterAccess(CLIENT_EXPIRE_AFTER_ACCESS, TimeUnit.SECONDS)
85 .expireAfterWrite(CLIENT_EXPIRE_AFTER_WRITE, TimeUnit.SECONDS)
86 .build(new CacheLoader<>() {
88 public SimpleHttpClient load(final Boolean withSslVerification) {
89 return buildClient(withSslVerification, certManager,
90 SimpleHttpClient.clientBuilder(clientBuilderFactory.get()));
94 public KeystoneAuthRealm() {
95 this(requireThreadLocal(CERT_MANAGER_TL), requireThreadLocal(SERVLET_SUPPORT_TL));
98 private static <T> T requireThreadLocal(final ThreadLocal<T> threadLocal) {
99 return verifyNotNull(threadLocal.get(), "KeystoneAuthRealm loading not prepared");
102 public KeystoneAuthRealm(final ICertificateManager certManager,
103 final Supplier<ClientBuilder> clientBuilderFactory) {
104 this.certManager = requireNonNull(certManager);
105 this.clientBuilderFactory = requireNonNull(clientBuilderFactory);
106 LOG.info("KeystoneAuthRealm created");
109 public static Registration prepareForLoad(final ICertificateManager certManager,
110 final ServletSupport servletSupport) {
111 CERT_MANAGER_TL.set(requireNonNull(certManager));
112 SERVLET_SUPPORT_TL.set(requireNonNull(servletSupport)::newClientBuilder);
114 CERT_MANAGER_TL.remove();
115 SERVLET_SUPPORT_TL.remove();
120 protected AuthorizationInfo doGetAuthorizationInfo(final PrincipalCollection principalCollection) {
121 final var primaryPrincipal = getAvailablePrincipal(principalCollection);
122 if (primaryPrincipal instanceof ODLPrincipal odlPrincipal) {
123 return new SimpleAuthorizationInfo(odlPrincipal.getRoles());
126 LOG.error("Unsupported principal {}", primaryPrincipal);
127 return new SimpleAuthorizationInfo();
131 protected AuthenticationInfo doGetAuthenticationInfo(final AuthenticationToken authenticationToken) {
132 final SimpleHttpClient client;
134 client = clientCache.getUnchecked(getSslVerification());
135 } catch (UncheckedExecutionException e) {
136 Throwables.throwIfInstanceOf(e.getCause(), AuthenticationException.class);
139 return doGetAuthenticationInfo(authenticationToken, client);
143 * As {@link #doGetAuthenticationInfo(AuthenticationToken)}
144 * but using the provided {@link SimpleHttpClient} to reach
145 * the Keystone server.
147 * @param authenticationToken see {@link AuthorizingRealm#doGetAuthenticationInfo(AuthenticationToken)}
148 * @param client the {@link SimpleHttpClient} to use.
149 * @return see {@link AuthorizingRealm#doGetAuthenticationInfo(AuthenticationToken)}
151 protected AuthenticationInfo doGetAuthenticationInfo(
152 final AuthenticationToken authenticationToken,
153 final SimpleHttpClient client) {
154 if (!(authenticationToken instanceof UsernamePasswordToken usernamePasswordToken)) {
155 LOG.error("Only basic authentication is supported");
156 throw new AuthenticationException(FATAL_ERROR_BASIC_AUTH_ONLY);
159 final URI theServerUri = getServerUri();
160 if (theServerUri == null) {
161 LOG.error("Invalid URL to Keystone server");
162 throw new AuthenticationException(FATAL_ERROR_INVALID_URL);
165 final String qualifiedUser = usernamePasswordToken.getUsername();
166 final String password = new String(usernamePasswordToken.getPassword());
167 final String[] qualifiedUserArray = qualifiedUser.split(USERNAME_DOMAIN_SEPARATOR, 2);
168 final String username = qualifiedUserArray.length > 0 ? qualifiedUserArray[0] : qualifiedUser;
169 final String domain = qualifiedUserArray.length > 1 ? qualifiedUserArray[1] : getDefaultDomain();
171 final KeystoneAuth keystoneAuth = new KeystoneAuth(username, password, domain);
172 final SimpleHttpRequest<KeystoneToken> httpRequest = client.requestBuilder(KeystoneToken.class)
175 .method(HttpMethod.POST)
176 .mediaType(MediaType.APPLICATION_JSON_TYPE)
177 .entity(keystoneAuth)
178 .queryParam(NO_CATALOG_OPTION,"")
181 KeystoneToken theToken;
183 theToken = httpRequest.execute();
184 } catch (WebApplicationException e) {
185 LOG.debug("Unable to authenticate - Keystone result code: {}", e.getResponse().getStatus(), e);
189 final Set<String> theRoles = theToken.getToken().getRoles()
191 .map(KeystoneToken.Token.Role::getName)
192 .collect(Collectors.toSet());
194 final String userId = username + USERNAME_DOMAIN_SEPARATOR + domain;
195 final ODLPrincipal odlPrincipal = ODLPrincipalImpl.createODLPrincipal(username, domain, userId, theRoles);
196 return new SimpleAuthenticationInfo(odlPrincipal, password.toCharArray(), getName());
200 * Used to obtain a {@link SimpleHttpClient} that optionally performs SSL
203 * @param withSslVerification if client should perform SSL verification.
204 * @param certificateManager used to obtain a secure SSL context.
205 * @param clientBuilder uset to build {@link SimpleHttpClient}.
206 * @return the {@link SimpleHttpClient}.
208 protected SimpleHttpClient buildClient(
209 final boolean withSslVerification,
210 final ICertificateManager certificateManager,
211 final SimpleHttpClient.Builder clientBuilder) {
212 final SSLContext sslContext;
213 final HostnameVerifier hostnameVerifier;
214 if (withSslVerification) {
215 sslContext = getSecureSSLContext(certificateManager);
216 hostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier();
218 sslContext = UntrustedSSL.getSSLContext();
219 hostnameVerifier = UntrustedSSL.getHostnameVerifier();
222 .hostnameVerifier(hostnameVerifier)
223 .sslContext(sslContext)
224 .provider(GsonProvider.class)
228 private static SSLContext getSecureSSLContext(final ICertificateManager certificateManager) {
229 if (certificateManager != null) {
230 final SSLContext sslContext = certificateManager.getServerContext();
231 if (sslContext != null) {
236 LOG.error("Could not get a valid SSL context from certificate manager");
237 throw new AuthenticationException(UNABLE_TO_AUTHENTICATE);
241 * The URI of the Keystone server.
245 public URI getServerUri() {
250 * Whether SSL verification is performed or untrusted access is allowed.
252 * @return the SSL verification flag.
254 public boolean getSslVerification() {
255 return sslVerification;
259 * Default domain to use when no domain is provided within the user
262 * @return the default domain.
264 public String getDefaultDomain() {
265 return defaultDomain;
269 * The URL of the Keystone server. Injected from
270 * <code>shiro.ini</code>.
272 * @param url the URL specified in <code>shiro.ini</code>.
274 public void setUrl(final String url) {
276 serverUri = new URL(url).toURI();
277 } catch (final MalformedURLException | URISyntaxException e) {
278 LOG.error("The keystone server URL {} could not be correctly parsed", url, e);
284 * Whether SSL verification is performed or untrusted access is allowed.
285 * Injected from <code>shiro.ini</code>.
287 * @param sslVerification specified in <code>shiro.ini</code>
289 public void setSslVerification(final boolean sslVerification) {
290 this.sslVerification = sslVerification;
294 * Default domain to use when no domain is provided within the user
295 * credentials. Injected from <code>shiro.ini</code>.
297 * @param defaultDomain specified in <code>shiro.ini</code>
299 public void setDefaultDomain(final String defaultDomain) {
300 this.defaultDomain = defaultDomain;