diff --git a/scb-engine/src/main/java/io/securecodebox/engine/WebMvcConfigurer.java b/scb-engine/src/main/java/io/securecodebox/engine/CustomWebMvcConfigurer.java similarity index 94% rename from scb-engine/src/main/java/io/securecodebox/engine/WebMvcConfigurer.java rename to scb-engine/src/main/java/io/securecodebox/engine/CustomWebMvcConfigurer.java index 8f15a32b..58ab3080 100644 --- a/scb-engine/src/main/java/io/securecodebox/engine/WebMvcConfigurer.java +++ b/scb-engine/src/main/java/io/securecodebox/engine/CustomWebMvcConfigurer.java @@ -21,7 +21,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** * This class adds additional resources to the spring application. @@ -31,7 +31,8 @@ * @since 02.03.18 */ @Configuration -public class WebMvcConfigurer extends WebMvcConfigurerAdapter { +public class CustomWebMvcConfigurer implements WebMvcConfigurer { + @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/forms/**").addResourceLocations("classpath:/forms/"); diff --git a/scb-engine/src/main/java/io/securecodebox/engine/auth/CamundaAuthContextSetup.java b/scb-engine/src/main/java/io/securecodebox/engine/auth/CamundaAuthContextSetup.java new file mode 100644 index 00000000..20d9bddf --- /dev/null +++ b/scb-engine/src/main/java/io/securecodebox/engine/auth/CamundaAuthContextSetup.java @@ -0,0 +1,79 @@ +/* + * + * SecureCodeBox (SCB) + * Copyright 2015-2018 iteratec GmbH + * + * 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. + * / + */ +package io.securecodebox.engine.auth; + +import io.securecodebox.engine.service.AuthService; +import org.camunda.bpm.engine.IdentityService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +@Configuration +@EnableWebSecurity +@ConditionalOnProperty(name = "securecodebox.rest.auth", havingValue = "basic auth") +public class CamundaAuthContextSetup implements WebMvcConfigurer { + private static final Logger LOG = LoggerFactory.getLogger(CamundaAuthContextSetup.class); + + @Autowired + IdentityService identityService; + + @Autowired + private AuthService authService; + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(new CamundaAuthContextSetupInterceptor()).addPathPatterns("/box/**"); + } + + /** + * Sets up the Camunda Authentication Context before + * the Resource gets executed and tears it down afterwards + */ + private class CamundaAuthContextSetupInterceptor implements HandlerInterceptor { + + @Override + public boolean preHandle( + HttpServletRequest request, + HttpServletResponse response, + Object handler) { + identityService.setAuthentication(authService.getAuthentication()); + + return true; + } + + @Override + public void postHandle( + HttpServletRequest request, + HttpServletResponse response, + Object handler, + ModelAndView modelAndView) { + identityService.clearAuthentication(); + } + } +} diff --git a/scb-engine/src/main/java/io/securecodebox/engine/helper/DefaultGroupConfiguration.java b/scb-engine/src/main/java/io/securecodebox/engine/helper/DefaultGroupConfiguration.java index 2c4d43e6..30bb304f 100644 --- a/scb-engine/src/main/java/io/securecodebox/engine/helper/DefaultGroupConfiguration.java +++ b/scb-engine/src/main/java/io/securecodebox/engine/helper/DefaultGroupConfiguration.java @@ -63,7 +63,12 @@ public void postProcessEngineBuild(final ProcessEngine processEngine) { Resources.PROCESS_INSTANCE, Permissions.READ, Permissions.UPDATE ); - + createAuthorizationForGroup( + processEngine.getAuthorizationService(), + GROUP_SCANNER, + Resources.PROCESS_DEFINITION, + Permissions.READ, Permissions.READ_INSTANCE, Permissions.UPDATE_INSTANCE + ); createGroup(identityService, GROUP_CI); createAuthorizationForGroup( diff --git a/scb-engine/src/main/java/io/securecodebox/engine/listener/ListenerRegistrarPlugin.java b/scb-engine/src/main/java/io/securecodebox/engine/listener/ListenerRegistrarPlugin.java index a635e024..110f4c4e 100644 --- a/scb-engine/src/main/java/io/securecodebox/engine/listener/ListenerRegistrarPlugin.java +++ b/scb-engine/src/main/java/io/securecodebox/engine/listener/ListenerRegistrarPlugin.java @@ -19,6 +19,7 @@ package io.securecodebox.engine.listener; +import io.securecodebox.engine.tenancy.CustomTenantIdProvider; import org.camunda.bpm.engine.impl.bpmn.parser.BpmnParseListener; import org.camunda.bpm.engine.impl.cfg.AbstractProcessEnginePlugin; import org.camunda.bpm.engine.impl.cfg.ProcessEngineConfigurationImpl; @@ -38,9 +39,14 @@ public class ListenerRegistrarPlugin extends AbstractProcessEnginePlugin { @Autowired DefaultListenerRegistrar registrar; + @Autowired + CustomTenantIdProvider tenantIdProvider; + @Override public void preInit(ProcessEngineConfigurationImpl processEngineConfiguration) { + processEngineConfiguration.setTenantIdProvider(tenantIdProvider); + // get all existing preParseListeners List preParseListeners = processEngineConfiguration.getCustomPreBPMNParseListeners(); diff --git a/scb-engine/src/main/java/io/securecodebox/engine/rest/ScanJobResource.java b/scb-engine/src/main/java/io/securecodebox/engine/rest/ScanJobResource.java index 31fa79a0..1e189841 100644 --- a/scb-engine/src/main/java/io/securecodebox/engine/rest/ScanJobResource.java +++ b/scb-engine/src/main/java/io/securecodebox/engine/rest/ScanJobResource.java @@ -36,6 +36,7 @@ import io.swagger.annotations.ApiResponse; import io.swagger.annotations.ApiResponses; import io.swagger.annotations.Authorization; +import org.camunda.bpm.engine.IdentityService; import org.camunda.bpm.engine.ProcessEngine; import org.camunda.bpm.engine.exception.NotFoundException; import org.camunda.bpm.engine.externaltask.ExternalTask; @@ -55,9 +56,11 @@ import javax.validation.Valid; import java.util.HashMap; +import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.UUID; +import java.util.stream.Collectors; /** * API / Endpoint for scan jobs. @@ -117,15 +120,20 @@ public ResponseEntity lockJob( ) @PathVariable UUID scannerId ) { - try{ + try { authService.checkAuthorizedFor(ResourceType.SECURITY_TEST, PermissionType.READ); - }catch (InsufficientAuthenticationException e){ + } catch (InsufficientAuthenticationException e) { return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); } - ExternalTaskQueryBuilder externalTaskQueryBuilder = engine.getExternalTaskService() .fetchAndLock(1, scannerId.toString()); - externalTaskQueryBuilder.topic(topic, LOCK_DURATION_MS); + + List tenantIds = authService.getAuthentication().getTenantIds(); + if(tenantIds.isEmpty()){ + externalTaskQueryBuilder.topic(topic, LOCK_DURATION_MS).withoutTenantId(); + } else { + externalTaskQueryBuilder.topic(topic, LOCK_DURATION_MS).tenantIdIn(tenantIds.stream().toArray(String[]::new)); + } LockedExternalTask result = Iterables.getFirst(externalTaskQueryBuilder.execute(), null); if (result != null) { @@ -167,9 +175,9 @@ public ResponseEntity completeJob( @PathVariable UUID id, @Valid @RequestBody ScanResult result ) { - try{ + try { authService.checkAuthorizedFor(id.toString(), ResourceType.SECURITY_TEST, PermissionType.UPDATE); - } catch (InsufficientAuthenticationException e){ + } catch (InsufficientAuthenticationException e) { return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); } @@ -227,9 +235,9 @@ public ResponseEntity failJob( @PathVariable UUID id, @Valid @RequestBody ScanFailure result ) { - try{ + try { authService.checkAuthorizedFor(id.toString(), ResourceType.SECURITY_TEST, PermissionType.UPDATE); - }catch (InsufficientAuthenticationException e){ + } catch (InsufficientAuthenticationException e) { return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); } @@ -253,6 +261,7 @@ public ResponseEntity failJob( engine.getExternalTaskService() .handleFailure(id.toString(), result.getScannerId().toString(), result.getErrorMessage(), result.getErrorDetails(), retriesLeft, 1000); + return ResponseEntity.ok().build(); } diff --git a/scb-engine/src/main/java/io/securecodebox/engine/rest/SecurityTestResource.java b/scb-engine/src/main/java/io/securecodebox/engine/rest/SecurityTestResource.java index d9e44004..cfa7d900 100644 --- a/scb-engine/src/main/java/io/securecodebox/engine/rest/SecurityTestResource.java +++ b/scb-engine/src/main/java/io/securecodebox/engine/rest/SecurityTestResource.java @@ -141,8 +141,12 @@ public ResponseEntity> startSecurityTests( List processInstances = new LinkedList<>(); - for (SecurityTestConfiguration securityTest : securityTests) { - processInstances.add(securityTestService.startSecurityTest(securityTest)); + try { + for (SecurityTestConfiguration securityTest : securityTests) { + processInstances.add(securityTestService.startSecurityTest(securityTest)); + } + } catch (InsufficientAuthorizationException e){ + return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); } return ResponseEntity.status(HttpStatus.CREATED).body(processInstances); @@ -209,8 +213,8 @@ public ResponseEntity getSecurityTest( if (securityTest.isFinished()) { return ResponseEntity.status(HttpStatus.OK).body(securityTest); } - return ResponseEntity.status(HttpStatus.PARTIAL_CONTENT).body(securityTest); + return ResponseEntity.status(HttpStatus.PARTIAL_CONTENT).body(securityTest); } catch (SecurityTestService.SecurityTestNotFoundException e) { return ResponseEntity.status(HttpStatus.NOT_FOUND).build(); } catch (SecurityTestService.SecurityTestErroredException e) { diff --git a/scb-engine/src/main/java/io/securecodebox/engine/service/AuthService.java b/scb-engine/src/main/java/io/securecodebox/engine/service/AuthService.java index 7101bc90..a6c7a9e9 100644 --- a/scb-engine/src/main/java/io/securecodebox/engine/service/AuthService.java +++ b/scb-engine/src/main/java/io/securecodebox/engine/service/AuthService.java @@ -22,6 +22,7 @@ import io.securecodebox.engine.model.PermissionType; import io.securecodebox.engine.model.ResourceType; import org.camunda.bpm.engine.identity.Group; +import org.camunda.bpm.engine.identity.Tenant; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.Authentication; @@ -39,20 +40,24 @@ public class AuthService { private static String AUTH_DISABLED_TYPE = "none"; private static final Logger LOG = LoggerFactory.getLogger(AuthService.class); - @Autowired ProcessEngine engine; @Value("${securecodebox.rest.auth}") private String authType; + @Autowired + public AuthService(ProcessEngine engine){ + this.engine = engine; + } + public void checkAuthorizedFor(String resourceId, ResourceType resource, PermissionType permission) throws InsufficientAuthorizationException { - if(AUTH_DISABLED_TYPE.equals(authType)){ + if (AUTH_DISABLED_TYPE.equals(authType)) { return; } Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - if(authentication == null) { + if (authentication == null) { throw new InsufficientAuthorizationException("No authentication provided."); } @@ -66,7 +71,7 @@ public void checkAuthorizedFor(String resourceId, ResourceType resource, Permiss .collect(Collectors.toList()); boolean isAuthorized = false; - if(resourceId == null){ + if (resourceId == null) { isAuthorized = engine.getAuthorizationService().isUserAuthorized( authentication.getName(), groups, @@ -86,7 +91,7 @@ public void checkAuthorizedFor(String resourceId, ResourceType resource, Permiss LOG.trace("Current User '{}' with groups: '{}'", authentication.getName(), groups); LOG.trace("Access check for [{}, {}, {}]: {}", resourceId, resource, permission, isAuthorized); - if(!isAuthorized){ + if (!isAuthorized) { throw new InsufficientAuthorizationException("User is not authorised to perform this action."); } } @@ -94,4 +99,33 @@ public void checkAuthorizedFor(String resourceId, ResourceType resource, Permiss public void checkAuthorizedFor(ResourceType resource, PermissionType permission) throws InsufficientAuthorizationException { this.checkAuthorizedFor(null, resource, permission); } + + public org.camunda.bpm.engine.impl.identity.Authentication getAuthentication() { + String userId = SecurityContextHolder.getContext().getAuthentication().getName(); + + List groups = engine + .getIdentityService() + .createGroupQuery() + .groupMember(userId) + .list() + .stream() + .map(Group::getId) + .collect(Collectors.toList()); + + List tenants = engine + .getIdentityService() + .createTenantQuery() + .userMember(userId) + .list() + .stream() + .map(Tenant::getId) + .collect(Collectors.toList()); + + return new org.camunda.bpm.engine.impl.identity.Authentication( + userId, + groups, + tenants + ); + + } } diff --git a/scb-engine/src/main/java/io/securecodebox/engine/service/SecurityTestService.java b/scb-engine/src/main/java/io/securecodebox/engine/service/SecurityTestService.java index 12916df8..37424a15 100644 --- a/scb-engine/src/main/java/io/securecodebox/engine/service/SecurityTestService.java +++ b/scb-engine/src/main/java/io/securecodebox/engine/service/SecurityTestService.java @@ -19,6 +19,7 @@ package io.securecodebox.engine.service; import io.securecodebox.constants.DefaultFields; +import io.securecodebox.engine.auth.InsufficientAuthorizationException; import io.securecodebox.model.execution.Target; import io.securecodebox.model.findings.Finding; import io.securecodebox.model.rest.Report; @@ -65,7 +66,7 @@ public void checkSecurityTestDefinitionExistence(String key) throws NonExistentS } } - public UUID startSecurityTest(SecurityTestConfiguration securityTest){ + public UUID startSecurityTest(SecurityTestConfiguration securityTest) throws InsufficientAuthorizationException { Map values = new HashMap<>(); List targets = new LinkedList<>(); @@ -77,6 +78,10 @@ public UUID startSecurityTest(SecurityTestConfiguration securityTest){ values.put(DefaultFields.PROCESS_TARGETS.name(), ProcessVariableHelper.generateObjectValue(targets)); values.put(DefaultFields.PROCESS_META_DATA.name(), securityTest.getMetaData()); + if(securityTest.getTenant() != null){ + values.put(DefaultFields.PROCESS_TENANT.name(), securityTest.getTenant()); + } + ProcessInstance instance = engine.getRuntimeService().startProcessInstanceByKey(securityTest.getProcessDefinitionKey(), values); return UUID.fromString(instance.getProcessInstanceId()); } @@ -130,10 +135,15 @@ public SecurityTest getCompletedSecurityTest(UUID id) throws SecurityTestNotFoun String context = (String) variables.get(DefaultFields.PROCESS_CONTEXT.name()).getValue(); String name = (String) variables.get(DefaultFields.PROCESS_NAME.name()).getValue(); + String tenant = null; + if(variables.containsKey(DefaultFields.PROCESS_TENANT.name())){ + tenant = (String) variables.get(DefaultFields.PROCESS_TENANT.name()).getValue(); + } + List targets = getListValue(variables, DefaultFields.PROCESS_TARGETS, Target.class); Map metaData = (Map) variables.get(DefaultFields.PROCESS_META_DATA.name()).getValue(); - return new SecurityTest(id, context, name, targets.get(0), report, metaData); + return new SecurityTest(id, context, name, targets.get(0), report, metaData, tenant); } private List getListValue(Map variables, DefaultFields name, Class type) { diff --git a/scb-engine/src/main/java/io/securecodebox/engine/tenancy/CustomTenantIdProvider.java b/scb-engine/src/main/java/io/securecodebox/engine/tenancy/CustomTenantIdProvider.java new file mode 100644 index 00000000..b8b0fed0 --- /dev/null +++ b/scb-engine/src/main/java/io/securecodebox/engine/tenancy/CustomTenantIdProvider.java @@ -0,0 +1,96 @@ +/* + * + * SecureCodeBox (SCB) + * Copyright 2015-2018 iteratec GmbH + * + * 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. + * / + */ +package io.securecodebox.engine.tenancy; + +import io.securecodebox.constants.DefaultFields; +import io.securecodebox.engine.auth.InsufficientAuthorizationException; +import io.securecodebox.engine.service.AuthService; +import org.camunda.bpm.engine.IdentityService; +import org.camunda.bpm.engine.impl.cfg.multitenancy.TenantIdProvider; +import org.camunda.bpm.engine.impl.cfg.multitenancy.TenantIdProviderCaseInstanceContext; +import org.camunda.bpm.engine.impl.cfg.multitenancy.TenantIdProviderHistoricDecisionInstanceContext; +import org.camunda.bpm.engine.impl.cfg.multitenancy.TenantIdProviderProcessInstanceContext; +import org.camunda.bpm.engine.impl.context.Context; +import org.camunda.bpm.engine.impl.identity.Authentication; +import org.camunda.bpm.engine.variable.VariableMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class CustomTenantIdProvider implements TenantIdProvider { + private static final Logger LOG = LoggerFactory.getLogger(CustomTenantIdProvider.class); + + CustomTenantIdProvider (){ + super(); + LOG.debug("Init CustomTenantIdProvider"); + } + + @Override + public String provideTenantIdForProcessInstance(TenantIdProviderProcessInstanceContext ctx) { + return getTenantIdOfCurrentAuthentication(ctx.getVariables()); + } + + @Override + public String provideTenantIdForCaseInstance(TenantIdProviderCaseInstanceContext ctx) { + return getTenantIdOfCurrentAuthentication(ctx.getVariables()); + } + + @Override + public String provideTenantIdForHistoricDecisionInstance(TenantIdProviderHistoricDecisionInstanceContext ctx) { + return getTenantIdOfCurrentAuthentication(ctx.getExecution().getVariablesTyped()); + } + + protected String getTenantIdOfCurrentAuthentication(VariableMap variableMap) { + LOG.debug("Determining if process should be started with a tenant."); + + // Process doesn't have tenant variable -> was most likely started via camunda ui + if(!variableMap.containsKey(DefaultFields.PROCESS_TENANT.name())){ + LOG.debug("Process started without tenant variable -> Process will not be associated with a tenant"); + return null; + } + + String specifiedTenant = variableMap.getValue(DefaultFields.PROCESS_TENANT.name(), String.class); + + // Tenant Id was left empty or was explicitly set to null -> start without tenant + if(specifiedTenant == null || specifiedTenant.equals("")){ + LOG.debug("Tenant field in target was not specified or set to null -> Process will not be associated with a tenant"); + return null; + } + + AuthService authService = new AuthService(Context.getProcessEngineConfiguration().getProcessEngine()); + Authentication currentAuthentication = authService.getAuthentication(); + + if (currentAuthentication == null) { + throw new InsufficientAuthorizationException("No authenticated user"); + } + + boolean userIsMemberOfTenant = currentAuthentication.getTenantIds().stream().anyMatch(tenant -> tenant.equals(specifiedTenant)); + + if(userIsMemberOfTenant){ + LOG.debug("Process started with tenant and user is member of tenant -> Process will be started with tenant '{}'", specifiedTenant); + return specifiedTenant; + } else { + LOG.debug("Process started with tenant, BUT user is NOT a member of the tenant -> Process will crash"); + throw new InsufficientAuthorizationException("User is not a member of the specified Tenant"); + } + } + +} diff --git a/scb-sdk/src/main/java/io/securecodebox/constants/DefaultFields.java b/scb-sdk/src/main/java/io/securecodebox/constants/DefaultFields.java index 1f1af1f6..f812784d 100644 --- a/scb-sdk/src/main/java/io/securecodebox/constants/DefaultFields.java +++ b/scb-sdk/src/main/java/io/securecodebox/constants/DefaultFields.java @@ -27,5 +27,6 @@ public enum DefaultFields { PROCESS_CONTEXT, PROCESS_SCANNER_ID, PROCESS_SCANNER_TYPE, PROCESS_AUTOMATED, PROCESS_FINDINGS, PROCESS_RAW_FINDINGS, PROCESS_SCANNERS, PROCESS_TARGETS, PROCESS_REPORT, - PROCESS_RESULT_APPROVED, PROCESS_ATTRIBUTE_MAPPING, PROCESS_NAME, PROCESS_META_DATA + PROCESS_RESULT_APPROVED, PROCESS_ATTRIBUTE_MAPPING, PROCESS_NAME, PROCESS_META_DATA, + PROCESS_TENANT } diff --git a/scb-sdk/src/main/java/io/securecodebox/model/securitytest/AbstractSecurityTest.java b/scb-sdk/src/main/java/io/securecodebox/model/securitytest/AbstractSecurityTest.java index c64e72d1..a1eed49a 100644 --- a/scb-sdk/src/main/java/io/securecodebox/model/securitytest/AbstractSecurityTest.java +++ b/scb-sdk/src/main/java/io/securecodebox/model/securitytest/AbstractSecurityTest.java @@ -43,7 +43,15 @@ public abstract class AbstractSecurityTest { Target target; @JsonProperty - private Map metaData; + @ApiModelProperty( + value = "A tenant is a camunda concept. A tenant can have both users and groups. It can be used to restrict the access to your security tests to members of the tenant.", + allowEmptyValue = true, + example = "team-1" + ) + String tenant; + + @JsonProperty + private Map metaData; public String getContext() { return context; @@ -76,4 +84,12 @@ public Map getMetaData() { public void setMetaData(Map metaData) { this.metaData = metaData; } + + public String getTenant() { + return tenant; + } + + public void setTenant(String tenant) { + this.tenant = tenant; + } } diff --git a/scb-sdk/src/main/java/io/securecodebox/model/securitytest/SecurityTest.java b/scb-sdk/src/main/java/io/securecodebox/model/securitytest/SecurityTest.java index c221c2be..c6f159d6 100644 --- a/scb-sdk/src/main/java/io/securecodebox/model/securitytest/SecurityTest.java +++ b/scb-sdk/src/main/java/io/securecodebox/model/securitytest/SecurityTest.java @@ -37,11 +37,16 @@ public class SecurityTest extends AbstractSecurityTest { public SecurityTest() {} public SecurityTest(UUID id, String context, String name, Target target, Report report, Map metaData) { + this(id, context, name, target, report, metaData, null); + } + + public SecurityTest(UUID id, String context, String name, Target target, Report report, Map metaData, String tenant) { this.id = id; this.context = context; this.name = name; this.target = target; this.report = report; + this.tenant = tenant; this.setMetaData(metaData); }