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