Migrate CERT_MANAGER_TL
[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.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;
52
53 /**
54  * KeystoneAuthRealm is a Shiro Realm that authenticates users from OpenStack Keystone.
55  */
56 // Non-final for testing
57 public class KeystoneAuthRealm extends AuthorizingRealm {
58     private static final Logger LOG = LoggerFactory.getLogger(KeystoneAuthRealm.class);
59
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";
67
68     private static final int CLIENT_EXPIRE_AFTER_ACCESS = 1;
69     private static final int CLIENT_EXPIRE_AFTER_WRITE = 10;
70
71     private static final ThreadLocal<ICertificateManager> CERT_MANAGER_TL = new ThreadLocal<>();
72
73     private volatile URI serverUri = null;
74     private volatile boolean sslVerification = true;
75     private volatile String defaultDomain = DEFAULT_KEYSTONE_DOMAIN;
76
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<>() {
82             @Override
83             public SimpleHttpClient load(final Boolean withSslVerification) {
84                 return buildClient(withSslVerification, certManager, SimpleHttpClient.clientBuilder());
85             }
86         });
87
88     public KeystoneAuthRealm() {
89         this(verifyNotNull(CERT_MANAGER_TL.get(), "KeystoneAuthRealm loading not prepared"));
90     }
91
92     public KeystoneAuthRealm(final ICertificateManager certManager) {
93         this.certManager = requireNonNull(certManager);
94         LOG.info("KeystoneAuthRealm created");
95     }
96
97     public static Registration prepareForLoad(final ICertificateManager certManager) {
98         CERT_MANAGER_TL.set(requireNonNull(certManager));
99         return CERT_MANAGER_TL::remove;
100     }
101
102     @Override
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());
107         }
108
109         LOG.error("Unsupported principal {}", primaryPrincipal);
110         return new SimpleAuthorizationInfo();
111     }
112
113     @Override
114     protected AuthenticationInfo doGetAuthenticationInfo(final AuthenticationToken authenticationToken) {
115         final SimpleHttpClient client;
116         try {
117             client = clientCache.getUnchecked(getSslVerification());
118         } catch (UncheckedExecutionException e) {
119             Throwables.throwIfInstanceOf(e.getCause(), AuthenticationException.class);
120             throw e;
121         }
122         return doGetAuthenticationInfo(authenticationToken, client);
123     }
124
125     /**
126      * As {@link #doGetAuthenticationInfo(AuthenticationToken)}
127      * but using the provided {@link SimpleHttpClient} to reach
128      * the Keystone server.
129      *
130      * @param authenticationToken see {@link AuthorizingRealm#doGetAuthenticationInfo(AuthenticationToken)}
131      * @param client the {@link SimpleHttpClient} to use.
132      * @return see {@link AuthorizingRealm#doGetAuthenticationInfo(AuthenticationToken)}
133      */
134     protected AuthenticationInfo doGetAuthenticationInfo(
135             final AuthenticationToken authenticationToken,
136             final SimpleHttpClient client) {
137
138         final URI theServerUri = getServerUri();
139         final String theDefaultDomain = getDefaultDomain();
140
141         if (!(authenticationToken instanceof UsernamePasswordToken)) {
142             LOG.error("Only basic authentication is supported");
143             throw new AuthenticationException(FATAL_ERROR_BASIC_AUTH_ONLY);
144         }
145
146         if (theServerUri == null) {
147             LOG.error("Invalid URL to Keystone server");
148             throw new AuthenticationException(FATAL_ERROR_INVALID_URL);
149         }
150
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;
157
158         final KeystoneAuth keystoneAuth = new KeystoneAuth(username, password, domain);
159         final SimpleHttpRequest<KeystoneToken> httpRequest = client.requestBuilder(KeystoneToken.class)
160                 .uri(theServerUri)
161                 .path(AUTH_PATH)
162                 .method(HttpMethod.POST)
163                 .mediaType(MediaType.APPLICATION_JSON_TYPE)
164                 .entity(keystoneAuth)
165                 .queryParam(NO_CATALOG_OPTION,"")
166                 .build();
167
168         KeystoneToken theToken;
169         try {
170             theToken = httpRequest.execute();
171         } catch (WebApplicationException e) {
172             LOG.debug("Unable to authenticate - Keystone result code: {}", e.getResponse().getStatus(), e);
173             return null;
174         }
175
176         final Set<String> theRoles = theToken.getToken().getRoles()
177                 .stream()
178                 .map(KeystoneToken.Token.Role::getName)
179                 .collect(Collectors.toSet());
180
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());
184     }
185
186     /**
187      * Used to obtain a {@link SimpleHttpClient} that optionally performs SSL
188      * verification.
189      *
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}.
194      */
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();
204         } else {
205             sslContext = UntrustedSSL.getSSLContext();
206             hostnameVerifier = UntrustedSSL.getHostnameVerifier();
207         }
208         return clientBuilder
209                 .hostnameVerifier(hostnameVerifier)
210                 .sslContext(sslContext)
211                 .provider(GsonProvider.class)
212                 .build();
213     }
214
215     private static SSLContext getSecureSSLContext(final ICertificateManager certificateManager) {
216         if (certificateManager != null) {
217             final SSLContext sslContext = certificateManager.getServerContext();
218             if (sslContext != null) {
219                 return sslContext;
220             }
221         }
222
223         LOG.error("Could not get a valid SSL context from certificate manager");
224         throw new AuthenticationException(UNABLE_TO_AUTHENTICATE);
225     }
226
227     /**
228      * The URI of the Keystone server.
229      *
230      * @return the URI.
231      */
232     public URI getServerUri() {
233         return serverUri;
234     }
235
236     /**
237      * Whether SSL verification is performed or untrusted access is allowed.
238      *
239      * @return the SSL verification flag.
240      */
241     public boolean getSslVerification() {
242         return sslVerification;
243     }
244
245     /**
246      * Default domain to use when no domain is provided within the user
247      * credentials.
248      *
249      * @return the default domain.
250      */
251     public String getDefaultDomain() {
252         return defaultDomain;
253     }
254
255     /**
256      * The URL of the Keystone server. Injected from
257      * <code>shiro.ini</code>.
258      *
259      * @param url the URL specified in <code>shiro.ini</code>.
260      */
261     public void setUrl(final String url) {
262         try {
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);
266             serverUri = null;
267         }
268     }
269
270     /**
271      * Whether SSL verification is performed or untrusted access is allowed.
272      * Injected from <code>shiro.ini</code>.
273      *
274      * @param sslVerification specified in <code>shiro.ini</code>
275      */
276     public void setSslVerification(final boolean sslVerification) {
277         this.sslVerification = sslVerification;
278     }
279
280     /**
281      * Default domain to use when no domain is provided within the user
282      * credentials. Injected from <code>shiro.ini</code>.
283      *
284      * @param defaultDomain specified in <code>shiro.ini</code>
285      */
286     public void setDefaultDomain(final String defaultDomain) {
287         this.defaultDomain = defaultDomain;
288     }
289
290 }