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