From cf04a1a714ef4a1df973929dc750232b4d67d7b4 Mon Sep 17 00:00:00 2001 From: Kartik Hegde Date: Sat, 12 Nov 2022 14:29:11 +0530 Subject: Multitenancy in SDC Issue-ID: SDC-4215 Change-Id: Ie24ba38acc9f1998d4a7e722e8f98456dab9201d Signed-off-by: Kartik Hegde --- .../server/filters/MultitenancyKeycloakFilter.java | 286 +++++++++++++++++++++ 1 file changed, 286 insertions(+) create mode 100644 openecomp-be/api/openecomp-sdc-rest-webapp/onboarding-rest-war/src/main/java/org/openecomp/server/filters/MultitenancyKeycloakFilter.java (limited to 'openecomp-be/api/openecomp-sdc-rest-webapp/onboarding-rest-war/src/main/java/org/openecomp/server/filters') diff --git a/openecomp-be/api/openecomp-sdc-rest-webapp/onboarding-rest-war/src/main/java/org/openecomp/server/filters/MultitenancyKeycloakFilter.java b/openecomp-be/api/openecomp-sdc-rest-webapp/onboarding-rest-war/src/main/java/org/openecomp/server/filters/MultitenancyKeycloakFilter.java new file mode 100644 index 0000000000..8cb87e3e33 --- /dev/null +++ b/openecomp-be/api/openecomp-sdc-rest-webapp/onboarding-rest-war/src/main/java/org/openecomp/server/filters/MultitenancyKeycloakFilter.java @@ -0,0 +1,286 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2022 Tech-Mahindra Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.openecomp.server.filters; + +import org.keycloak.adapters.AdapterDeploymentContext; +import org.keycloak.adapters.AuthenticatedActionsHandler; +import org.keycloak.adapters.KeycloakConfigResolver; +import org.keycloak.adapters.KeycloakDeployment; +import org.keycloak.adapters.KeycloakDeploymentBuilder; +import org.keycloak.adapters.NodesRegistrationManagement; +import org.keycloak.adapters.PreAuthActionsHandler; +import org.keycloak.adapters.servlet.FilterRequestAuthenticator; +import org.keycloak.adapters.servlet.OIDCFilterSessionStore; +import org.keycloak.adapters.servlet.OIDCServletHttpFacade; +import org.keycloak.adapters.spi.AuthChallenge; +import org.keycloak.adapters.spi.AuthOutcome; +import org.keycloak.adapters.spi.InMemorySessionIdMapper; +import org.keycloak.adapters.spi.SessionIdMapper; +import org.keycloak.adapters.spi.UserSessionManagement; +import org.openecomp.sdc.common.util.Multitenancy; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import javax.servlet.http.HttpServletResponse; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Modifier; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Pattern; + + + +public class MultitenancyKeycloakFilter implements Filter { + + private static final Logger log = Logger.getLogger("" + MultitenancyKeycloakFilter.class); + + public static final String SKIP_PATTERN_PARAM = "keycloak.config.skipPattern"; + + public static final String ID_MAPPER_PARAM = "keycloak.config.idMapper"; + + public static final String CONFIG_RESOLVER_PARAM = "keycloak.config.resolver"; + + public static final String CONFIG_FILE_PARAM = "keycloak.config.file"; + + public static final String CONFIG_PATH_PARAM = "keycloak.config.path"; + + protected AdapterDeploymentContext deploymentContext; + + protected SessionIdMapper idMapper = new InMemorySessionIdMapper(); + + protected NodesRegistrationManagement nodesRegistrationManagement; + + protected Pattern skipPattern; + + private final KeycloakConfigResolver definedconfigResolver; + + boolean keycloak; + + /** + * Constructor that can be used to define a {@code KeycloakConfigResolver} that will be used at initialization to + * provide the {@code KeycloakDeployment}. + * @param definedconfigResolver the resolver + */ + public MultitenancyKeycloakFilter(KeycloakConfigResolver definedconfigResolver) { + this.definedconfigResolver = definedconfigResolver; + } + + public MultitenancyKeycloakFilter() { + this(null); + } + + @Override + public void init(final FilterConfig filterConfig) throws ServletException { + String skipPatternDefinition = filterConfig.getInitParameter(SKIP_PATTERN_PARAM); + if (skipPatternDefinition != null) { + skipPattern = Pattern.compile(skipPatternDefinition, Pattern.DOTALL); + } + + String idMapperClassName = filterConfig.getInitParameter(ID_MAPPER_PARAM); + if (idMapperClassName != null) { + try { + final Class idMapperClass = getClass().getClassLoader().loadClass(idMapperClassName); + final Constructor idMapperConstructor = idMapperClass.getDeclaredConstructor(); + Object idMapperInstance = null; + // for KEYCLOAK-13745 test + if (idMapperConstructor.getModifiers() == Modifier.PRIVATE) { + idMapperInstance = idMapperClass.getMethod("getInstance").invoke(null); + } else { + idMapperInstance = idMapperConstructor.newInstance(); + } + if(idMapperInstance instanceof SessionIdMapper) { + this.idMapper = (SessionIdMapper) idMapperInstance; + } else { + log.log(Level.WARNING, "SessionIdMapper class {0} is not instance of org.keycloak.adapters.spi.SessionIdMapper", idMapperClassName); + } + } catch (ClassNotFoundException | NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) { + log.log(Level.WARNING, "SessionIdMapper class could not be instanced", e); + } + } + + if (definedconfigResolver != null) { + deploymentContext = new AdapterDeploymentContext(definedconfigResolver); + log.log(Level.INFO, "Using {0} to resolve Keycloak configuration on a per-request basis.", definedconfigResolver.getClass()); + } else { + String configResolverClass = filterConfig.getInitParameter(CONFIG_RESOLVER_PARAM); + if (configResolverClass != null) { + try { + KeycloakConfigResolver configResolver = (KeycloakConfigResolver) getClass().getClassLoader().loadClass(configResolverClass).getDeclaredConstructor().newInstance(); + deploymentContext = new AdapterDeploymentContext(configResolver); + log.log(Level.INFO, "Using {0} to resolve Keycloak configuration on a per-request basis.", configResolverClass); + } catch (Exception ex) { + log.log(Level.FINE, "The specified resolver {0} could NOT be loaded. Keycloak is unconfigured and will deny all requests. Reason: {1}", new Object[]{configResolverClass, ex.getMessage()}); + deploymentContext = new AdapterDeploymentContext(new KeycloakDeployment()); + } + } else { + String fp = filterConfig.getInitParameter(CONFIG_FILE_PARAM); + InputStream is = null; + if (fp != null) { + try { + is = new FileInputStream(fp); + } catch (FileNotFoundException e) { + log.log(Level.FINE, "config file is empty",e); + } + } else { + String path = "/WEB-INF/keycloak.json"; + String pathParam = filterConfig.getInitParameter(CONFIG_PATH_PARAM); + if (pathParam != null) path = pathParam; + is = filterConfig.getServletContext().getResourceAsStream(path); + } + KeycloakDeployment kd = createKeycloakDeploymentFrom(is); + deploymentContext = new AdapterDeploymentContext(kd); + log.fine("Keycloak is using a per-deployment configuration."); + } + } + filterConfig.getServletContext().setAttribute(AdapterDeploymentContext.class.getName(), deploymentContext); + nodesRegistrationManagement = new NodesRegistrationManagement(); + } + + private KeycloakDeployment createKeycloakDeploymentFrom(InputStream is) { + if (is == null) { + log.fine("No adapter configuration. Keycloak is unconfigured and will deny all requests."); + return new KeycloakDeployment(); + } + return KeycloakDeploymentBuilder.build(is); + } + + + @Override + public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { + log.fine("Keycloak OIDC Filter"); + Multitenancy keyaccess= new Multitenancy(); + keycloak= keyaccess.multiTenancyCheck(); + + HttpServletRequest request = (HttpServletRequest) req; + HttpServletResponse response = (HttpServletResponse) res; + + if (!keycloak) { + chain.doFilter(req, res); + return; + } + + if (shouldSkip(request)) { + chain.doFilter(req, res); + return; + } + + OIDCServletHttpFacade facade = new OIDCServletHttpFacade(request, response); + KeycloakDeployment deployment = deploymentContext.resolveDeployment(facade); + if (deployment == null || !deployment.isConfigured()) { + response.sendError(403); + log.fine("deployment not configured"); + return; + } + + PreAuthActionsHandler preActions = new PreAuthActionsHandler(new org.openecomp.server.filters.MultitenancyKeycloakFilter.IdMapperUserSessionManagement(), deploymentContext, facade); + + if (preActions.handleRequest()) { + return; + } + + + nodesRegistrationManagement.tryRegister(deployment); + OIDCFilterSessionStore tokenStore = new OIDCFilterSessionStore(request, facade, 100000, deployment, idMapper); + tokenStore.checkCurrentToken(); + + + FilterRequestAuthenticator authenticator = new FilterRequestAuthenticator(deployment, tokenStore, facade, request, 8443); + AuthOutcome outcome = authenticator.authenticate(); + if (outcome == AuthOutcome.AUTHENTICATED) { + log.fine("AUTHENTICATED"); + if (facade.isEnded()) { + return; + } + AuthenticatedActionsHandler actions = new AuthenticatedActionsHandler(deployment, facade); + if (actions.handledRequest()) { + return; + } else { + HttpServletRequestWrapper wrapper = tokenStore.buildWrapper(); + chain.doFilter(wrapper, res); + return; + } + } + AuthChallenge challenge = authenticator.getChallenge(); + if (challenge != null) { + log.fine("challenge"); + challenge.challenge(facade); + return; + } + response.sendError(403); + + } + + /** + * Decides whether this {@link Filter} should skip the given {@link HttpServletRequest} based on the configured {@link org.keycloak.adapters.servlet.KeycloakOIDCFilter#skipPattern}. + * Patterns are matched against the {@link HttpServletRequest#getRequestURI() requestURI} of a request without the context-path. + * A request for {@code /myapp/index.html} would be tested with {@code /index.html} against the skip pattern. + * Skipped requests will not be processed further by {@link org.keycloak.adapters.servlet.KeycloakOIDCFilter} and immediately delegated to the {@link FilterChain}. + * + * @param request the request to check + * @return {@code true} if the request should not be handled, + * {@code false} otherwise. + */ + private boolean shouldSkip(HttpServletRequest request) { + + if (skipPattern == null) { + return false; + } + + String requestPath = request.getRequestURI().substring(request.getContextPath().length()); + return skipPattern.matcher(requestPath).matches(); + } + + @Override + public void destroy() { + + } + + private class IdMapperUserSessionManagement implements UserSessionManagement { + @Override + public void logoutAll() { + if (idMapper != null) { + idMapper.clear(); + } + } + + @Override + public void logoutHttpSessions(List ids) { + log.fine("**************** logoutHttpSessions"); + for (String id : ids) { + log.finest(id); + idMapper.removeSession(id); + } + + } + } +} -- cgit 1.2.3-korg