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