2  * ============LICENSE_START=======================================================
 
   3  * ONAP : ccsdk features
 
   4  * ================================================================================
 
   5  * Copyright (C) 2020 highstreet technologies GmbH Intellectual Property.
 
   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
 
  12  *     http://www.apache.org/licenses/LICENSE-2.0
 
  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=========================================================
 
  22 package org.onap.ccsdk.features.sdnr.wt.oauthprovider.http;
 
  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;
 
  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.authc.BearerToken;
 
  43 import org.apache.shiro.codec.Base64;
 
  44 import org.apache.shiro.session.Session;
 
  45 import org.apache.shiro.subject.Subject;
 
  46 import org.jolokia.osgi.security.Authenticator;
 
  47 import org.onap.ccsdk.features.sdnr.wt.common.http.BaseHTTPClient;
 
  48 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.data.Config;
 
  49 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.data.InvalidConfigurationException;
 
  50 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.data.NoDefinitionFoundException;
 
  51 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.data.OAuthProviderConfig;
 
  52 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.data.OAuthToken;
 
  53 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.data.OdlPolicy;
 
  54 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.data.UserTokenPayload;
 
  55 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.providers.AuthService;
 
  56 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.providers.AuthService.PublicOAuthProviderConfig;
 
  57 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.providers.MdSalAuthorizationStore;
 
  58 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.providers.OAuthProviderFactory;
 
  59 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.providers.TokenCreator;
 
  60 import org.opendaylight.aaa.api.IdMService;
 
  61 import org.opendaylight.mdsal.binding.api.DataBroker;
 
  62 import org.opendaylight.yang.gen.v1.urn.opendaylight.aaa.app.config.rev170619.ShiroConfiguration;
 
  63 import org.opendaylight.yang.gen.v1.urn.opendaylight.aaa.app.config.rev170619.shiro.configuration.Main;
 
  64 import org.opendaylight.yang.gen.v1.urn.opendaylight.aaa.app.config.rev170619.shiro.configuration.Urls;
 
  65 import org.slf4j.Logger;
 
  66 import org.slf4j.LoggerFactory;
 
  68 public class AuthHttpServlet extends HttpServlet {
 
  70     private static final Logger LOG = LoggerFactory.getLogger(AuthHttpServlet.class.getName());
 
  71     private static final long serialVersionUID = 1L;
 
  72     private static final String BASEURI = "/oauth";
 
  73     private static final String LOGINURI = BASEURI + "/login";
 
  74     private static final String LOGOUTURI = BASEURI + "/logout";
 
  75     private static final String PROVIDERSURI = BASEURI + "/providers";
 
  76     public static final String REDIRECTURI = BASEURI + "/redirect";
 
  77     private static final String REDIRECTURI_FORMAT = REDIRECTURI + "/%s";
 
  78     private static final String POLICIESURI = BASEURI + "/policies";
 
  79     private static final String REDIRECTID_REGEX = "^\\" + BASEURI + "\\/redirect\\/([^\\/]+)$";
 
  80     private static final String LOGIN_REDIRECT_REGEX = "^\\" + LOGINURI + "\\/([^\\/]+)$";
 
  81     private static final Pattern REDIRECTID_PATTERN = Pattern.compile(REDIRECTID_REGEX);
 
  82     private static final Pattern LOGIN_REDIRECT_PATTERN = Pattern.compile(LOGIN_REDIRECT_REGEX);
 
  84     private static final String DEFAULT_DOMAIN = "sdn";
 
  85     private static final String HEAEDER_AUTHORIZATION = "Authorization";
 
  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";
 
  95     private final ObjectMapper mapper;
 
  96     /* state <=> AuthProviderService> */
 
  97     private final Map<String, AuthService> providerStore;
 
  98     private final TokenCreator tokenCreator;
 
  99     private final Config config;
 
 100     private static Authenticator odlAuthenticator;
 
 101     private static IdMService odlIdentityService;
 
 102     private static ShiroConfiguration shiroConfiguration;
 
 103     private static MdSalAuthorizationStore mdsalAuthStore;
 
 105     public AuthHttpServlet() throws IllegalArgumentException, IOException, InvalidConfigurationException {
 
 106         this.config = Config.getInstance();
 
 107         this.tokenCreator = TokenCreator.getInstance(this.config);
 
 108         this.mapper = new ObjectMapper();
 
 109         this.providerStore = new HashMap<>();
 
 110         for (OAuthProviderConfig pc : config.getProviders()) {
 
 111             this.providerStore.put(pc.getId(), OAuthProviderFactory.create(pc.getType(), pc,
 
 112                     this.config.getRedirectUri(), TokenCreator.getInstance(this.config)));
 
 117     public void setOdlAuthenticator(Authenticator odlAuthenticator2) {
 
 118         odlAuthenticator = odlAuthenticator2;
 
 121     public void setOdlIdentityService(IdMService odlIdentityService2) {
 
 122         odlIdentityService = odlIdentityService2;
 
 125     public void setShiroConfiguration(ShiroConfiguration shiroConfiguration2) {
 
 126         shiroConfiguration = shiroConfiguration2;
 
 129     public void setDataBroker(DataBroker dataBroker) {
 
 130         mdsalAuthStore = new MdSalAuthorizationStore(dataBroker);
 
 134     protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
 
 135         LOG.debug("GET request for {}", req.getRequestURI());
 
 137         if (PROVIDERSURI.equals(req.getRequestURI())) {
 
 138             this.sendResponse(resp, HttpServletResponse.SC_OK, getConfigs(this.providerStore.values()));
 
 139         } else if (req.getRequestURI().startsWith(LOGINURI)) {
 
 140             this.handleLoginRedirect(req, resp);
 
 141         } else if (req.getRequestURI().equals(LOGOUTURI)) {
 
 142             this.handleLogout(req, resp);
 
 143         } else if (POLICIESURI.equals(req.getRequestURI())) {
 
 144             this.sendResponse(resp, HttpServletResponse.SC_OK, this.getPoliciesForUser(req));
 
 145         } else if (req.getRequestURI().startsWith(REDIRECTURI)) {
 
 146             this.handleRedirect(req, resp);
 
 148             resp.sendError(HttpServletResponse.SC_NOT_FOUND);
 
 153     private void handleLogout(HttpServletRequest req, HttpServletResponse resp) throws IOException {
 
 155         this.sendResponse(resp, HttpServletResponse.SC_OK, "");
 
 158     private void handleLoginRedirect(HttpServletRequest req, HttpServletResponse resp) throws IOException {
 
 159         final String uri = req.getRequestURI();
 
 160         final Matcher matcher = LOGIN_REDIRECT_PATTERN.matcher(uri);
 
 161         if (matcher.find()) {
 
 162             final String id = matcher.group(1);
 
 163             AuthService provider = this.providerStore.getOrDefault(id, null);
 
 164             if (provider != null) {
 
 165                 String redirectUrl = getHost(req) + String.format(REDIRECTURI_FORMAT, id);
 
 166                 provider.sendLoginRedirectResponse(resp, redirectUrl);
 
 170         this.sendResponse(resp, HttpServletResponse.SC_NOT_FOUND, "");
 
 174      * find out what urls can be accessed by user and which are forbidden
 
 176      * urlEntries: "anon" -> any access allowed "authcXXX" -> no grouping rule -> any access for user allowed "authcXXX,
 
 177      * roles[abc] -> user needs to have role abc "authcXXX, roles["abc,def"] -> user needs to have roles abc AND def
 
 178      * "authcXXX, anyroles[abc] -> user needs to have role abc "authcXXX, anyroles["abc,def"] -> user needs to have
 
 185     private List<OdlPolicy> getPoliciesForUser(HttpServletRequest req) {
 
 186         List<Urls> urlRules = shiroConfiguration.getUrls();
 
 187         UserTokenPayload data = this.getUserInfo(req);
 
 188         List<OdlPolicy> policies = new ArrayList<>();
 
 189         if (urlRules != null) {
 
 190             LOG.debug("try to find rules for user {} with roles {}",
 
 191                     data == null ? "null" : data.getPreferredUsername(), data == null ? "null" : data.getRoles());
 
 192             final String regex = "^([^,]+)[,]?[\\ ]?([anyroles]+)?(\\[\"?([a-zA-Z,]+)\"?\\])?";
 
 193             final Pattern pattern = Pattern.compile(regex);
 
 195             for (Urls urlRule : urlRules) {
 
 196                 matcher = pattern.matcher(urlRule.getPairValue());
 
 197                 if (matcher.find()) {
 
 199                         final String authClass = getAuthClass(matcher.group(1));
 
 200                         Optional<OdlPolicy> policy = Optional.empty();
 
 201                         //anon access allowed
 
 202                         if (authClass == null) {
 
 203                             policy = Optional.of(OdlPolicy.allowAll(urlRule.getPairKey()));
 
 204                         } else if (authClass.equals(CLASSNAME_ODLBASICAUTH)) {
 
 205                             policy = isBasic(req) ? this.getTokenBasedPolicy(urlRule, matcher, data)
 
 206                                     : Optional.of(OdlPolicy.denyAll(urlRule.getPairKey()));
 
 207                         } else if (authClass.equals(CLASSNAME_ODLBEARERANDBASICAUTH)) {
 
 208                             policy = this.getTokenBasedPolicy(urlRule, matcher, data);
 
 209                         } else if (authClass.equals(CLASSNAME_ODLMDSALAUTH)) {
 
 210                             policy = this.getMdSalBasedPolicy(urlRule, data);
 
 212                         if (policy.isPresent()) {
 
 213                             policies.add(policy.get());
 
 215                             LOG.warn("unable to get policy for authClass {} for entry {}", authClass,
 
 216                                     urlRule.getPairValue());
 
 217                             policies.add(OdlPolicy.denyAll(urlRule.getPairKey()));
 
 219                     } catch (NoDefinitionFoundException e) {
 
 220                         LOG.warn("unknown authClass: ", e);
 
 224                     LOG.warn("unable to detect url role value: {}", urlRule.getPairValue());
 
 228             LOG.debug("no url rules found");
 
 234      * extract policy rule for user from MD-SAL not yet supported
 
 240     private Optional<OdlPolicy> getMdSalBasedPolicy(Urls urlRule, UserTokenPayload data) {
 
 241         if (mdsalAuthStore != null) {
 
 242             return data != null ? mdsalAuthStore.getPolicy(urlRule.getPairKey(), data.getRoles())
 
 243                     : Optional.of(OdlPolicy.denyAll(urlRule.getPairKey()));
 
 245         return Optional.empty();
 
 249      * extract policy rule for user from url rules of config
 
 256     private Optional<OdlPolicy> getTokenBasedPolicy(Urls urlRule, Matcher matcher, UserTokenPayload data) {
 
 257         final String url = urlRule.getPairKey();
 
 258         final String rule = urlRule.getPairValue();
 
 259         if (!rule.contains(",")) {
 
 260             LOG.debug("found rule without roles for '{}'", matcher.group(1));
 
 261             //not important if anon or authcXXX
 
 262             if (data != null || "anon".equals(matcher.group(1))) {
 
 263                 return Optional.of(OdlPolicy.allowAll(url));
 
 267             LOG.debug("found rule with roles '{}'", matcher.group(4));
 
 268             if ("roles".equals(matcher.group(2))) {
 
 269                 if (this.rolesMatch(data.getRoles(), Arrays.asList(matcher.group(4).split(",")), false)) {
 
 270                     return Optional.of(OdlPolicy.allowAll(url));
 
 272                     return Optional.of(OdlPolicy.denyAll(url));
 
 274             } else if ("anyroles".equals(matcher.group(2))) {
 
 275                 if (this.rolesMatch(data.getRoles(), Arrays.asList(matcher.group(4).split(",")), true)) {
 
 276                     return Optional.of(OdlPolicy.allowAll(url));
 
 278                     return Optional.of(OdlPolicy.denyAll(url));
 
 281                 LOG.warn("unable to detect url role value: {}", urlRule.getPairValue());
 
 284             return Optional.of(OdlPolicy.denyAll(url));
 
 286         return Optional.empty();
 
 289     private String getAuthClass(String key) throws NoDefinitionFoundException {
 
 290         if ("anon".equals(key)) {
 
 293         List<Main> list = shiroConfiguration.getMain();
 
 294         Optional<Main> main =
 
 295                 list == null ? Optional.empty() : list.stream().filter(e -> e.getPairKey().equals(key)).findFirst();
 
 296         if (main.isPresent()) {
 
 297             return main.get().getPairValue();
 
 299         throw new NoDefinitionFoundException("unable to find def for " + key);
 
 302     private UserTokenPayload getUserInfo(HttpServletRequest req) {
 
 304             UserTokenPayload data = this.tokenCreator.decode(req);
 
 308         } else if (isBasic(req)) {
 
 309             String username = getBasicAuthUsername(req);
 
 310             if (username != null) {
 
 311                 final String domain = getBasicAuthDomain(username);
 
 312                 if (!username.contains("@")) {
 
 313                     username = String.format("%s@%s", username, domain);
 
 315                 List<String> roles = odlIdentityService.listRoles(username, domain);
 
 316                 return UserTokenPayload.create(username, roles);
 
 322     private static String getBasicAuthDomain(String username) {
 
 323         if (username.contains("@")) {
 
 324             return username.split("@")[1];
 
 326         return DEFAULT_DOMAIN;
 
 329     private static String getBasicAuthUsername(HttpServletRequest req) {
 
 330         final String header = req.getHeader(HEAEDER_AUTHORIZATION);
 
 331         final String decoded = Base64.decodeToString(header.substring(6));
 
 332         // attempt to decode username/password; otherwise decode as token
 
 333         if (decoded.contains(":")) {
 
 334             return decoded.split(":")[0];
 
 336         LOG.warn("unable to detect username from basicauth header {}", header);
 
 340     private static boolean isBasic(HttpServletRequest req) {
 
 341         final String header = req.getHeader(HEAEDER_AUTHORIZATION);
 
 342         return header == null ? false : header.startsWith("Basic");
 
 345     private static boolean isBearer(HttpServletRequest req) {
 
 346         final String header = req.getHeader(HEAEDER_AUTHORIZATION);
 
 347         return header == null ? false : header.startsWith("Bearer");
 
 350     private boolean rolesMatch(List<String> userRoles, List<String> policyRoles, boolean any) {
 
 352             for (String policyRole : policyRoles) {
 
 353                 if (userRoles.contains(policyRole)) {
 
 359             for (String policyRole : policyRoles) {
 
 360                 if (!userRoles.contains(policyRole)) {
 
 369     public String getHost(HttpServletRequest req) {
 
 370         String hostUrl = this.config.getPublicUrl();
 
 371         if (hostUrl == null) {
 
 372             final String tmp = req.getRequestURL().toString();
 
 373             final String regex = "^(http[s]{0,1}:\\/\\/[^\\/]+)";
 
 374             final Pattern pattern = Pattern.compile(regex, Pattern.MULTILINE);
 
 375             final Matcher matcher = pattern.matcher(tmp);
 
 376             if (matcher.find()) {
 
 377                 hostUrl = matcher.group(1);
 
 380         LOG.info("host={}", hostUrl);
 
 385     private List<PublicOAuthProviderConfig> getConfigs(Collection<AuthService> values) {
 
 386         List<PublicOAuthProviderConfig> configs = new ArrayList<>();
 
 387         for (AuthService svc : values) {
 
 388             configs.add(svc.getConfig());
 
 394      * GET /oauth/redirect/{providerID}
 
 398      * @throws IOException
 
 400     private void handleRedirect(HttpServletRequest req, HttpServletResponse resp) throws IOException {
 
 401         final String uri = req.getRequestURI();
 
 402         final Matcher matcher = REDIRECTID_PATTERN.matcher(uri);
 
 403         if (matcher.find()) {
 
 404             AuthService provider = this.providerStore.getOrDefault(matcher.group(1), null);
 
 405             if (provider != null) {
 
 406                 //provider.setLocalHostUrl(getHost(req));
 
 407                 provider.handleRedirect(req, resp, getHost(req));
 
 411         resp.sendError(HttpServletResponse.SC_FORBIDDEN);
 
 415     protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
 
 417         LOG.debug("POST request for {}", req.getRequestURI());
 
 418         if (this.config.loginActive() &&  this.config.doSupportOdlUsers() && LOGINURI.equals(req.getRequestURI())) {
 
 419             final String username = req.getParameter("username");
 
 420             final String domain = req.getParameter("domain");
 
 422                     this.doLogin(username, req.getParameter("password"), domain != null ? domain : DEFAULT_DOMAIN);
 
 424                 sendResponse(resp, HttpServletResponse.SC_OK, new OAuthToken(token));
 
 425                 LOG.debug("login for odluser {} succeeded", username);
 
 428                 LOG.debug("login failed");
 
 432         resp.sendError(HttpServletResponse.SC_NOT_FOUND);
 
 435     private BearerToken doLogin(String username, String password, String domain) {
 
 436         if (!username.contains("@")) {
 
 437             username = String.format("%s@%s", username, domain);
 
 439         HttpServletRequest req = new HeadersOnlyHttpServletRequest(
 
 440                 Map.of("Authorization", BaseHTTPClient.getAuthorizationHeaderValue(username, password)));
 
 441         if (odlAuthenticator.authenticate(req)) {
 
 442             List<String> roles = odlIdentityService.listRoles(username, domain);
 
 443             UserTokenPayload data = new UserTokenPayload();
 
 444             data.setPreferredUsername(username);
 
 445             data.setFamilyName("");
 
 446             data.setGivenName(username);
 
 447             data.setIat(this.tokenCreator.getDefaultIat());
 
 448             data.setExp(this.tokenCreator.getDefaultExp());
 
 449             data.setRoles(roles);
 
 450             return this.tokenCreator.createNewJWT(data);
 
 458     private void sendResponse(HttpServletResponse resp, int code, Object data) throws IOException {
 
 459         byte[] output = data != null ? mapper.writeValueAsString(data).getBytes() : new byte[0];
 
 461         resp.setStatus(code);
 
 462         resp.setContentLength(output.length);
 
 463         resp.setContentType("application/json");
 
 464         ServletOutputStream os = null;
 
 465         os = resp.getOutputStream();
 
 470     private void logout() {
 
 471         final Subject subject = SecurityUtils.getSubject();
 
 474             Session session = subject.getSession(false);
 
 475             if (session != null) {
 
 478         } catch (ShiroException e) {
 
 479             LOG.debug("Couldn't log out {}", subject, e);