Bump MRI upstreams
[aaa.git] / aaa-shiro / impl / src / main / java / org / opendaylight / aaa / shiro / realm / KeystoneAuthRealm.java
1 /*
2  * Copyright (c) 2017 Ericsson Inc. and others.  All rights reserved.
3  *
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
7  */
8 package org.opendaylight.aaa.shiro.realm;
9
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;
15 import java.net.URI;
16 import java.net.URISyntaxException;
17 import java.net.URL;
18 import java.util.Objects;
19 import java.util.Optional;
20 import java.util.Set;
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;
50
51 /**
52  * KeystoneAuthRealm is a Shiro Realm that authenticates users from
53  * OpenStack Keystone.
54  */
55 public class KeystoneAuthRealm extends AuthorizingRealm {
56
57     private static final Logger LOG = LoggerFactory.getLogger(KeystoneAuthRealm.class);
58
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";
66
67     private static final int CLIENT_EXPIRE_AFTER_ACCESS = 1;
68     private static final int CLIENT_EXPIRE_AFTER_WRITE = 10;
69
70     private volatile URI serverUri = null;
71     private volatile boolean sslVerification = true;
72     private volatile String defaultDomain = DEFAULT_KEYSTONE_DOMAIN;
73
74     private final LoadingCache<Boolean, SimpleHttpClient> clientCache = buildCache();
75
76     private final ICertificateManager certManager;
77
78     public KeystoneAuthRealm() {
79         this.certManager = Objects.requireNonNull(ThreadLocals.CERT_MANAGER_TL.get());
80         LOG.info("KeystoneAuthRealm created");
81     }
82
83     @Override
84     protected AuthorizationInfo doGetAuthorizationInfo(final PrincipalCollection principalCollection) {
85         final Object primaryPrincipal = getAvailablePrincipal(principalCollection);
86         final ODLPrincipal odlPrincipal;
87         try {
88             odlPrincipal = (ODLPrincipal) primaryPrincipal;
89             return new SimpleAuthorizationInfo(odlPrincipal.getRoles());
90         } catch (ClassCastException e) {
91             LOG.error("Couldn't decode authorization request", e);
92         }
93         return new SimpleAuthorizationInfo();
94     }
95
96     @Override
97     @SuppressWarnings("checkstyle:AvoidHidingCauseException")
98     protected AuthenticationInfo doGetAuthenticationInfo(final AuthenticationToken authenticationToken) {
99         try {
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;
107             }
108             throw e;
109         }
110     }
111
112     /**
113      * As {@link #doGetAuthenticationInfo(AuthenticationToken)}
114      * but using the provided {@link SimpleHttpClient} to reach
115      * the Keystone server.
116      *
117      * @param authenticationToken see {@link AuthorizingRealm#doGetAuthenticationInfo(AuthenticationToken)}
118      * @param client the {@link SimpleHttpClient} to use.
119      * @return see {@link AuthorizingRealm#doGetAuthenticationInfo(AuthenticationToken)}
120      */
121     protected AuthenticationInfo doGetAuthenticationInfo(
122             final AuthenticationToken authenticationToken,
123             final SimpleHttpClient client) {
124
125         final URI theServerUri = getServerUri();
126         final String theDefaultDomain = getDefaultDomain();
127
128         if (!(authenticationToken instanceof UsernamePasswordToken)) {
129             LOG.error("Only basic authentication is supported");
130             throw new AuthenticationException(FATAL_ERROR_BASIC_AUTH_ONLY);
131         }
132
133         if (Objects.isNull(theServerUri)) {
134             LOG.error("Invalid URL to Keystone server");
135             throw new AuthenticationException(FATAL_ERROR_INVALID_URL);
136         }
137
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;
144
145         final KeystoneAuth keystoneAuth = new KeystoneAuth(username, password, domain);
146         final SimpleHttpRequest<KeystoneToken> httpRequest = client.requestBuilder(KeystoneToken.class)
147                 .uri(theServerUri)
148                 .path(AUTH_PATH)
149                 .method(HttpMethod.POST)
150                 .mediaType(MediaType.APPLICATION_JSON_TYPE)
151                 .entity(keystoneAuth)
152                 .queryParam(NO_CATALOG_OPTION,"")
153                 .build();
154
155         KeystoneToken theToken;
156         try {
157             theToken = httpRequest.execute();
158         } catch (WebApplicationException e) {
159             LOG.debug("Unable to authenticate - Keystone result code: {}", e.getResponse().getStatus(), e);
160             return null;
161         }
162
163         final Set<String> theRoles = theToken.getToken().getRoles()
164                 .stream()
165                 .map(KeystoneToken.Token.Role::getName)
166                 .collect(Collectors.toSet());
167
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());
171     }
172
173     /**
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.
177      *
178      * @return the cache.
179      */
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>() {
185                     @Override
186                     public SimpleHttpClient load(Boolean withSslVerification) throws Exception {
187                         return buildClient(withSslVerification, certManager, SimpleHttpClient.clientBuilder());
188                     }
189                 });
190     }
191
192     /**
193      * Used to obtain a {@link SimpleHttpClient} that optionally performs SSL
194      * verification.
195      *
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}.
200      */
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();
210         } else {
211             sslContext = UntrustedSSL.getSSLContext();
212             hostnameVerifier = UntrustedSSL.getHostnameVerifier();
213         }
214         return clientBuilder
215                 .hostnameVerifier(hostnameVerifier)
216                 .sslContext(sslContext)
217                 .provider(GsonProvider.class)
218                 .build();
219     }
220
221     private SSLContext getSecureSSLContext(final ICertificateManager certificateManager) {
222         final SSLContext sslContext = Optional.ofNullable(certificateManager)
223                 .map(ICertificateManager::getServerContext)
224                 .orElse(null);
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);
228         }
229         return sslContext;
230     }
231
232     /**
233      * The URI of the Keystone server.
234      *
235      * @return the URI.
236      */
237     public URI getServerUri() {
238         return serverUri;
239     }
240
241     /**
242      * Whether SSL verification is performed or untrusted access is allowed.
243      *
244      * @return the SSL verification flag.
245      */
246     public boolean getSslVerification() {
247         return sslVerification;
248     }
249
250     /**
251      * Default domain to use when no domain is provided within the user
252      * credentials.
253      *
254      * @return the default domain.
255      */
256     public String getDefaultDomain() {
257         return defaultDomain;
258     }
259
260     /**
261      * The URL of the Keystone server. Injected from
262      * <code>shiro.ini</code>.
263      *
264      * @param url the URL specified in <code>shiro.ini</code>.
265      */
266     public void setUrl(final String url) {
267         try {
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);
271             serverUri = null;
272         }
273     }
274
275     /**
276      * Whether SSL verification is performed or untrusted access is allowed.
277      * Injected from <code>shiro.ini</code>.
278      *
279      * @param sslVerification specified in <code>shiro.ini</code>
280      */
281     public void setSslVerification(final boolean sslVerification) {
282         this.sslVerification = sslVerification;
283     }
284
285     /**
286      * Default domain to use when no domain is provided within the user
287      * credentials. Injected from <code>shiro.ini</code>.
288      *
289      * @param defaultDomain specified in <code>shiro.ini</code>
290      */
291     public void setDefaultDomain(final String defaultDomain) {
292         this.defaultDomain = defaultDomain;
293     }
294
295 }