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