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