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