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 com.google.common.cache.CacheBuilder;
11 import com.google.common.cache.CacheLoader;
12 import com.google.common.cache.LoadingCache;
13 import com.google.common.util.concurrent.UncheckedExecutionException;
14 import java.net.MalformedURLException;
16 import java.net.URISyntaxException;
18 import java.util.Objects;
19 import java.util.Optional;
21 import java.util.concurrent.TimeUnit;
22 import java.util.stream.Collectors;
23 import javax.net.ssl.HostnameVerifier;
24 import javax.net.ssl.HttpsURLConnection;
25 import javax.net.ssl.SSLContext;
26 import javax.ws.rs.HttpMethod;
27 import javax.ws.rs.WebApplicationException;
28 import javax.ws.rs.core.MediaType;
29 import org.apache.shiro.authc.AuthenticationException;
30 import org.apache.shiro.authc.AuthenticationInfo;
31 import org.apache.shiro.authc.AuthenticationToken;
32 import org.apache.shiro.authc.SimpleAuthenticationInfo;
33 import org.apache.shiro.authc.UsernamePasswordToken;
34 import org.apache.shiro.authz.AuthorizationInfo;
35 import org.apache.shiro.authz.SimpleAuthorizationInfo;
36 import org.apache.shiro.realm.AuthorizingRealm;
37 import org.apache.shiro.subject.PrincipalCollection;
38 import org.opendaylight.aaa.api.shiro.principal.ODLPrincipal;
39 import org.opendaylight.aaa.cert.api.ICertificateManager;
40 import org.opendaylight.aaa.provider.GsonProvider;
41 import org.opendaylight.aaa.shiro.keystone.domain.KeystoneAuth;
42 import org.opendaylight.aaa.shiro.keystone.domain.KeystoneToken;
43 import org.opendaylight.aaa.shiro.principal.ODLPrincipalImpl;
44 import org.opendaylight.aaa.shiro.realm.util.http.SimpleHttpClient;
45 import org.opendaylight.aaa.shiro.realm.util.http.SimpleHttpRequest;
46 import org.opendaylight.aaa.shiro.realm.util.http.UntrustedSSL;
47 import org.opendaylight.aaa.shiro.web.env.ThreadLocals;
48 import org.slf4j.Logger;
49 import org.slf4j.LoggerFactory;
52 * KeystoneAuthRealm is a Shiro Realm that authenticates users from
55 public class KeystoneAuthRealm extends AuthorizingRealm {
57 private static final Logger LOG = LoggerFactory.getLogger(KeystoneAuthRealm.class);
59 private static final String NO_CATALOG_OPTION = "nocatalog";
60 private static final String DEFAULT_KEYSTONE_DOMAIN = "Default";
61 private static final String USERNAME_DOMAIN_SEPARATOR = "@";
62 private static final String FATAL_ERROR_BASIC_AUTH_ONLY = "{\"error\":\"Only basic authentication is supported\"}";
63 private static final String FATAL_ERROR_INVALID_URL = "{\"error\":\"Invalid URL to Keystone server\"}";
64 private static final String UNABLE_TO_AUTHENTICATE = "{\"error\":\"Could not authenticate\"}";
65 private static final String AUTH_PATH = "v3/auth/tokens";
67 private static final int CLIENT_EXPIRE_AFTER_ACCESS = 1;
68 private static final int CLIENT_EXPIRE_AFTER_WRITE = 10;
70 private volatile URI serverUri = null;
71 private volatile boolean sslVerification = true;
72 private volatile String defaultDomain = DEFAULT_KEYSTONE_DOMAIN;
74 private final LoadingCache<Boolean, SimpleHttpClient> clientCache = buildCache();
76 private final ICertificateManager certManager;
78 public KeystoneAuthRealm() {
79 this.certManager = Objects.requireNonNull(ThreadLocals.CERT_MANAGER_TL.get());
80 LOG.info("KeystoneAuthRealm created");
84 protected AuthorizationInfo doGetAuthorizationInfo(final PrincipalCollection principalCollection) {
85 final Object primaryPrincipal = getAvailablePrincipal(principalCollection);
86 final ODLPrincipal odlPrincipal;
88 odlPrincipal = (ODLPrincipal) primaryPrincipal;
89 return new SimpleAuthorizationInfo(odlPrincipal.getRoles());
90 } catch (ClassCastException e) {
91 LOG.error("Couldn't decode authorization request", e);
93 return new SimpleAuthorizationInfo();
97 @SuppressWarnings("checkstyle:AvoidHidingCauseException")
98 protected AuthenticationInfo doGetAuthenticationInfo(final AuthenticationToken authenticationToken) {
100 final boolean hasSslVerification = getSslVerification();
101 final SimpleHttpClient client = clientCache.getUnchecked(hasSslVerification);
102 return doGetAuthenticationInfo(authenticationToken, client);
103 } catch (UncheckedExecutionException e) {
104 Throwable cause = e.getCause();
105 if (!Objects.isNull(cause) && cause instanceof AuthenticationException) {
106 throw (AuthenticationException) cause;
113 * As {@link #doGetAuthenticationInfo(AuthenticationToken)}
114 * but using the provided {@link SimpleHttpClient} to reach
115 * the Keystone server.
117 * @param authenticationToken see {@link AuthorizingRealm#doGetAuthenticationInfo(AuthenticationToken)}
118 * @param client the {@link SimpleHttpClient} to use.
119 * @return see {@link AuthorizingRealm#doGetAuthenticationInfo(AuthenticationToken)}
121 protected AuthenticationInfo doGetAuthenticationInfo(
122 final AuthenticationToken authenticationToken,
123 final SimpleHttpClient client) {
125 final URI theServerUri = getServerUri();
126 final String theDefaultDomain = getDefaultDomain();
128 if (!(authenticationToken instanceof UsernamePasswordToken)) {
129 LOG.error("Only basic authentication is supported");
130 throw new AuthenticationException(FATAL_ERROR_BASIC_AUTH_ONLY);
133 if (Objects.isNull(theServerUri)) {
134 LOG.error("Invalid URL to Keystone server");
135 throw new AuthenticationException(FATAL_ERROR_INVALID_URL);
138 final UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) authenticationToken;
139 final String qualifiedUser = usernamePasswordToken.getUsername();
140 final String password = new String(usernamePasswordToken.getPassword());
141 final String[] qualifiedUserArray = qualifiedUser.split(USERNAME_DOMAIN_SEPARATOR, 2);
142 final String username = qualifiedUserArray.length > 0 ? qualifiedUserArray[0] : qualifiedUser;
143 final String domain = qualifiedUserArray.length > 1 ? qualifiedUserArray[1] : theDefaultDomain;
145 final KeystoneAuth keystoneAuth = new KeystoneAuth(username, password, domain);
146 final SimpleHttpRequest<KeystoneToken> httpRequest = client.requestBuilder(KeystoneToken.class)
149 .method(HttpMethod.POST)
150 .mediaType(MediaType.APPLICATION_JSON_TYPE)
151 .entity(keystoneAuth)
152 .queryParam(NO_CATALOG_OPTION,"")
155 KeystoneToken theToken;
157 theToken = httpRequest.execute();
158 } catch (WebApplicationException e) {
159 LOG.debug("Unable to authenticate - Keystone result code: {}", e.getResponse().getStatus(), e);
163 final Set<String> theRoles = theToken.getToken().getRoles()
165 .map(KeystoneToken.Token.Role::getName)
166 .collect(Collectors.toSet());
168 final String userId = username + USERNAME_DOMAIN_SEPARATOR + domain;
169 final ODLPrincipal odlPrincipal = ODLPrincipalImpl.createODLPrincipal(username, domain, userId, theRoles);
170 return new SimpleAuthenticationInfo(odlPrincipal, password.toCharArray(), getName());
174 * Used to build a cache of {@link SimpleHttpClient}. In practice, only one
175 * client instance is used at a time but SSL verification flag is used as
176 * key for convenience.
180 protected LoadingCache<Boolean, SimpleHttpClient> buildCache() {
181 return CacheBuilder.newBuilder()
182 .expireAfterAccess(CLIENT_EXPIRE_AFTER_ACCESS, TimeUnit.SECONDS)
183 .expireAfterWrite(CLIENT_EXPIRE_AFTER_WRITE, TimeUnit.SECONDS)
184 .build(new CacheLoader<Boolean, SimpleHttpClient>() {
186 public SimpleHttpClient load(Boolean withSslVerification) throws Exception {
187 return buildClient(withSslVerification, certManager, SimpleHttpClient.clientBuilder());
193 * Used to obtain a {@link SimpleHttpClient} that optionally performs SSL
196 * @param withSslVerification if client should perform SSL verification.
197 * @param certificateManager used to obtain a secure SSL context.
198 * @param clientBuilder uset to build {@link SimpleHttpClient}.
199 * @return the {@link SimpleHttpClient}.
201 protected SimpleHttpClient buildClient(
202 final boolean withSslVerification,
203 final ICertificateManager certificateManager,
204 final SimpleHttpClient.Builder clientBuilder) {
205 final SSLContext sslContext;
206 final HostnameVerifier hostnameVerifier;
207 if (withSslVerification) {
208 sslContext = getSecureSSLContext(certificateManager);
209 hostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier();
211 sslContext = UntrustedSSL.getSSLContext();
212 hostnameVerifier = UntrustedSSL.getHostnameVerifier();
215 .hostnameVerifier(hostnameVerifier)
216 .sslContext(sslContext)
217 .provider(GsonProvider.class)
221 private SSLContext getSecureSSLContext(final ICertificateManager certificateManager) {
222 final SSLContext sslContext = Optional.ofNullable(certificateManager)
223 .map(ICertificateManager::getServerContext)
225 if (Objects.isNull(sslContext)) {
226 LOG.error("Could not get a valid SSL context from certificate manager");
227 throw new AuthenticationException(UNABLE_TO_AUTHENTICATE);
233 * The URI of the Keystone server.
237 public URI getServerUri() {
242 * Whether SSL verification is performed or untrusted access is allowed.
244 * @return the SSL verification flag.
246 public boolean getSslVerification() {
247 return sslVerification;
251 * Default domain to use when no domain is provided within the user
254 * @return the default domain.
256 public String getDefaultDomain() {
257 return defaultDomain;
261 * The URL of the Keystone server. Injected from
262 * <code>shiro.ini</code>.
264 * @param url the URL specified in <code>shiro.ini</code>.
266 public void setUrl(final String url) {
268 serverUri = new URL(url).toURI();
269 } catch (final MalformedURLException | URISyntaxException e) {
270 LOG.error("The keystone server URL {} could not be correctly parsed", url, e);
276 * Whether SSL verification is performed or untrusted access is allowed.
277 * Injected from <code>shiro.ini</code>.
279 * @param sslVerification specified in <code>shiro.ini</code>
281 public void setSslVerification(final boolean sslVerification) {
282 this.sslVerification = sslVerification;
286 * Default domain to use when no domain is provided within the user
287 * credentials. Injected from <code>shiro.ini</code>.
289 * @param defaultDomain specified in <code>shiro.ini</code>
291 public void setDefaultDomain(final String defaultDomain) {
292 this.defaultDomain = defaultDomain;