7de57278a83263917843a54787a618195c5355c5
[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) {
123             return new SimpleAuthorizationInfo(((ODLPrincipal) primaryPrincipal).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
155         final URI theServerUri = getServerUri();
156         final String theDefaultDomain = getDefaultDomain();
157
158         if (!(authenticationToken instanceof UsernamePasswordToken)) {
159             LOG.error("Only basic authentication is supported");
160             throw new AuthenticationException(FATAL_ERROR_BASIC_AUTH_ONLY);
161         }
162
163         if (theServerUri == null) {
164             LOG.error("Invalid URL to Keystone server");
165             throw new AuthenticationException(FATAL_ERROR_INVALID_URL);
166         }
167
168         final UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) authenticationToken;
169         final String qualifiedUser = usernamePasswordToken.getUsername();
170         final String password = new String(usernamePasswordToken.getPassword());
171         final String[] qualifiedUserArray = qualifiedUser.split(USERNAME_DOMAIN_SEPARATOR, 2);
172         final String username = qualifiedUserArray.length > 0 ? qualifiedUserArray[0] : qualifiedUser;
173         final String domain = qualifiedUserArray.length > 1 ? qualifiedUserArray[1] : theDefaultDomain;
174
175         final KeystoneAuth keystoneAuth = new KeystoneAuth(username, password, domain);
176         final SimpleHttpRequest<KeystoneToken> httpRequest = client.requestBuilder(KeystoneToken.class)
177                 .uri(theServerUri)
178                 .path(AUTH_PATH)
179                 .method(HttpMethod.POST)
180                 .mediaType(MediaType.APPLICATION_JSON_TYPE)
181                 .entity(keystoneAuth)
182                 .queryParam(NO_CATALOG_OPTION,"")
183                 .build();
184
185         KeystoneToken theToken;
186         try {
187             theToken = httpRequest.execute();
188         } catch (WebApplicationException e) {
189             LOG.debug("Unable to authenticate - Keystone result code: {}", e.getResponse().getStatus(), e);
190             return null;
191         }
192
193         final Set<String> theRoles = theToken.getToken().getRoles()
194                 .stream()
195                 .map(KeystoneToken.Token.Role::getName)
196                 .collect(Collectors.toSet());
197
198         final String userId = username + USERNAME_DOMAIN_SEPARATOR + domain;
199         final ODLPrincipal odlPrincipal = ODLPrincipalImpl.createODLPrincipal(username, domain, userId, theRoles);
200         return new SimpleAuthenticationInfo(odlPrincipal, password.toCharArray(), getName());
201     }
202
203     /**
204      * Used to obtain a {@link SimpleHttpClient} that optionally performs SSL
205      * verification.
206      *
207      * @param withSslVerification if client should perform SSL verification.
208      * @param certificateManager used to obtain a secure SSL context.
209      * @param clientBuilder uset to build {@link SimpleHttpClient}.
210      * @return the {@link SimpleHttpClient}.
211      */
212     protected SimpleHttpClient buildClient(
213             final boolean withSslVerification,
214             final ICertificateManager certificateManager,
215             final SimpleHttpClient.Builder clientBuilder) {
216         final SSLContext sslContext;
217         final HostnameVerifier hostnameVerifier;
218         if (withSslVerification) {
219             sslContext = getSecureSSLContext(certificateManager);
220             hostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier();
221         } else {
222             sslContext = UntrustedSSL.getSSLContext();
223             hostnameVerifier = UntrustedSSL.getHostnameVerifier();
224         }
225         return clientBuilder
226                 .hostnameVerifier(hostnameVerifier)
227                 .sslContext(sslContext)
228                 .provider(GsonProvider.class)
229                 .build();
230     }
231
232     private static SSLContext getSecureSSLContext(final ICertificateManager certificateManager) {
233         if (certificateManager != null) {
234             final SSLContext sslContext = certificateManager.getServerContext();
235             if (sslContext != null) {
236                 return sslContext;
237             }
238         }
239
240         LOG.error("Could not get a valid SSL context from certificate manager");
241         throw new AuthenticationException(UNABLE_TO_AUTHENTICATE);
242     }
243
244     /**
245      * The URI of the Keystone server.
246      *
247      * @return the URI.
248      */
249     public URI getServerUri() {
250         return serverUri;
251     }
252
253     /**
254      * Whether SSL verification is performed or untrusted access is allowed.
255      *
256      * @return the SSL verification flag.
257      */
258     public boolean getSslVerification() {
259         return sslVerification;
260     }
261
262     /**
263      * Default domain to use when no domain is provided within the user
264      * credentials.
265      *
266      * @return the default domain.
267      */
268     public String getDefaultDomain() {
269         return defaultDomain;
270     }
271
272     /**
273      * The URL of the Keystone server. Injected from
274      * <code>shiro.ini</code>.
275      *
276      * @param url the URL specified in <code>shiro.ini</code>.
277      */
278     public void setUrl(final String url) {
279         try {
280             serverUri = new URL(url).toURI();
281         } catch (final MalformedURLException | URISyntaxException e) {
282             LOG.error("The keystone server URL {} could not be correctly parsed", url, e);
283             serverUri = null;
284         }
285     }
286
287     /**
288      * Whether SSL verification is performed or untrusted access is allowed.
289      * Injected from <code>shiro.ini</code>.
290      *
291      * @param sslVerification specified in <code>shiro.ini</code>
292      */
293     public void setSslVerification(final boolean sslVerification) {
294         this.sslVerification = sslVerification;
295     }
296
297     /**
298      * Default domain to use when no domain is provided within the user
299      * credentials. Injected from <code>shiro.ini</code>.
300      *
301      * @param defaultDomain specified in <code>shiro.ini</code>
302      */
303     public void setDefaultDomain(final String defaultDomain) {
304         this.defaultDomain = defaultDomain;
305     }
306
307 }