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.stream.Collectors;
25 import javax.net.ssl.HostnameVerifier;
26 import javax.net.ssl.HttpsURLConnection;
27 import javax.net.ssl.SSLContext;
28 import javax.ws.rs.HttpMethod;
29 import javax.ws.rs.WebApplicationException;
30 import javax.ws.rs.core.MediaType;
31 import org.apache.shiro.authc.AuthenticationException;
32 import org.apache.shiro.authc.AuthenticationInfo;
33 import org.apache.shiro.authc.AuthenticationToken;
34 import org.apache.shiro.authc.SimpleAuthenticationInfo;
35 import org.apache.shiro.authc.UsernamePasswordToken;
36 import org.apache.shiro.authz.AuthorizationInfo;
37 import org.apache.shiro.authz.SimpleAuthorizationInfo;
38 import org.apache.shiro.realm.AuthorizingRealm;
39 import org.apache.shiro.subject.PrincipalCollection;
40 import org.opendaylight.aaa.api.shiro.principal.ODLPrincipal;
41 import org.opendaylight.aaa.cert.api.ICertificateManager;
42 import org.opendaylight.aaa.provider.GsonProvider;
43 import org.opendaylight.aaa.shiro.keystone.domain.KeystoneAuth;
44 import org.opendaylight.aaa.shiro.keystone.domain.KeystoneToken;
45 import org.opendaylight.aaa.shiro.principal.ODLPrincipalImpl;
46 import org.opendaylight.aaa.shiro.realm.util.http.SimpleHttpClient;
47 import org.opendaylight.aaa.shiro.realm.util.http.SimpleHttpRequest;
48 import org.opendaylight.aaa.shiro.realm.util.http.UntrustedSSL;
49 import org.opendaylight.yangtools.concepts.Registration;
50 import org.slf4j.Logger;
51 import org.slf4j.LoggerFactory;
54 * KeystoneAuthRealm is a Shiro Realm that authenticates users from OpenStack Keystone.
56 // Non-final for testing
57 public class KeystoneAuthRealm extends AuthorizingRealm {
58 private static final Logger LOG = LoggerFactory.getLogger(KeystoneAuthRealm.class);
60 private static final String NO_CATALOG_OPTION = "nocatalog";
61 private static final String DEFAULT_KEYSTONE_DOMAIN = "Default";
62 private static final String USERNAME_DOMAIN_SEPARATOR = "@";
63 private static final String FATAL_ERROR_BASIC_AUTH_ONLY = "{\"error\":\"Only basic authentication is supported\"}";
64 private static final String FATAL_ERROR_INVALID_URL = "{\"error\":\"Invalid URL to Keystone server\"}";
65 private static final String UNABLE_TO_AUTHENTICATE = "{\"error\":\"Could not authenticate\"}";
66 private static final String AUTH_PATH = "v3/auth/tokens";
68 private static final int CLIENT_EXPIRE_AFTER_ACCESS = 1;
69 private static final int CLIENT_EXPIRE_AFTER_WRITE = 10;
71 private static final ThreadLocal<ICertificateManager> CERT_MANAGER_TL = new ThreadLocal<>();
73 private volatile URI serverUri = null;
74 private volatile boolean sslVerification = true;
75 private volatile String defaultDomain = DEFAULT_KEYSTONE_DOMAIN;
77 private final ICertificateManager certManager;
78 private final LoadingCache<Boolean, SimpleHttpClient> clientCache = CacheBuilder.newBuilder()
79 .expireAfterAccess(CLIENT_EXPIRE_AFTER_ACCESS, TimeUnit.SECONDS)
80 .expireAfterWrite(CLIENT_EXPIRE_AFTER_WRITE, TimeUnit.SECONDS)
81 .build(new CacheLoader<>() {
83 public SimpleHttpClient load(final Boolean withSslVerification) {
84 return buildClient(withSslVerification, certManager, SimpleHttpClient.clientBuilder());
88 public KeystoneAuthRealm() {
89 this(verifyNotNull(CERT_MANAGER_TL.get(), "KeystoneAuthRealm loading not prepared"));
92 public KeystoneAuthRealm(final ICertificateManager certManager) {
93 this.certManager = requireNonNull(certManager);
94 LOG.info("KeystoneAuthRealm created");
97 public static Registration prepareForLoad(final ICertificateManager certManager) {
98 CERT_MANAGER_TL.set(requireNonNull(certManager));
99 return CERT_MANAGER_TL::remove;
103 protected AuthorizationInfo doGetAuthorizationInfo(final PrincipalCollection principalCollection) {
104 final var primaryPrincipal = getAvailablePrincipal(principalCollection);
105 if (primaryPrincipal instanceof ODLPrincipal) {
106 return new SimpleAuthorizationInfo(((ODLPrincipal) primaryPrincipal).getRoles());
109 LOG.error("Unsupported principal {}", primaryPrincipal);
110 return new SimpleAuthorizationInfo();
114 protected AuthenticationInfo doGetAuthenticationInfo(final AuthenticationToken authenticationToken) {
115 final SimpleHttpClient client;
117 client = clientCache.getUnchecked(getSslVerification());
118 } catch (UncheckedExecutionException e) {
119 Throwables.throwIfInstanceOf(e.getCause(), AuthenticationException.class);
122 return doGetAuthenticationInfo(authenticationToken, client);
126 * As {@link #doGetAuthenticationInfo(AuthenticationToken)}
127 * but using the provided {@link SimpleHttpClient} to reach
128 * the Keystone server.
130 * @param authenticationToken see {@link AuthorizingRealm#doGetAuthenticationInfo(AuthenticationToken)}
131 * @param client the {@link SimpleHttpClient} to use.
132 * @return see {@link AuthorizingRealm#doGetAuthenticationInfo(AuthenticationToken)}
134 protected AuthenticationInfo doGetAuthenticationInfo(
135 final AuthenticationToken authenticationToken,
136 final SimpleHttpClient client) {
138 final URI theServerUri = getServerUri();
139 final String theDefaultDomain = getDefaultDomain();
141 if (!(authenticationToken instanceof UsernamePasswordToken)) {
142 LOG.error("Only basic authentication is supported");
143 throw new AuthenticationException(FATAL_ERROR_BASIC_AUTH_ONLY);
146 if (theServerUri == null) {
147 LOG.error("Invalid URL to Keystone server");
148 throw new AuthenticationException(FATAL_ERROR_INVALID_URL);
151 final UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) authenticationToken;
152 final String qualifiedUser = usernamePasswordToken.getUsername();
153 final String password = new String(usernamePasswordToken.getPassword());
154 final String[] qualifiedUserArray = qualifiedUser.split(USERNAME_DOMAIN_SEPARATOR, 2);
155 final String username = qualifiedUserArray.length > 0 ? qualifiedUserArray[0] : qualifiedUser;
156 final String domain = qualifiedUserArray.length > 1 ? qualifiedUserArray[1] : theDefaultDomain;
158 final KeystoneAuth keystoneAuth = new KeystoneAuth(username, password, domain);
159 final SimpleHttpRequest<KeystoneToken> httpRequest = client.requestBuilder(KeystoneToken.class)
162 .method(HttpMethod.POST)
163 .mediaType(MediaType.APPLICATION_JSON_TYPE)
164 .entity(keystoneAuth)
165 .queryParam(NO_CATALOG_OPTION,"")
168 KeystoneToken theToken;
170 theToken = httpRequest.execute();
171 } catch (WebApplicationException e) {
172 LOG.debug("Unable to authenticate - Keystone result code: {}", e.getResponse().getStatus(), e);
176 final Set<String> theRoles = theToken.getToken().getRoles()
178 .map(KeystoneToken.Token.Role::getName)
179 .collect(Collectors.toSet());
181 final String userId = username + USERNAME_DOMAIN_SEPARATOR + domain;
182 final ODLPrincipal odlPrincipal = ODLPrincipalImpl.createODLPrincipal(username, domain, userId, theRoles);
183 return new SimpleAuthenticationInfo(odlPrincipal, password.toCharArray(), getName());
187 * Used to obtain a {@link SimpleHttpClient} that optionally performs SSL
190 * @param withSslVerification if client should perform SSL verification.
191 * @param certificateManager used to obtain a secure SSL context.
192 * @param clientBuilder uset to build {@link SimpleHttpClient}.
193 * @return the {@link SimpleHttpClient}.
195 protected SimpleHttpClient buildClient(
196 final boolean withSslVerification,
197 final ICertificateManager certificateManager,
198 final SimpleHttpClient.Builder clientBuilder) {
199 final SSLContext sslContext;
200 final HostnameVerifier hostnameVerifier;
201 if (withSslVerification) {
202 sslContext = getSecureSSLContext(certificateManager);
203 hostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier();
205 sslContext = UntrustedSSL.getSSLContext();
206 hostnameVerifier = UntrustedSSL.getHostnameVerifier();
209 .hostnameVerifier(hostnameVerifier)
210 .sslContext(sslContext)
211 .provider(GsonProvider.class)
215 private static SSLContext getSecureSSLContext(final ICertificateManager certificateManager) {
216 if (certificateManager != null) {
217 final SSLContext sslContext = certificateManager.getServerContext();
218 if (sslContext != null) {
223 LOG.error("Could not get a valid SSL context from certificate manager");
224 throw new AuthenticationException(UNABLE_TO_AUTHENTICATE);
228 * The URI of the Keystone server.
232 public URI getServerUri() {
237 * Whether SSL verification is performed or untrusted access is allowed.
239 * @return the SSL verification flag.
241 public boolean getSslVerification() {
242 return sslVerification;
246 * Default domain to use when no domain is provided within the user
249 * @return the default domain.
251 public String getDefaultDomain() {
252 return defaultDomain;
256 * The URL of the Keystone server. Injected from
257 * <code>shiro.ini</code>.
259 * @param url the URL specified in <code>shiro.ini</code>.
261 public void setUrl(final String url) {
263 serverUri = new URL(url).toURI();
264 } catch (final MalformedURLException | URISyntaxException e) {
265 LOG.error("The keystone server URL {} could not be correctly parsed", url, e);
271 * Whether SSL verification is performed or untrusted access is allowed.
272 * Injected from <code>shiro.ini</code>.
274 * @param sslVerification specified in <code>shiro.ini</code>
276 public void setSslVerification(final boolean sslVerification) {
277 this.sslVerification = sslVerification;
281 * Default domain to use when no domain is provided within the user
282 * credentials. Injected from <code>shiro.ini</code>.
284 * @param defaultDomain specified in <code>shiro.ini</code>
286 public void setDefaultDomain(final String defaultDomain) {
287 this.defaultDomain = defaultDomain;