migrate sdnr features to phosphorus
[ccsdk/features.git] / sdnr / wt / oauth-provider / provider-jar / src / main / java / org / onap / ccsdk / features / sdnr / wt / oauthprovider / http / AuthHttpServlet.java
1 /*
2  * ============LICENSE_START=======================================================
3  * ONAP : ccsdk features
4  * ================================================================================
5  * Copyright (C) 2020 highstreet technologies GmbH Intellectual Property.
6  * All rights reserved.
7  * ================================================================================
8  * Licensed under the Apache License, Version 2.0 (the "License");
9  * you may not use this file except in compliance with the License.
10  * You may obtain a copy of the License at
11  *
12  *     http://www.apache.org/licenses/LICENSE-2.0
13  *
14  * Unless required by applicable law or agreed to in writing, software
15  * distributed under the License is distributed on an "AS IS" BASIS,
16  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17  * See the License for the specific language governing permissions and
18  * limitations under the License.
19  * ============LICENSE_END=========================================================
20  *
21  */
22 package org.onap.ccsdk.features.sdnr.wt.oauthprovider.http;
23
24 import com.fasterxml.jackson.databind.ObjectMapper;
25 import java.io.IOException;
26 import java.util.ArrayList;
27 import java.util.Arrays;
28 import java.util.Collection;
29 import java.util.HashMap;
30 import java.util.List;
31 import java.util.Map;
32 import java.util.Optional;
33 import java.util.regex.Matcher;
34 import java.util.regex.Pattern;
35 import javax.servlet.ServletException;
36 import javax.servlet.ServletOutputStream;
37 import javax.servlet.http.HttpServlet;
38 import javax.servlet.http.HttpServletRequest;
39 import javax.servlet.http.HttpServletResponse;
40 import org.apache.shiro.SecurityUtils;
41 import org.apache.shiro.ShiroException;
42 import org.apache.shiro.codec.Base64;
43 import org.apache.shiro.session.Session;
44 import org.apache.shiro.subject.Subject;
45 import org.jolokia.osgi.security.Authenticator;
46 import org.onap.ccsdk.features.sdnr.wt.common.http.BaseHTTPClient;
47 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.data.Config;
48 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.data.InvalidConfigurationException;
49 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.data.NoDefinitionFoundException;
50 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.data.OAuthProviderConfig;
51 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.data.OAuthToken;
52 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.data.OdlPolicy;
53 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.data.UserTokenPayload;
54 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.providers.AuthService;
55 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.providers.AuthService.PublicOAuthProviderConfig;
56 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.providers.MdSalAuthorizationStore;
57 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.providers.OAuthProviderFactory;
58 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.providers.TokenCreator;
59 import org.opendaylight.aaa.api.IdMService;
60 import org.apache.shiro.authc.BearerToken;
61 import org.opendaylight.mdsal.binding.api.DataBroker;
62 import org.opendaylight.yang.gen.v1.urn.opendaylight.aaa.app.config.rev170619.ShiroConfiguration;
63 import org.opendaylight.yang.gen.v1.urn.opendaylight.aaa.app.config.rev170619.shiro.configuration.Main;
64 import org.opendaylight.yang.gen.v1.urn.opendaylight.aaa.app.config.rev170619.shiro.configuration.Urls;
65 import org.slf4j.Logger;
66 import org.slf4j.LoggerFactory;
67
68 public class AuthHttpServlet extends HttpServlet {
69
70     private static final Logger LOG = LoggerFactory.getLogger(AuthHttpServlet.class.getName());
71     private static final long serialVersionUID = 1L;
72     private static final String BASEURI = "/oauth";
73     private static final String LOGINURI = BASEURI + "/login";
74     private static final String LOGOUTURI = BASEURI + "/logout";
75     private static final String PROVIDERSURI = BASEURI + "/providers";
76     public static final String REDIRECTURI = BASEURI + "/redirect";
77     private static final String REDIRECTURI_FORMAT = REDIRECTURI + "/%s";
78     private static final String POLICIESURI = BASEURI + "/policies";
79     private static final String REDIRECTID_REGEX = "^\\" + BASEURI + "\\/redirect\\/([^\\/]+)$";
80     private static final String LOGIN_REDIRECT_REGEX = "^\\" + LOGINURI + "\\/([^\\/]+)$";
81     private static final Pattern REDIRECTID_PATTERN = Pattern.compile(REDIRECTID_REGEX);
82     private static final Pattern LOGIN_REDIRECT_PATTERN = Pattern.compile(LOGIN_REDIRECT_REGEX);
83
84     private static final String DEFAULT_DOMAIN = "sdn";
85     private static final String HEAEDER_AUTHORIZATION = "Authorization";
86
87     private static final String CLASSNAME_ODLBASICAUTH =
88             "org.opendaylight.aaa.shiro.filters.ODLHttpAuthenticationFilter";
89     private static final String CLASSNAME_ODLBEARERANDBASICAUTH =
90             "org.opendaylight.aaa.shiro.filters.ODLHttpAuthenticationFilter2";
91     private static final String CLASSNAME_ODLMDSALAUTH =
92             "org.opendaylight.aaa.shiro.realm.MDSALDynamicAuthorizationFilter";
93     public static final String LOGIN_REDIRECT_FORMAT = LOGINURI + "/%s";
94
95     private final ObjectMapper mapper;
96     /* state <=> AuthProviderService> */
97     private final Map<String, AuthService> providerStore;
98     private final TokenCreator tokenCreator;
99     private final Config config;
100     private static Authenticator odlAuthenticator;
101     private static IdMService odlIdentityService;
102     private static ShiroConfiguration shiroConfiguration;
103     private static MdSalAuthorizationStore mdsalAuthStore;
104
105     public AuthHttpServlet() throws IllegalArgumentException, IOException, InvalidConfigurationException {
106         this.config = Config.getInstance();
107         this.tokenCreator = TokenCreator.getInstance(this.config);
108         this.mapper = new ObjectMapper();
109         this.providerStore = new HashMap<>();
110         for (OAuthProviderConfig pc : config.getProviders()) {
111             this.providerStore.put(pc.getId(), OAuthProviderFactory.create(pc.getType(), pc,
112                     this.config.getRedirectUri(), TokenCreator.getInstance(this.config)));
113         }
114
115     }
116
117     public void setOdlAuthenticator(Authenticator odlAuthenticator2) {
118         odlAuthenticator = odlAuthenticator2;
119     }
120
121     public void setOdlIdentityService(IdMService odlIdentityService2) {
122         odlIdentityService = odlIdentityService2;
123     }
124
125     public void setShiroConfiguration(ShiroConfiguration shiroConfiguration2) {
126         shiroConfiguration = shiroConfiguration2;
127     }
128
129     public void setDataBroker(DataBroker dataBroker) {
130         mdsalAuthStore = new MdSalAuthorizationStore(dataBroker);
131     }
132
133     @Override
134     protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
135         LOG.debug("GET request for {}", req.getRequestURI());
136         getHost(req);
137         if (PROVIDERSURI.equals(req.getRequestURI())) {
138             this.sendResponse(resp, HttpServletResponse.SC_OK, getConfigs(this.providerStore.values()));
139         } else if (req.getRequestURI().startsWith(LOGINURI)) {
140             this.handleLoginRedirect(req, resp);
141         } else if (req.getRequestURI().equals(LOGOUTURI)) {
142             this.handleLogout(req, resp);
143         } else if (POLICIESURI.equals(req.getRequestURI())) {
144             this.sendResponse(resp, HttpServletResponse.SC_OK, this.getPoliciesForUser(req));
145         } else if (req.getRequestURI().startsWith(REDIRECTURI)) {
146             this.handleRedirect(req, resp);
147         } else {
148             resp.sendError(HttpServletResponse.SC_NOT_FOUND);
149         }
150
151     }
152
153     private void handleLogout(HttpServletRequest req, HttpServletResponse resp) throws IOException {
154         this.logout();
155         this.sendResponse(resp, HttpServletResponse.SC_OK, "");
156     }
157
158     private void handleLoginRedirect(HttpServletRequest req, HttpServletResponse resp) throws IOException {
159         final String uri = req.getRequestURI();
160         final Matcher matcher = LOGIN_REDIRECT_PATTERN.matcher(uri);
161         if (matcher.find()) {
162             final String id = matcher.group(1);
163             AuthService provider = this.providerStore.getOrDefault(id, null);
164             if (provider != null) {
165                 String redirectUrl = getHost(req) + String.format(REDIRECTURI_FORMAT, id);
166                 provider.sendLoginRedirectResponse(resp, redirectUrl);
167                 return;
168             }
169         }
170         this.sendResponse(resp, HttpServletResponse.SC_NOT_FOUND, "");
171     }
172
173     /**
174      * find out what urls can be accessed by user and which are forbidden
175      *
176      * urlEntries: "anon" -> any access allowed "authcXXX" -> no grouping rule -> any access for user allowed "authcXXX,
177      * roles[abc] -> user needs to have role abc "authcXXX, roles["abc,def"] -> user needs to have roles abc AND def
178      * "authcXXX, anyroles[abc] -> user needs to have role abc "authcXXX, anyroles["abc,def"] -> user needs to have
179      * roles abc OR def
180      *
181      *
182      * @param req
183      * @return
184      */
185     private List<OdlPolicy> getPoliciesForUser(HttpServletRequest req) {
186         List<Urls> urlRules = shiroConfiguration.getUrls();
187         UserTokenPayload data = this.getUserInfo(req);
188         List<OdlPolicy> policies = new ArrayList<>();
189         if (urlRules != null) {
190             LOG.debug("try to find rules for user {} with roles {}",
191                     data == null ? "null" : data.getPreferredUsername(), data == null ? "null" : data.getRoles());
192             final String regex = "^([^,]+)[,]?[\\ ]?([anyroles]+)?(\\[\"?([a-zA-Z,]+)\"?\\])?";
193             final Pattern pattern = Pattern.compile(regex);
194             Matcher matcher;
195             for (Urls urlRule : urlRules) {
196                 matcher = pattern.matcher(urlRule.getPairValue());
197                 if (matcher.find()) {
198                     try {
199                         final String authClass = getAuthClass(matcher.group(1));
200                         Optional<OdlPolicy> policy = Optional.empty();
201                         //anon access allowed
202                         if (authClass == null) {
203                             policy = Optional.of(OdlPolicy.allowAll(urlRule.getPairKey()));
204                         } else if (authClass.equals(CLASSNAME_ODLBASICAUTH)) {
205                             policy = isBasic(req) ? this.getTokenBasedPolicy(urlRule, matcher, data)
206                                     : Optional.of(OdlPolicy.denyAll(urlRule.getPairKey()));
207                         } else if (authClass.equals(CLASSNAME_ODLBEARERANDBASICAUTH)) {
208                             policy = this.getTokenBasedPolicy(urlRule, matcher, data);
209                         } else if (authClass.equals(CLASSNAME_ODLMDSALAUTH)) {
210                             policy = this.getMdSalBasedPolicy(urlRule, data);
211                         }
212                         if (policy.isPresent()) {
213                             policies.add(policy.get());
214                         } else {
215                             LOG.warn("unable to get policy for authClass {} for entry {}", authClass,
216                                     urlRule.getPairValue());
217                             policies.add(OdlPolicy.denyAll(urlRule.getPairKey()));
218                         }
219                     } catch (NoDefinitionFoundException e) {
220                         LOG.warn("unknown authClass: ", e);
221                     }
222
223                 } else {
224                     LOG.warn("unable to detect url role value: {}", urlRule.getPairValue());
225                 }
226             }
227         } else {
228             LOG.debug("no url rules found");
229         }
230         return policies;
231     }
232
233     /**
234      * extract policy rule for user from MD-SAL not yet supported
235      *
236      * @param urlRule
237      * @param data
238      * @return
239      */
240     private Optional<OdlPolicy> getMdSalBasedPolicy(Urls urlRule, UserTokenPayload data) {
241         if (mdsalAuthStore != null) {
242             return data != null ? mdsalAuthStore.getPolicy(urlRule.getPairKey(), data.getRoles())
243                     : Optional.of(OdlPolicy.denyAll(urlRule.getPairKey()));
244         }
245         return Optional.empty();
246     }
247
248     /**
249      * extract policy rule for user from url rules of config
250      *
251      * @param urlRule
252      * @param matcher
253      * @param data
254      * @return
255      */
256     private Optional<OdlPolicy> getTokenBasedPolicy(Urls urlRule, Matcher matcher, UserTokenPayload data) {
257         final String url = urlRule.getPairKey();
258         final String rule = urlRule.getPairValue();
259         if (!rule.contains(",")) {
260             LOG.debug("found rule without roles for '{}'", matcher.group(1));
261             //not important if anon or authcXXX
262             if (data != null || "anon".equals(matcher.group(1))) {
263                 return Optional.of(OdlPolicy.allowAll(url));
264             }
265         }
266         if (data != null) {
267             LOG.debug("found rule with roles '{}'", matcher.group(4));
268             if ("roles".equals(matcher.group(2))) {
269                 if (this.rolesMatch(data.getRoles(), Arrays.asList(matcher.group(4).split(",")), false)) {
270                     return Optional.of(OdlPolicy.allowAll(url));
271                 } else {
272                     return Optional.of(OdlPolicy.denyAll(url));
273                 }
274             } else if ("anyroles".equals(matcher.group(2))) {
275                 if (this.rolesMatch(data.getRoles(), Arrays.asList(matcher.group(4).split(",")), true)) {
276                     return Optional.of(OdlPolicy.allowAll(url));
277                 } else {
278                     return Optional.of(OdlPolicy.denyAll(url));
279                 }
280             } else {
281                 LOG.warn("unable to detect url role value: {}", urlRule.getPairValue());
282             }
283         } else {
284             return Optional.of(OdlPolicy.denyAll(url));
285         }
286         return Optional.empty();
287     }
288
289     private String getAuthClass(String key) throws NoDefinitionFoundException {
290         if ("anon".equals(key)) {
291             return null;
292         }
293         List<Main> list = shiroConfiguration.getMain();
294         Optional<Main> main =
295                 list == null ? Optional.empty() : list.stream().filter(e -> e.getPairKey().equals(key)).findFirst();
296         if (main.isPresent()) {
297             return main.get().getPairValue();
298         }
299         throw new NoDefinitionFoundException("unable to find def for " + key);
300     }
301
302     private UserTokenPayload getUserInfo(HttpServletRequest req) {
303         if (isBearer(req)) {
304             UserTokenPayload data = this.tokenCreator.decode(req);
305             if (data != null) {
306                 return data;
307             }
308         } else if (isBasic(req)) {
309             String username = getBasicAuthUsername(req);
310             if (username != null) {
311                 final String domain = getBasicAuthDomain(username);
312                 if (!username.contains("@")) {
313                     username = String.format("%s@%s", username, domain);
314                 }
315                 List<String> roles = odlIdentityService.listRoles(username, domain);
316                 return UserTokenPayload.create(username, roles);
317             }
318         }
319         return null;
320     }
321
322     private static String getBasicAuthDomain(String username) {
323         if (username.contains("@")) {
324             return username.split("@")[1];
325         }
326         return DEFAULT_DOMAIN;
327     }
328
329     private static String getBasicAuthUsername(HttpServletRequest req) {
330         final String header = req.getHeader(HEAEDER_AUTHORIZATION);
331         final String decoded = Base64.decodeToString(header.substring(6));
332         // attempt to decode username/password; otherwise decode as token
333         if (decoded.contains(":")) {
334             return decoded.split(":")[0];
335         }
336         LOG.warn("unable to detect username from basicauth header {}", header);
337         return null;
338     }
339
340     private static boolean isBasic(HttpServletRequest req) {
341         final String header = req.getHeader(HEAEDER_AUTHORIZATION);
342         return header == null ? false : header.startsWith("Basic");
343     }
344
345     private static boolean isBearer(HttpServletRequest req) {
346         final String header = req.getHeader(HEAEDER_AUTHORIZATION);
347         return header == null ? false : header.startsWith("Bearer");
348     }
349
350     private boolean rolesMatch(List<String> userRoles, List<String> policyRoles, boolean any) {
351         if (any) {
352             for (String policyRole : policyRoles) {
353                 if (userRoles.contains(policyRole)) {
354                     return true;
355                 }
356             }
357             return false;
358         } else {
359             for (String policyRole : policyRoles) {
360                 if (!userRoles.contains(policyRole)) {
361                     return false;
362                 }
363             }
364             return true;
365         }
366
367     }
368
369     public String getHost(HttpServletRequest req) {
370         String hostUrl = this.config.getPublicUrl();
371         if (hostUrl == null) {
372             final String tmp = req.getRequestURL().toString();
373             final String regex = "^(http[s]{0,1}:\\/\\/[^\\/]+)";
374             final Pattern pattern = Pattern.compile(regex, Pattern.MULTILINE);
375             final Matcher matcher = pattern.matcher(tmp);
376             if (matcher.find()) {
377                 hostUrl = matcher.group(1);
378             }
379         }
380         LOG.info("host={}", hostUrl);
381         return hostUrl;
382
383     }
384
385     private List<PublicOAuthProviderConfig> getConfigs(Collection<AuthService> values) {
386         List<PublicOAuthProviderConfig> configs = new ArrayList<>();
387         for (AuthService svc : values) {
388             configs.add(svc.getConfig());
389         }
390         return configs;
391     }
392
393     /**
394      * GET /oauth/redirect/{providerID}
395      *
396      * @param req
397      * @param resp
398      * @throws IOException
399      */
400     private void handleRedirect(HttpServletRequest req, HttpServletResponse resp) throws IOException {
401         final String uri = req.getRequestURI();
402         final Matcher matcher = REDIRECTID_PATTERN.matcher(uri);
403         if (matcher.find()) {
404             AuthService provider = this.providerStore.getOrDefault(matcher.group(1), null);
405             if (provider != null) {
406                 //provider.setLocalHostUrl(getHost(req));
407                 provider.handleRedirect(req, resp, getHost(req));
408                 return;
409             }
410         }
411         resp.sendError(HttpServletResponse.SC_FORBIDDEN);
412     }
413
414     @Override
415     protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
416
417         LOG.debug("POST request for {}", req.getRequestURI());
418         if (this.config.loginActive() &&  this.config.doSupportOdlUsers() && LOGINURI.equals(req.getRequestURI())) {
419             final String username = req.getParameter("username");
420             final String domain = req.getParameter("domain");
421             BearerToken token =
422                     this.doLogin(username, req.getParameter("password"), domain != null ? domain : DEFAULT_DOMAIN);
423             if (token != null) {
424                 sendResponse(resp, HttpServletResponse.SC_OK, new OAuthToken(token));
425                 LOG.debug("login for odluser {} succeeded", username);
426                 return;
427             } else {
428                 LOG.debug("login failed");
429             }
430
431         }
432         resp.sendError(HttpServletResponse.SC_NOT_FOUND);
433     }
434
435     private BearerToken doLogin(String username, String password, String domain) {
436         if (!username.contains("@")) {
437             username = String.format("%s@%s", username, domain);
438         }
439         HttpServletRequest req = new HeadersOnlyHttpServletRequest(
440                 Map.of("Authorization", BaseHTTPClient.getAuthorizationHeaderValue(username, password)));
441         if (odlAuthenticator.authenticate(req)) {
442             List<String> roles = odlIdentityService.listRoles(username, domain);
443             UserTokenPayload data = new UserTokenPayload();
444             data.setPreferredUsername(username);
445             data.setFamilyName("");
446             data.setGivenName(username);
447             data.setIat(this.tokenCreator.getDefaultIat());
448             data.setExp(this.tokenCreator.getDefaultExp());
449             data.setRoles(roles);
450             return this.tokenCreator.createNewJWT(data);
451
452         }
453         return null;
454     }
455
456
457
458     private void sendResponse(HttpServletResponse resp, int code, Object data) throws IOException {
459         byte[] output = data != null ? mapper.writeValueAsString(data).getBytes() : new byte[0];
460         // output
461         resp.setStatus(code);
462         resp.setContentLength(output.length);
463         resp.setContentType("application/json");
464         ServletOutputStream os = null;
465         os = resp.getOutputStream();
466         os.write(output);
467
468     }
469
470     private void logout() {
471         final Subject subject = SecurityUtils.getSubject();
472         try {
473             subject.logout();
474             Session session = subject.getSession(false);
475             if (session != null) {
476                 session.stop();
477             }
478         } catch (ShiroException e) {
479             LOG.debug("Couldn't log out {}", subject, e);
480         }
481     }
482 }