4bd67c3180a335b4e109b1996db36a6e01b1ce07
[aaa.git] / aaa-shiro / impl / src / main / java / org / opendaylight / aaa / shiro / realm / MoonRealm.java
1 /*
2  * Copyright (c) 2016 Orange 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.Preconditions.checkArgument;
11 import static com.google.common.base.Verify.verifyNotNull;
12 import static java.util.Objects.requireNonNull;
13
14 import com.google.common.collect.ImmutableSet;
15 import com.google.gson.JsonParser;
16 import java.net.MalformedURLException;
17 import java.net.URL;
18 import javax.ws.rs.client.Entity;
19 import javax.ws.rs.client.WebTarget;
20 import javax.ws.rs.core.MediaType;
21 import org.apache.shiro.authc.AuthenticationException;
22 import org.apache.shiro.authc.AuthenticationInfo;
23 import org.apache.shiro.authc.AuthenticationToken;
24 import org.apache.shiro.authc.SimpleAuthenticationInfo;
25 import org.apache.shiro.authc.UsernamePasswordToken;
26 import org.apache.shiro.authz.AuthorizationInfo;
27 import org.apache.shiro.realm.AuthorizingRealm;
28 import org.apache.shiro.subject.PrincipalCollection;
29 import org.opendaylight.aaa.shiro.moon.MoonPrincipal;
30 import org.opendaylight.aaa.web.servlet.ServletSupport;
31 import org.opendaylight.yangtools.concepts.Registration;
32 import org.slf4j.Logger;
33 import org.slf4j.LoggerFactory;
34
35 /**
36  * MoonRealm is a Shiro Realm that authenticates users from OPNFV/moon platform.
37  *
38  * @author Alioune BA alioune.ba@orange.com
39  */
40 public class MoonRealm extends AuthorizingRealm {
41     private static final Logger LOG = LoggerFactory.getLogger(MoonRealm.class);
42     private static final ThreadLocal<ServletSupport> SERVLET_SUPPORT_TL = new ThreadLocal<>();
43     private static final String MOON_DEFAULT_DOMAIN = "sdn";
44
45     private final ServletSupport servletSupport;
46     private volatile WebTarget moonServer;
47
48     public MoonRealm() {
49         this(verifyNotNull(SERVLET_SUPPORT_TL.get(), "MoonRealm loading not prepared"));
50     }
51
52     public MoonRealm(final ServletSupport servletSupport) {
53         this.servletSupport = requireNonNull(servletSupport);
54     }
55
56     public static Registration prepareForLoad(final ServletSupport jaxrsSupport) {
57         SERVLET_SUPPORT_TL.set(requireNonNull(jaxrsSupport));
58         return SERVLET_SUPPORT_TL::remove;
59     }
60
61     @Override
62     protected AuthorizationInfo doGetAuthorizationInfo(final PrincipalCollection principalCollection) {
63         return null;
64     }
65
66     @Override
67     protected AuthenticationInfo doGetAuthenticationInfo(final AuthenticationToken authenticationToken)
68             throws AuthenticationException {
69         final var principal = authenticationToken.getPrincipal();
70         if (!(principal instanceof String)) {
71             throw new AuthenticationException("Non-string principal " + principal);
72         }
73
74         if (!(authenticationToken instanceof UsernamePasswordToken)) {
75             throw new AuthenticationException("Token is not UsernamePasswordToken: " + authenticationToken);
76         }
77
78         final var password = new String(((UsernamePasswordToken) authenticationToken).getPassword());
79         // FIXME: make the domain name configurable
80         final var moonPrincipal = moonAuthenticate((String) principal, password, MOON_DEFAULT_DOMAIN);
81         return moonPrincipal == null ? null
82             : new SimpleAuthenticationInfo(moonPrincipal, password.toCharArray(), getName());
83     }
84
85     public MoonPrincipal moonAuthenticate(final String username, final String password, final String domain) {
86         final var moon = moonServer;
87         if (moon == null) {
88             LOG.debug("moon server not specified, cannot authenticate");
89             return null;
90         }
91
92         final var element = JsonParser.parseString(moon.request(MediaType.APPLICATION_JSON).post(
93             // FIXME: String literal when we have JDK17
94             Entity.entity("{\"username\": \"" + username + "\",\n"
95                 + "  \"password\": \"" + password + "\",\n"
96                 + "  \"project\": \"" + domain + "\"\n}",
97                 MediaType.APPLICATION_JSON),
98             String.class));
99         if (!element.isJsonObject()) {
100             throw new IllegalStateException("Authentication error: returned output is not a JSON object");
101         }
102
103         final var object = element.getAsJsonObject();
104         final var error = object.get("error").getAsJsonObject();
105         if (error != null) {
106             throw new IllegalStateException("Authentication Error : " + error.get("title").getAsString());
107         }
108
109         final var token = object.get("token");
110         if (token == null) {
111             return null;
112         }
113
114         final var userRoles = ImmutableSet.<String>builder();
115         final var roles = object.get("roles");
116         if (roles != null) {
117             for (var role : roles.getAsJsonArray()) {
118                 try {
119                     userRoles.add(role.getAsString());
120                 } catch (ClassCastException e) {
121                     LOG.debug("Unable to cast role as String, skipping {}", role, e);
122                 }
123             }
124         }
125         return new MoonPrincipal(username, domain, username + "@" + domain, userRoles.build(), token.getAsString());
126     }
127
128     /**
129      * Injected from {@code shiro.ini}.
130      *
131      * @param moonServerURL specified in {@code shiro.ini}
132      * @throws NullPointerException If {@code moonServerURL} is {@code null}
133      * @throws IllegalArgumentException If the given string violates RFC&nbsp;2396 or it does not specify a host
134      *                                  and a port.
135      */
136     public void setMoonServerURL(final String moonServerURL) {
137         final URL url;
138         try {
139             url = new URL(moonServerURL);
140         } catch (MalformedURLException e) {
141             throw new IllegalArgumentException(e);
142         }
143
144         final var uriHost = url.getHost();
145         checkArgument(uriHost != null, "moon host not specified in %s", url);
146         final var uriPort = url.getPort();
147         checkArgument(uriPort >= 0, "moon port not specified in %s", url);
148
149         final var port = Integer.toString(uriPort);
150         // FIXME: allow HTTPS!
151         // FIXME: allow authentication: and that really means configuring a Client!
152         final var server = String.format("http://%s:%s/moon/auth/tokens", uriHost, port);
153         LOG.debug("Moon server is at: {}:{} and will be accessed through {}", uriHost, port, server);
154         moonServer = servletSupport.newClientBuilder().build().target(server);
155     }
156 }