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