Use pattern match on instanceof
[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 static com.google.common.base.Verify.verifyNotNull;
11 import static java.util.Objects.requireNonNull;
12
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;
19 import java.net.URI;
20 import java.net.URISyntaxException;
21 import java.net.URL;
22 import java.util.Set;
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;
55
56 /**
57  * KeystoneAuthRealm is a Shiro Realm that authenticates users from OpenStack Keystone.
58  */
59 // Non-final for testing
60 public class KeystoneAuthRealm extends AuthorizingRealm {
61     private static final Logger LOG = LoggerFactory.getLogger(KeystoneAuthRealm.class);
62
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";
70
71     private static final int CLIENT_EXPIRE_AFTER_ACCESS = 1;
72     private static final int CLIENT_EXPIRE_AFTER_WRITE = 10;
73
74     private static final ThreadLocal<ICertificateManager> CERT_MANAGER_TL = new ThreadLocal<>();
75     private static final ThreadLocal<Supplier<ClientBuilder>> SERVLET_SUPPORT_TL = new ThreadLocal<>();
76
77     private volatile URI serverUri = null;
78     private volatile boolean sslVerification = true;
79     private volatile String defaultDomain = DEFAULT_KEYSTONE_DOMAIN;
80
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<>() {
87             @Override
88             public SimpleHttpClient load(final Boolean withSslVerification) {
89                 return buildClient(withSslVerification, certManager,
90                     SimpleHttpClient.clientBuilder(clientBuilderFactory.get()));
91             }
92         });
93
94     public KeystoneAuthRealm() {
95         this(requireThreadLocal(CERT_MANAGER_TL), requireThreadLocal(SERVLET_SUPPORT_TL));
96     }
97
98     private static <T> T requireThreadLocal(final ThreadLocal<T> threadLocal) {
99         return verifyNotNull(threadLocal.get(), "KeystoneAuthRealm loading not prepared");
100     }
101
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");
107     }
108
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);
113         return () -> {
114             CERT_MANAGER_TL.remove();
115             SERVLET_SUPPORT_TL.remove();
116         };
117     }
118
119     @Override
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());
124         }
125
126         LOG.error("Unsupported principal {}", primaryPrincipal);
127         return new SimpleAuthorizationInfo();
128     }
129
130     @Override
131     protected AuthenticationInfo doGetAuthenticationInfo(final AuthenticationToken authenticationToken) {
132         final SimpleHttpClient client;
133         try {
134             client = clientCache.getUnchecked(getSslVerification());
135         } catch (UncheckedExecutionException e) {
136             Throwables.throwIfInstanceOf(e.getCause(), AuthenticationException.class);
137             throw e;
138         }
139         return doGetAuthenticationInfo(authenticationToken, client);
140     }
141
142     /**
143      * As {@link #doGetAuthenticationInfo(AuthenticationToken)}
144      * but using the provided {@link SimpleHttpClient} to reach
145      * the Keystone server.
146      *
147      * @param authenticationToken see {@link AuthorizingRealm#doGetAuthenticationInfo(AuthenticationToken)}
148      * @param client the {@link SimpleHttpClient} to use.
149      * @return see {@link AuthorizingRealm#doGetAuthenticationInfo(AuthenticationToken)}
150      */
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);
157         }
158
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);
163         }
164
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();
170
171         final KeystoneAuth keystoneAuth = new KeystoneAuth(username, password, domain);
172         final SimpleHttpRequest<KeystoneToken> httpRequest = client.requestBuilder(KeystoneToken.class)
173                 .uri(theServerUri)
174                 .path(AUTH_PATH)
175                 .method(HttpMethod.POST)
176                 .mediaType(MediaType.APPLICATION_JSON_TYPE)
177                 .entity(keystoneAuth)
178                 .queryParam(NO_CATALOG_OPTION,"")
179                 .build();
180
181         KeystoneToken theToken;
182         try {
183             theToken = httpRequest.execute();
184         } catch (WebApplicationException e) {
185             LOG.debug("Unable to authenticate - Keystone result code: {}", e.getResponse().getStatus(), e);
186             return null;
187         }
188
189         final Set<String> theRoles = theToken.getToken().getRoles()
190                 .stream()
191                 .map(KeystoneToken.Token.Role::getName)
192                 .collect(Collectors.toSet());
193
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());
197     }
198
199     /**
200      * Used to obtain a {@link SimpleHttpClient} that optionally performs SSL
201      * verification.
202      *
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}.
207      */
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();
217         } else {
218             sslContext = UntrustedSSL.getSSLContext();
219             hostnameVerifier = UntrustedSSL.getHostnameVerifier();
220         }
221         return clientBuilder
222                 .hostnameVerifier(hostnameVerifier)
223                 .sslContext(sslContext)
224                 .provider(GsonProvider.class)
225                 .build();
226     }
227
228     private static SSLContext getSecureSSLContext(final ICertificateManager certificateManager) {
229         if (certificateManager != null) {
230             final SSLContext sslContext = certificateManager.getServerContext();
231             if (sslContext != null) {
232                 return sslContext;
233             }
234         }
235
236         LOG.error("Could not get a valid SSL context from certificate manager");
237         throw new AuthenticationException(UNABLE_TO_AUTHENTICATE);
238     }
239
240     /**
241      * The URI of the Keystone server.
242      *
243      * @return the URI.
244      */
245     public URI getServerUri() {
246         return serverUri;
247     }
248
249     /**
250      * Whether SSL verification is performed or untrusted access is allowed.
251      *
252      * @return the SSL verification flag.
253      */
254     public boolean getSslVerification() {
255         return sslVerification;
256     }
257
258     /**
259      * Default domain to use when no domain is provided within the user
260      * credentials.
261      *
262      * @return the default domain.
263      */
264     public String getDefaultDomain() {
265         return defaultDomain;
266     }
267
268     /**
269      * The URL of the Keystone server. Injected from
270      * <code>shiro.ini</code>.
271      *
272      * @param url the URL specified in <code>shiro.ini</code>.
273      */
274     public void setUrl(final String url) {
275         try {
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);
279             serverUri = null;
280         }
281     }
282
283     /**
284      * Whether SSL verification is performed or untrusted access is allowed.
285      * Injected from <code>shiro.ini</code>.
286      *
287      * @param sslVerification specified in <code>shiro.ini</code>
288      */
289     public void setSslVerification(final boolean sslVerification) {
290         this.sslVerification = sslVerification;
291     }
292
293     /**
294      * Default domain to use when no domain is provided within the user
295      * credentials. Injected from <code>shiro.ini</code>.
296      *
297      * @param defaultDomain specified in <code>shiro.ini</code>
298      */
299     public void setDefaultDomain(final String defaultDomain) {
300         this.defaultDomain = defaultDomain;
301     }
302
303 }