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 --- .../files/default/error-configuration.yaml | 8 + .../sdc/be/catalog/impl/ComponentMessage.java | 3 + .../sdc/be/components/impl/ImportUtils.java | 1 + .../be/components/impl/ResourceBusinessLogic.java | 2 + .../be/components/impl/ResourceImportManager.java | 1 + .../be/components/impl/ServiceImportManager.java | 1 + .../components/impl/ServiceImportParseLogic.java | 3 + .../sdc/be/filters/MultitenancyFilter.java | 270 +++++++++++++++++++++ .../openecomp/sdc/be/servlets/ElementServlet.java | 24 +- .../sdc/be/servlets/ResourcesServlet.java | 47 +++- .../openecomp/sdc/be/servlets/ServiceServlet.java | 41 +++- .../openecomp/sdc/be/tosca/ToscaExportHandler.java | 1 + .../main/resources/config/error-configuration.yaml | 8 + catalog-be/src/main/webapp/WEB-INF/keycloak.json | 11 + catalog-be/src/main/webapp/WEB-INF/web.xml | 12 + .../components/impl/ElementBusinessLogicTest.java | 84 +++++++ .../components/impl/ResourceBusinessLogicTest.java | 57 +++++ .../components/impl/ServiceBusinessLogicTest.java | 43 ++++ 18 files changed, 593 insertions(+), 24 deletions(-) create mode 100644 catalog-be/src/main/java/org/openecomp/sdc/be/filters/MultitenancyFilter.java create mode 100644 catalog-be/src/main/webapp/WEB-INF/keycloak.json (limited to 'catalog-be/src') diff --git a/catalog-be/src/main/docker/backend/chef-repo/cookbooks/sdc-catalog-be/files/default/error-configuration.yaml b/catalog-be/src/main/docker/backend/chef-repo/cookbooks/sdc-catalog-be/files/default/error-configuration.yaml index 32bbf73e9d..a9df460cf2 100644 --- a/catalog-be/src/main/docker/backend/chef-repo/cookbooks/sdc-catalog-be/files/default/error-configuration.yaml +++ b/catalog-be/src/main/docker/backend/chef-repo/cookbooks/sdc-catalog-be/files/default/error-configuration.yaml @@ -2822,6 +2822,14 @@ errors: message: "Capability '%1' not found in '%2' '%3'." messageId: "SVC4186" + #---------SVC4950----------------------------- + MISSING_TENANT_NAME: { + code: 400, + message: "Error: Missing Tenant name.", + messageId: "SVC4950" + } + + #---------SVC4001------------------------------ NOT_PERMITTED_SPECIAL_CHARS: { code: 406, diff --git a/catalog-be/src/main/java/org/openecomp/sdc/be/catalog/impl/ComponentMessage.java b/catalog-be/src/main/java/org/openecomp/sdc/be/catalog/impl/ComponentMessage.java index 69a04e1df5..34e2add887 100644 --- a/catalog-be/src/main/java/org/openecomp/sdc/be/catalog/impl/ComponentMessage.java +++ b/catalog-be/src/main/java/org/openecomp/sdc/be/catalog/impl/ComponentMessage.java @@ -85,6 +85,9 @@ public class ComponentMessage extends CatalogComponent implements IComponentMess setIsHighestVersion(component.isHighestVersion()); // isHighestVersion setDescription(component.getDescription()); // description + + setTenant(component.getTenant()); // tenant + if (component.getTags() != null) { setTags(component.getTags()); // tags } diff --git a/catalog-be/src/main/java/org/openecomp/sdc/be/components/impl/ImportUtils.java b/catalog-be/src/main/java/org/openecomp/sdc/be/components/impl/ImportUtils.java index 9c2c070c74..51444a2cc1 100644 --- a/catalog-be/src/main/java/org/openecomp/sdc/be/components/impl/ImportUtils.java +++ b/catalog-be/src/main/java/org/openecomp/sdc/be/components/impl/ImportUtils.java @@ -797,6 +797,7 @@ public final class ImportUtils { public static final String ESCAPED_DOUBLE_QUOTE = "\""; public static final String QUOTE = "'"; public static final String VF_DESCRIPTION = "Nested VF in service"; + public static final String TENANT = "tenant"; private Constants() { } diff --git a/catalog-be/src/main/java/org/openecomp/sdc/be/components/impl/ResourceBusinessLogic.java b/catalog-be/src/main/java/org/openecomp/sdc/be/components/impl/ResourceBusinessLogic.java index cb57ab3b12..619a923b48 100644 --- a/catalog-be/src/main/java/org/openecomp/sdc/be/components/impl/ResourceBusinessLogic.java +++ b/catalog-be/src/main/java/org/openecomp/sdc/be/components/impl/ResourceBusinessLogic.java @@ -1411,6 +1411,7 @@ public class ResourceBusinessLogic extends ComponentBusinessLogic { resourceMetaData.setIcon(ImportUtils.Constants.DEFAULT_ICON); resourceMetaData.setContactId(user.getUserId()); resourceMetaData.setVendorName(resourceVf.getVendorName()); + resourceMetaData.setTenant(resourceVf.getTenant()); resourceMetaData.setVendorRelease(resourceVf.getVendorRelease()); resourceMetaData.setModel(resourceVf.getModel()); // Setting tag @@ -1444,6 +1445,7 @@ public class ResourceBusinessLogic extends ComponentBusinessLogic { cvfc.setContactId(csarInfo.getModifier().getUserId()); cvfc.setCreatorUserId(csarInfo.getModifier().getUserId()); cvfc.setVendorName(resourceVf.getVendorName()); + cvfc.setTenant(resourceVf.getTenant()); cvfc.setVendorRelease(resourceVf.getVendorRelease()); cvfc.setModel(resourceVf.getModel()); cvfc.setResourceVendorModelNumber(resourceVf.getResourceVendorModelNumber()); diff --git a/catalog-be/src/main/java/org/openecomp/sdc/be/components/impl/ResourceImportManager.java b/catalog-be/src/main/java/org/openecomp/sdc/be/components/impl/ResourceImportManager.java index 79b3c961f7..076dd50655 100644 --- a/catalog-be/src/main/java/org/openecomp/sdc/be/components/impl/ResourceImportManager.java +++ b/catalog-be/src/main/java/org/openecomp/sdc/be/components/impl/ResourceImportManager.java @@ -330,6 +330,7 @@ public class ResourceImportManager { resource.setIcon(resourceMetaData.getResourceIconPath()); resource.setResourceVendorModelNumber(resourceMetaData.getResourceVendorModelNumber()); resource.setResourceType(ResourceTypeEnum.valueOf(resourceMetaData.getResourceType())); + resource.setTenant(resourceMetaData.getTenant()); if (resourceMetaData.getVendorName() != null) { resource.setVendorName(resourceMetaData.getVendorName()); } diff --git a/catalog-be/src/main/java/org/openecomp/sdc/be/components/impl/ServiceImportManager.java b/catalog-be/src/main/java/org/openecomp/sdc/be/components/impl/ServiceImportManager.java index 6f10aaea9c..595603ec07 100644 --- a/catalog-be/src/main/java/org/openecomp/sdc/be/components/impl/ServiceImportManager.java +++ b/catalog-be/src/main/java/org/openecomp/sdc/be/components/impl/ServiceImportManager.java @@ -62,6 +62,7 @@ public class ServiceImportManager { public void populateServiceMetadata(UploadServiceInfo serviceMetaData, Service service) { if (service != null && serviceMetaData != null) { service.setDescription(serviceMetaData.getDescription()); + service.setTenant(serviceMetaData.getTenant()); service.setTags(serviceMetaData.getTags()); service.setCategories(serviceMetaData.getCategories()); service.setContactId(serviceMetaData.getContactId()); diff --git a/catalog-be/src/main/java/org/openecomp/sdc/be/components/impl/ServiceImportParseLogic.java b/catalog-be/src/main/java/org/openecomp/sdc/be/components/impl/ServiceImportParseLogic.java index 43b88fd63e..41babdfba5 100644 --- a/catalog-be/src/main/java/org/openecomp/sdc/be/components/impl/ServiceImportParseLogic.java +++ b/catalog-be/src/main/java/org/openecomp/sdc/be/components/impl/ServiceImportParseLogic.java @@ -917,6 +917,7 @@ public class ServiceImportParseLogic { cvfc.setContactId(csarInfo.getModifier().getUserId()); cvfc.setCreatorUserId(csarInfo.getModifier().getUserId()); cvfc.setVendorName(resourceVf.getVendorName()); + cvfc.setTenant(resourceVf.getTenant()); cvfc.setVendorRelease(resourceVf.getVendorRelease()); cvfc.setResourceVendorModelNumber(resourceVf.getResourceVendorModelNumber()); cvfc.setToscaResourceName(buildNestedToscaResourceName(ResourceTypeEnum.VF.name(), csarInfo.getVfResourceName(), nodeName).getLeft()); @@ -982,6 +983,7 @@ public class ServiceImportParseLogic { resourceMetaData.setIcon(ImportUtils.Constants.DEFAULT_ICON); resourceMetaData.setContactId(user.getUserId()); resourceMetaData.setVendorName(resourceVf.getVendorName()); + resourceMetaData.setTenant(resourceVf.getTenant()); resourceMetaData.setVendorRelease(resourceVf.getVendorRelease()); // Setting tag List tags = new ArrayList<>(); @@ -1306,6 +1308,7 @@ public class ServiceImportParseLogic { cvfc.setContactId(csarInfo.getModifier().getUserId()); cvfc.setCreatorUserId(csarInfo.getModifier().getUserId()); cvfc.setVendorName("cmri"); + cvfc.setTenant("tenant"); cvfc.setVendorRelease("1.0"); cvfc.setResourceVendorModelNumber(""); cvfc.setToscaResourceName(buildNestedToscaResourceName(ResourceTypeEnum.VF.name(), csarInfo.getVfResourceName(), nodeName).getLeft()); diff --git a/catalog-be/src/main/java/org/openecomp/sdc/be/filters/MultitenancyFilter.java b/catalog-be/src/main/java/org/openecomp/sdc/be/filters/MultitenancyFilter.java new file mode 100644 index 0000000000..45dbbad587 --- /dev/null +++ b/catalog-be/src/main/java/org/openecomp/sdc/be/filters/MultitenancyFilter.java @@ -0,0 +1,270 @@ +/*- + * ============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.sdc.be.filters; + +import org.keycloak.adapters.*; +import org.keycloak.adapters.servlet.FilterRequestAuthenticator; +import org.keycloak.adapters.servlet.OIDCFilterSessionStore; +import org.keycloak.adapters.servlet.OIDCServletHttpFacade; +import org.keycloak.adapters.spi.*; +import org.openecomp.sdc.common.util.Multitenancy; + +import javax.servlet.*; +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 MultitenancyFilter implements Filter { + + private static final Logger log = Logger.getLogger("" + MultitenancyFilter.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; + + String path = "/WEB-INF/keycloak.json"; + + /** + * 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 MultitenancyFilter(KeycloakConfigResolver definedConfigResolver) { + this.definedConfigResolver = definedConfigResolver; + } + + public MultitenancyFilter() { + 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 not configured 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) { + throw new RuntimeException(e); + } + } else { + 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 not configured 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.sdc.be.filters.MultitenancyFilter.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) { + idMapper.removeSession(id); + } + + } + } +} diff --git a/catalog-be/src/main/java/org/openecomp/sdc/be/servlets/ElementServlet.java b/catalog-be/src/main/java/org/openecomp/sdc/be/servlets/ElementServlet.java index 5e4085cf33..578319208c 100644 --- a/catalog-be/src/main/java/org/openecomp/sdc/be/servlets/ElementServlet.java +++ b/catalog-be/src/main/java/org/openecomp/sdc/be/servlets/ElementServlet.java @@ -33,6 +33,8 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; import javax.inject.Inject; import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; @@ -79,9 +81,10 @@ import org.openecomp.sdc.be.ui.model.UiCategories; import org.openecomp.sdc.be.user.UserBusinessLogic; import org.openecomp.sdc.common.api.Constants; import org.openecomp.sdc.common.log.wrappers.Logger; +import org.openecomp.sdc.common.util.Multitenancy; import org.openecomp.sdc.exception.ResponseFormat; import org.springframework.stereotype.Controller; - +import org.keycloak.representations.AccessToken; @Path("/v1/") /** * @@ -522,8 +525,23 @@ public class ElementServlet extends BeGenericServlet { log.debug("failed to get followed resources services "); return buildErrorResponse(followedResourcesServices.right().value()); } - Object data = RepresentationUtils.toRepresentation(followedResourcesServices.left().value()); - return buildOkResponse(getComponentsUtils().getResponseFormat(ActionStatus.OK), data); + Multitenancy keyaccess= new Multitenancy(); + if (keyaccess.multiTenancyCheck()) { + AccessToken.Access realmAccess = keyaccess.getAccessToken(request).getRealmAccess(); + Set realmroles = realmAccess.getRoles(); + Map> dataResponse = new HashMap<>(); + followedResourcesServices.left().value().entrySet().stream() + .forEach(component->{component.setValue(component.getValue().stream().filter(cm->realmroles.stream() + .anyMatch(role->cm.getTenant().equals(role))).collect(Collectors.toList())); + dataResponse.put(component.getKey(), component.getValue()); + }); + Object data = RepresentationUtils.toRepresentation(dataResponse); + return buildOkResponse(getComponentsUtils().getResponseFormat(ActionStatus.OK), data); + } + else{ + Object data = RepresentationUtils.toRepresentation(followedResourcesServices.left().value()); + return buildOkResponse(getComponentsUtils().getResponseFormat(ActionStatus.OK), data); + } } catch (Exception e) { BeEcompErrorManager.getInstance().logBeRestApiGeneralError("Get Followed Resources / Services Categories"); log.debug("Getting followed resources/services failed with exception", e); diff --git a/catalog-be/src/main/java/org/openecomp/sdc/be/servlets/ResourcesServlet.java b/catalog-be/src/main/java/org/openecomp/sdc/be/servlets/ResourcesServlet.java index d84e40c3d8..08f26fff4a 100644 --- a/catalog-be/src/main/java/org/openecomp/sdc/be/servlets/ResourcesServlet.java +++ b/catalog-be/src/main/java/org/openecomp/sdc/be/servlets/ResourcesServlet.java @@ -35,6 +35,7 @@ import java.io.File; import java.io.IOException; import java.util.List; import java.util.Map; +import java.util.Set; import javax.inject.Inject; import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; @@ -56,6 +57,7 @@ import org.glassfish.jersey.media.multipart.FormDataContentDisposition; import org.glassfish.jersey.media.multipart.FormDataParam; import org.json.JSONException; import org.json.JSONObject; +import org.keycloak.representations.AccessToken; import org.openecomp.sdc.be.components.impl.ComponentInstanceBusinessLogic; import org.openecomp.sdc.be.components.impl.CsarValidationUtils; import org.openecomp.sdc.be.components.impl.ImportUtils; @@ -87,6 +89,7 @@ import org.openecomp.sdc.common.log.wrappers.Logger; import org.openecomp.sdc.common.util.ValidationUtils; import org.openecomp.sdc.common.zip.exception.ZipException; import org.openecomp.sdc.exception.ResponseFormat; +import org.openecomp.sdc.common.util.Multitenancy; import org.springframework.stereotype.Controller; @Loggable(prepend = true, value = Loggable.DEBUG, trim = false) @@ -115,11 +118,12 @@ public class ResourcesServlet extends AbstractValidationsServlet { @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) @Operation(description = "Create Resource", method = "POST", summary = "Returns created resource", responses = { - @ApiResponse(content = @Content(array = @ArraySchema(schema = @Schema(implementation = Resource.class)))), - @ApiResponse(responseCode = "201", description = "Resource created"), - @ApiResponse(responseCode = "403", description = "Restricted operation"), - @ApiResponse(responseCode = "400", description = "Invalid content / Missing content"), - @ApiResponse(responseCode = "409", description = "Resource already exist")}) + @ApiResponse(content = @Content(array = @ArraySchema(schema = @Schema(implementation = Resource.class)))), + @ApiResponse(responseCode = "201", description = "Resource created"), + @ApiResponse(responseCode = "403", description = "Restricted operation"), + @ApiResponse(responseCode = "400", description = "Invalid content / Missing content"), + @ApiResponse(responseCode = "409", description = "Resource already exist"), + @ApiResponse(responseCode = "401", description = "Unauthorized Tenant")}) @PermissionAllowed(AafPermission.PermNames.INTERNAL_ALL_VALUE) public Response createResource(@Parameter(description = "Resource object to be created", required = true) String data, @Context final HttpServletRequest request, @HeaderParam(value = Constants.USER_ID_HEADER) String userId) @@ -148,14 +152,33 @@ public class ResourcesServlet extends AbstractValidationsServlet { response = buildErrorResponse(convertResponse.right().value()); return response; } + Multitenancy keyaccess = new Multitenancy(); Resource resource = convertResponse.left().value(); - Resource createdResource = resourceBusinessLogic.createResource(resource, AuditingActionEnum.CREATE_RESOURCE, modifier, null, null); - Object representation = RepresentationUtils.toRepresentation(createdResource); - response = buildOkResponse(getComponentsUtils().getResponseFormat(ActionStatus.CREATED), representation); - responseWrapper.setInnerElement(response); - loggerSupportability - .log(LoggerSupportabilityActions.CREATE_RESOURCE, resource.getComponentMetadataForSupportLog(), StatusCode.COMPLETE, - "Resource successfully created user {}", userId); + if (keyaccess.multiTenancyCheck()) + { + AccessToken.Access realmAccess = keyaccess.getAccessToken(request).getRealmAccess(); + Set realmroles = realmAccess.getRoles(); + boolean match = realmroles.contains(resource.getTenant()); + if (match) { + Resource createdResource = resourceBusinessLogic.createResource(resource, AuditingActionEnum.CREATE_RESOURCE, modifier, null, null); + Object representation = RepresentationUtils.toRepresentation(createdResource); + response = buildOkResponse(getComponentsUtils().getResponseFormat(ActionStatus.CREATED), representation); + responseWrapper.setInnerElement(response); + loggerSupportability + .log(LoggerSupportabilityActions.CREATE_RESOURCE, resource.getComponentMetadataForSupportLog(), StatusCode.COMPLETE, + "Resource successfully created user {}", userId); + } else { + return Response.status(401, "Unauthorized Tenant").build(); + } + } else { + Resource createdResource = resourceBusinessLogic.createResource(resource, AuditingActionEnum.CREATE_RESOURCE, modifier, null, null); + Object representation = RepresentationUtils.toRepresentation(createdResource); + response = buildOkResponse(getComponentsUtils().getResponseFormat(ActionStatus.CREATED), representation); + responseWrapper.setInnerElement(response); + loggerSupportability + .log(LoggerSupportabilityActions.CREATE_RESOURCE, resource.getComponentMetadataForSupportLog(), StatusCode.COMPLETE, + "Resource successfully created user {}", userId); + } } return responseWrapper.getInnerElement(); } catch (final IOException | ZipException e) { diff --git a/catalog-be/src/main/java/org/openecomp/sdc/be/servlets/ServiceServlet.java b/catalog-be/src/main/java/org/openecomp/sdc/be/servlets/ServiceServlet.java index fcac7dce35..3c2b72be2a 100644 --- a/catalog-be/src/main/java/org/openecomp/sdc/be/servlets/ServiceServlet.java +++ b/catalog-be/src/main/java/org/openecomp/sdc/be/servlets/ServiceServlet.java @@ -41,6 +41,7 @@ import java.lang.reflect.Type; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Set; import javax.inject.Inject; import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; @@ -92,10 +93,11 @@ import org.openecomp.sdc.common.log.elements.LoggerSupportability; import org.openecomp.sdc.common.log.enums.LoggerSupportabilityActions; import org.openecomp.sdc.common.log.enums.StatusCode; import org.openecomp.sdc.common.log.wrappers.Logger; +import org.openecomp.sdc.common.util.Multitenancy; import org.openecomp.sdc.common.zip.exception.ZipException; import org.openecomp.sdc.exception.ResponseFormat; import org.springframework.stereotype.Controller; - +import org.keycloak.representations.AccessToken; @Loggable(prepend = true, value = Loggable.DEBUG, trim = false) @Path("/v1/catalog") @Server(url = "/sdc2/rest") @@ -127,7 +129,8 @@ public class ServiceServlet extends AbstractValidationsServlet { @ApiResponse(content = @Content(array = @ArraySchema(schema = @Schema(implementation = Service.class)))), @ApiResponse(responseCode = "201", description = "Service created"), @ApiResponse(responseCode = "403", description = "Restricted operation"), @ApiResponse(responseCode = "400", description = "Invalid content / Missing content"), - @ApiResponse(responseCode = "409", description = "Service already exist")}) + @ApiResponse(responseCode = "409", description = "Service already exist"), + @ApiResponse(responseCode = "401", description = "Unauthorized Tenant")}) @PermissionAllowed(AafPermission.PermNames.INTERNAL_ALL_VALUE) public Response createService(@Parameter(description = "Service object to be created", required = true) String data, @Context final HttpServletRequest request, @HeaderParam(value = Constants.USER_ID_HEADER) String userId) { @@ -141,15 +144,35 @@ public class ServiceServlet extends AbstractValidationsServlet { if (convertResponse.isRight()) { throw new ByResponseFormatComponentException(convertResponse.right().value()); } + Multitenancy keyaccess = new Multitenancy(); Service service = convertResponse.left().value(); - Either actionResponse = serviceBusinessLogic.createService(service, modifier); - if (actionResponse.isRight()) { - log.debug("Failed to create service"); - throw new ByResponseFormatComponentException(actionResponse.right().value()); + if (keyaccess.multiTenancyCheck()) { + AccessToken.Access realmAccess = keyaccess.getAccessToken(request).getRealmAccess(); + Set realmroles = realmAccess.getRoles(); + boolean match = realmroles.contains(service.getTenant()); + if (match) { + Either actionResponse = serviceBusinessLogic.createService(service, modifier); + if (actionResponse.isRight()) { + log.debug("Failed to create service"); + throw new ByResponseFormatComponentException(actionResponse.right().value()); + } + loggerSupportability.log(LoggerSupportabilityActions.CREATE_SERVICE, service.getComponentMetadataForSupportLog(), StatusCode.COMPLETE, + "Service {} has been created by user {} ", service.getName(), userId); + return buildOkResponse(getComponentsUtils().getResponseFormat(ActionStatus.CREATED), actionResponse.left().value()); + } else { + log.debug("Unauthorized Tenant"); + return Response.status(401, "Unauthorized Tenant").build(); + } + } else { + Either actionResponse = serviceBusinessLogic.createService(service, modifier); + if (actionResponse.isRight()) { + log.debug("Failed to create service"); + throw new ByResponseFormatComponentException(actionResponse.right().value()); + } + loggerSupportability.log(LoggerSupportabilityActions.CREATE_SERVICE, service.getComponentMetadataForSupportLog(), StatusCode.COMPLETE, + "Service {} has been created by user {} ", service.getName(), userId); + return buildOkResponse(getComponentsUtils().getResponseFormat(ActionStatus.CREATED), actionResponse.left().value()); } - loggerSupportability.log(LoggerSupportabilityActions.CREATE_SERVICE, service.getComponentMetadataForSupportLog(), StatusCode.COMPLETE, - "Service {} has been created by user {} ", service.getName(), userId); - return buildOkResponse(getComponentsUtils().getResponseFormat(ActionStatus.CREATED), actionResponse.left().value()); } public Either parseToService(String serviceJson, User user) { diff --git a/catalog-be/src/main/java/org/openecomp/sdc/be/tosca/ToscaExportHandler.java b/catalog-be/src/main/java/org/openecomp/sdc/be/tosca/ToscaExportHandler.java index ff6d015d62..68795e1003 100644 --- a/catalog-be/src/main/java/org/openecomp/sdc/be/tosca/ToscaExportHandler.java +++ b/catalog-be/src/main/java/org/openecomp/sdc/be/tosca/ToscaExportHandler.java @@ -546,6 +546,7 @@ public class ToscaExportHandler { } toscaMetadata.put(JsonPresentationFields.SUB_CATEGORY.getPresentation(), categoryDefinition.getSubcategories().get(0).getName()); toscaMetadata.put(JsonPresentationFields.RESOURCE_VENDOR.getPresentation(), resource.getVendorName()); + toscaMetadata.put(JsonPresentationFields.TENANT.getPresentation(), resource.getTenant()); toscaMetadata.put(JsonPresentationFields.RESOURCE_VENDOR_RELEASE.getPresentation(), resource.getVendorRelease()); toscaMetadata.put(JsonPresentationFields.RESOURCE_VENDOR_MODEL_NUMBER.getPresentation(), resource.getResourceVendorModelNumber()); break; diff --git a/catalog-be/src/main/resources/config/error-configuration.yaml b/catalog-be/src/main/resources/config/error-configuration.yaml index 6bed75ef2e..c53efc21b1 100644 --- a/catalog-be/src/main/resources/config/error-configuration.yaml +++ b/catalog-be/src/main/resources/config/error-configuration.yaml @@ -2822,6 +2822,14 @@ errors: message: "Capability '%1' not found in '%2' '%3'." messageId: "SVC4186" + +#---------SVC4950----------------------------- + MISSING_TENANT_NAME: { + code: 400, + message: "Error: Missing Tenant name.", + messageId: "SVC4950" + } + #---------SVC4001------------------------------ NOT_PERMITTED_SPECIAL_CHARS: { code: 406, diff --git a/catalog-be/src/main/webapp/WEB-INF/keycloak.json b/catalog-be/src/main/webapp/WEB-INF/keycloak.json new file mode 100644 index 0000000000..d037661aec --- /dev/null +++ b/catalog-be/src/main/webapp/WEB-INF/keycloak.json @@ -0,0 +1,11 @@ +{ +"realm": "sdc", +"auth-server-url": "http://10.32.243.37:31613/", +"ssl-required": "external", +"resource": "sdc-app", +"public-client":true, +"bearer-only":true, +"use-resource-role-mappings": true, +"principal-attribute":"preferred_username", +"confidential-port": 0 +} diff --git a/catalog-be/src/main/webapp/WEB-INF/web.xml b/catalog-be/src/main/webapp/WEB-INF/web.xml index 9761b38043..8bd981164c 100644 --- a/catalog-be/src/main/webapp/WEB-INF/web.xml +++ b/catalog-be/src/main/webapp/WEB-INF/web.xml @@ -163,6 +163,18 @@ /sdc/* + + MultitenancyFilter + org.openecomp.sdc.be.filters.MultitenancyFilter + + + MultitenancyFilter + /keycloak/* + /sdc2/rest/v1/catalog/resources/* + /sdc2/rest/v1/catalog/services/* + /sdc2/rest/v1/followed + + java.lang.RuntimeException /sdc2/rest/v1/catalog/handleException/ diff --git a/catalog-be/src/test/java/org/openecomp/sdc/be/components/impl/ElementBusinessLogicTest.java b/catalog-be/src/test/java/org/openecomp/sdc/be/components/impl/ElementBusinessLogicTest.java index 9fb0efb862..60d45ea752 100644 --- a/catalog-be/src/test/java/org/openecomp/sdc/be/components/impl/ElementBusinessLogicTest.java +++ b/catalog-be/src/test/java/org/openecomp/sdc/be/components/impl/ElementBusinessLogicTest.java @@ -38,6 +38,9 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.HashMap; +import java.util.stream.Collectors; +import org.junit.Assert; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; @@ -68,6 +71,8 @@ import org.openecomp.sdc.exception.ResponseFormat; class ElementBusinessLogicTest extends BaseBusinessLogicMock { + private static final boolean MULTITENANCY_ENABLED = true; + private static final String TEST_TENANT = "test_tenant"; private User user; @Mock @@ -319,4 +324,83 @@ class ElementBusinessLogicTest extends BaseBusinessLogicMock { assertTrue(elementBusinessLogic.getBaseTypes("CAT01", user.getUserId(), null).isRight()); } + + @Test + void testGetFollowed_withMultitenancyValidTenant_thenReturnsSuccess() { + Assert.assertTrue(MULTITENANCY_ENABLED); + user.setUserId("admin1"); + user.setRole(Role.ADMIN.name()); + Set resources = new HashSet<>(); + Set services = new HashSet<>(); + Resource resource = new Resource(); + Service service = new Service(); + service.setTenant(TEST_TENANT); + resource.setTenant(TEST_TENANT); + Assert.assertNotNull(service.getTenant()); + Assert.assertNotNull(resource.getTenant()); + resources.add(resource); + services.add(service); + Assert.assertNotNull(getTestRoles()); + Assert.assertTrue(getTestRoles().contains(TEST_TENANT)); + + when(toscaOperationFacade.getFollowed(any(), anySet(), any(), eq(ComponentTypeEnum.RESOURCE))) + .thenReturn(Either.left(resources)); + when(toscaOperationFacade.getFollowed(any(), anySet(), any(), eq(ComponentTypeEnum.SERVICE))) + .thenReturn(Either.left(services)); + Map> result = elementBusinessLogic.getFollowed(user).left().value(); + Set realmroles =getTestRoles(); + Map> dataResponse = new HashMap<>(); + result.entrySet().stream() + .forEach(component->{component.setValue(component.getValue().stream().filter(cm->realmroles.stream() + .anyMatch(role->cm.getTenant().equals(role))).collect(Collectors.toList())); + dataResponse.put(component.getKey(), component.getValue()); + }); + assertEquals(result.size(), dataResponse.values().size()); + assertEquals(1, dataResponse.get("services").size()); + assertEquals(1, dataResponse.get("resources").size()); + } + + @Test + void testGetFollowed_withMultitenancyInValidTenant_thenReturnsEmptyList() { + String INVALID_TENANT="invalid_tenant"; + Assert.assertTrue(MULTITENANCY_ENABLED); + user.setUserId("admin1"); + user.setRole(Role.ADMIN.name()); + Set resources = new HashSet<>(); + Set services = new HashSet<>(); + Resource resource = new Resource(); + Service service = new Service(); + service.setTenant(INVALID_TENANT); + resource.setTenant(INVALID_TENANT); + Assert.assertNotNull(service.getTenant()); + Assert.assertNotNull(resource.getTenant()); + resources.add(resource); + services.add(service); + Assert.assertNotNull(getTestRoles()); + + when(toscaOperationFacade.getFollowed(any(), anySet(), any(), eq(ComponentTypeEnum.RESOURCE))) + .thenReturn(Either.left(resources)); + when(toscaOperationFacade.getFollowed(any(), anySet(), any(), eq(ComponentTypeEnum.SERVICE))) + .thenReturn(Either.left(services)); + Map> result = elementBusinessLogic.getFollowed(user).left().value(); + Set realmroles =getTestRoles(); + Map> dataResponse = new HashMap<>(); + result.entrySet().stream() + .forEach(component->{component.setValue(component.getValue().stream().filter(cm->realmroles.stream() + .anyMatch(role->cm.getTenant().equals(role))).collect(Collectors.toList())); + dataResponse.put(component.getKey(), component.getValue()); + }); + + assertEquals(result.size(), dataResponse.values().size(),"No Data available for "+INVALID_TENANT); + assertEquals(0, dataResponse.get("services").size()); + assertEquals(0, dataResponse.get("resources").size()); + } + + private Set getTestRoles(){ + Set roles = new HashSet<>(); + roles.add("test_admin"); + roles.add("test_tenant"); + return roles; + } + } \ No newline at end of file diff --git a/catalog-be/src/test/java/org/openecomp/sdc/be/components/impl/ResourceBusinessLogicTest.java b/catalog-be/src/test/java/org/openecomp/sdc/be/components/impl/ResourceBusinessLogicTest.java index 714ec20c10..e9735710da 100644 --- a/catalog-be/src/test/java/org/openecomp/sdc/be/components/impl/ResourceBusinessLogicTest.java +++ b/catalog-be/src/test/java/org/openecomp/sdc/be/components/impl/ResourceBusinessLogicTest.java @@ -47,12 +47,16 @@ import java.util.EnumMap; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.HashSet; import java.util.Map.Entry; import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.Stream; import javax.servlet.ServletContext; import org.apache.commons.lang3.tuple.ImmutablePair; +import org.hamcrest.MatcherAssert; +import org.junit.Assert; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; @@ -171,6 +175,8 @@ class ResourceBusinessLogicTest { private static final String GENERIC_VF_NAME = "org.openecomp.resource.abstract.nodes.VF"; private static final String GENERIC_CR_NAME = "org.openecomp.resource.abstract.nodes.CR"; private static final String GENERIC_PNF_NAME = "org.openecomp.resource.abstract.nodes.PNF"; + private static final boolean MULTITENANCY_ENABLED = true; + private static final String TEST_TENANT = "test_tenant"; private final ServletContext servletContext = Mockito.mock(ServletContext.class); private IElementOperation mockElementDao; @@ -2556,4 +2562,55 @@ class ResourceBusinessLogicTest { assertEquals(ActionStatus.COMPONENT_IN_USE_BY_ANOTHER_COMPONENT, actualOperationException.getActionStatus()); assertEquals("resource_name", actualOperationException.getParams()[0]); } + + + @Test + void testCreateResource_withMultitenancyWithTenant_Success() { + Assert.assertTrue(MULTITENANCY_ENABLED); + validateUserRoles(Role.ADMIN, Role.DESIGNER); + Resource resource = createResourceObject(false); + resource.setTenant(TEST_TENANT); + Resource createdResource = null; + try { + when(toscaOperationFacade + .validateComponentNameAndModelExists(resource.getName(), null, ResourceTypeEnum.VFC, ComponentTypeEnum.RESOURCE)) + .thenReturn(Either.left(false)); + createdResource = bl.createResource(resource, AuditingActionEnum.CREATE_RESOURCE, user, null, null); + assertThat(createResourceObject(true)).isEqualTo(createdResource); + MatcherAssert.assertThat("Unauthorized Tenant", getTestRoles().contains(resource.getTenant())); + } catch (ComponentException e) { + assertThat(Integer.valueOf(200)).isEqualTo(e.getResponseFormat() + .getStatus()); + } + } + + @Test + void testCreateResource_withMultitenancyWithInvalidTenant_Failure() { + Assert.assertTrue(MULTITENANCY_ENABLED); + validateUserRoles(Role.ADMIN, Role.DESIGNER); + Resource resource = createResourceObject(false); + resource.setTenant("invalid_tenant"); + Resource createdResource = null; + try { + MatcherAssert.assertThat("Unauthorized Tenant", !getTestRoles().contains(resource.getTenant())); + when(toscaOperationFacade + .validateComponentNameAndModelExists(resource.getName(), null, ResourceTypeEnum.VFC, ComponentTypeEnum.RESOURCE)) + .thenReturn(Either.left(false)); + createdResource = bl.createResource(resource, AuditingActionEnum.CREATE_RESOURCE, user, null, null); + + assertThat(createResourceObject(true)).isEqualTo(createdResource); + MatcherAssert.assertThat("Unauthorized Tenant", !getTestRoles().contains(resource.getTenant())); + } catch (ComponentException e) { + assertThat(new Integer(200)).isEqualTo(e.getResponseFormat() + .getStatus()); + } + } + + private Set getTestRoles(){ + Set roles = new HashSet<>(); + roles.add("test_admin"); + roles.add("test_tenant"); + return roles; + } + } diff --git a/catalog-be/src/test/java/org/openecomp/sdc/be/components/impl/ServiceBusinessLogicTest.java b/catalog-be/src/test/java/org/openecomp/sdc/be/components/impl/ServiceBusinessLogicTest.java index 69938cd527..a5f823b12f 100644 --- a/catalog-be/src/test/java/org/openecomp/sdc/be/components/impl/ServiceBusinessLogicTest.java +++ b/catalog-be/src/test/java/org/openecomp/sdc/be/components/impl/ServiceBusinessLogicTest.java @@ -30,6 +30,8 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.Mockito.when; @@ -44,8 +46,12 @@ import java.util.List; import java.util.Map; import java.util.Optional; import java.util.UUID; +import java.util.Set; +import java.util.HashSet; import org.apache.commons.lang3.tuple.ImmutablePair; +import org.hamcrest.MatcherAssert; +import org.junit.Assert; import org.junit.jupiter.api.Test; import org.mockito.Mockito; import org.openecomp.sdc.be.components.impl.exceptions.ComponentException; @@ -83,6 +89,8 @@ class ServiceBusinessLogicTest extends ServiceBusinessLogicBaseTestSetup { private final static String DEFAULT_ICON = "defaulticon"; private static final String ALREADY_EXIST = "alreadyExist"; + private static final boolean MULTITENANCY_ENABLED = true; + private static final String TEST_TENANT = "test_tenant"; @Test void testGetComponentAuditRecordsCertifiedVersion() { @@ -1069,4 +1077,39 @@ class ServiceBusinessLogicTest extends ServiceBusinessLogicBaseTestSetup { return propertyList; } + + @Test + void testCreateService_withMultitenancyValidTenant_Success() { + Assert.assertTrue(MULTITENANCY_ENABLED); + Service service = createServiceObject(false); + service.setTenant(TEST_TENANT); + when(genericTypeBusinessLogic.fetchDerivedFromGenericType(service, null)).thenReturn(Either.left(genericService)); + Either createResponse = bl.createService(service, user); + + if (createResponse.isRight()) { + assertEquals(new Integer(200), createResponse.right().value().getStatus()); + } + MatcherAssert.assertThat("Unauthorized Tenant", getTestRoles().contains(service.getTenant())); + assertEquals(TEST_TENANT, service.getTenant()); + assertEqualsServiceObject(createServiceObject(true), createResponse.left().value()); + } + + + @Test + void testCreateService_withMultitenancyInvalidTenant_Failure() { + Service service = createServiceObject(false); + service.setTenant("invalid_tenant"); + when(genericTypeBusinessLogic.fetchDerivedFromGenericType(service, null)).thenReturn(Either.left(genericService)); + Either createResponse = bl.createService(service, user); + MatcherAssert.assertThat("Unauthorized Tenant", !getTestRoles().contains(service.getTenant())); + assertNotEquals(TEST_TENANT, service.getTenant()); + assertEqualsServiceObject(createServiceObject(true), createResponse.left().value()); + } + + private Set getTestRoles(){ + Set roles = new HashSet<>(); + roles.add("test_admin"); + roles.add("test_tenant"); + return roles; + } } -- cgit 1.2.3-korg