getDatabases(String type) {
+ return executeRequest(() -> restClient.getDatabases(type));
+ }
+
+ @Override
+ public CreatedDatabaseV3 createDatabase(String type, AdapterDatabaseCreateRequest createRequest) {
+ return executeRequest(() -> restClient.createDatabase(type, createRequest));
+ }
+
+ @Override
+ public String updateSettings(String type, String dbName, UpdateSettingsAdapterRequest request) {
+ return executeRequest(() -> restClient.updateSettings(type, dbName, request));
+ }
+
+ @Override
+ public LogicalBackupAdapterResponse backupV2(String dbType, BackupAdapterRequest request) {
+ return executeRequest(() -> restClient.backupV2(dbType, request));
+ }
+
+ @Override
+ public LogicalBackupAdapterResponse trackBackupV2(String dbType, String logicalBackupName, String storageName, String blobPath) {
+ return executeRequest(() -> restClient.trackBackupV2(dbType, logicalBackupName, storageName, blobPath));
+ }
+
+ @Override
+ public void deleteBackupV2(String dbType, String logicalBackupName, String blobPath) {
+ executeRequest(() -> {
+ restClient.deleteBackupV2(dbType, logicalBackupName, blobPath);
+ return null;
+ });
+ }
+
+ @Override
+ public LogicalRestoreAdapterResponse restoreV2(String dbType, String logicalRestoreName, Boolean dryRun, RestoreAdapterRequest request) {
+ return executeRequest(() -> restClient.restoreV2(dbType, logicalRestoreName, dryRun, request));
+ }
+
+ @Override
+ public LogicalRestoreAdapterResponse trackRestoreV2(String dbType, String logicalBackupName, String storageName, String blobPath) {
+ return executeRequest(() -> restClient.trackRestoreV2(dbType, logicalBackupName, storageName, blobPath));
+ }
+
+ @Override
+ public void close() throws Exception {
+ restClient.close();
+ }
+}
diff --git a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/security/ServiceAccountRolesAugmentor.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/security/ServiceAccountRolesAugmentor.java
new file mode 100644
index 00000000..a7861882
--- /dev/null
+++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/security/ServiceAccountRolesAugmentor.java
@@ -0,0 +1,70 @@
+package com.netcracker.cloud.dbaas.security;
+
+import com.netcracker.cloud.dbaas.Constants;
+import io.quarkus.security.credential.Credential;
+import io.quarkus.security.credential.PasswordCredential;
+import io.quarkus.security.identity.AuthenticationRequestContext;
+import io.quarkus.security.identity.SecurityIdentity;
+import io.quarkus.security.identity.SecurityIdentityAugmentor;
+import io.quarkus.security.runtime.QuarkusSecurityIdentity;
+import io.smallrye.mutiny.Uni;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+
+import java.util.Set;
+
+/**
+ * Augments a {@link SecurityIdentity} with roles based on a service account name
+ * derived from the principal.
+ *
+ * Principal name format
+ *
+ * The principal name is expected to contain the service account name as the
+ * last segment, separated by colons:
+ *
+ *
+ *
+ * <prefix>:<serviceAccountName>
+ *
+ *
+ *
+ * The service account name is extracted as the substring after the last {@code ':'}
+ * character and is used to resolve roles via {@link ServiceAccountRolesManager}.
+ *
+ */
+@ApplicationScoped
+public class ServiceAccountRolesAugmentor implements SecurityIdentityAugmentor {
+ @Inject
+ ServiceAccountRolesManager rolesManager;
+
+ public ServiceAccountRolesAugmentor(ServiceAccountRolesManager rolesManager) {
+ this.rolesManager = rolesManager;
+ }
+
+ @Override
+ public Uni augment(SecurityIdentity identity, AuthenticationRequestContext context) {
+ if (identity.isAnonymous()) {
+ return Uni.createFrom().item(identity);
+ }
+
+ // skip if basic auth
+ for (Credential cred : identity.getCredentials()) {
+ if (cred instanceof PasswordCredential) {
+ return Uni.createFrom().item(identity);
+ }
+ }
+
+ String principal = identity.getPrincipal().getName();
+ String serviceName = principal.substring(principal.lastIndexOf(':') + 1);
+ Set roles = rolesManager.getRolesByServiceAccountName(serviceName);
+
+ QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder(identity);
+ if (roles != null && !roles.isEmpty()) {
+ builder.addRoles(roles);
+ } else {
+ builder.addRole(Constants.DB_CLIENT);
+ }
+
+ return Uni.createFrom().item(builder.build());
+ }
+}
diff --git a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/security/ServiceAccountRolesManager.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/security/ServiceAccountRolesManager.java
new file mode 100644
index 00000000..854fcab4
--- /dev/null
+++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/security/ServiceAccountRolesManager.java
@@ -0,0 +1,42 @@
+package com.netcracker.cloud.dbaas.security;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
+import io.quarkus.runtime.StartupEvent;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.enterprise.event.Observes;
+import lombok.NoArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.*;
+
+@ApplicationScoped
+@NoArgsConstructor
+@Slf4j
+public class ServiceAccountRolesManager {
+ private final Map> serviceAccountsWithRoles = new HashMap<>();
+
+ public Set getRolesByServiceAccountName(String serviceAccountName) {
+ return serviceAccountsWithRoles.get(serviceAccountName);
+ }
+
+ void onStart(@Observes StartupEvent ev, @ConfigProperty(name = "dbaas.security.k8s.service.account.roles.path") String rolesConfigPath) {
+ try {
+ log.info("Start kubernetes service account roles loading from {}", rolesConfigPath);
+ String rawYaml = Files.readString(Path.of(rolesConfigPath));
+
+ ObjectMapper yamlReader = new ObjectMapper(new YAMLFactory());
+ Map rawServiceAccountsWithRoles = yamlReader.readValue(rawYaml, Map.class);
+ for (Map.Entry serviceAccount : rawServiceAccountsWithRoles.entrySet()) {
+ List roles = (List) serviceAccount.getValue();
+ serviceAccountsWithRoles.put(serviceAccount.getKey(), new HashSet<>(roles));
+ }
+ log.info("Roles for kubernetes service accounts loaded");
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to parse user roles secret YAML", e);
+ }
+ }
+}
diff --git a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/security/filters/AuthFilterSelector.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/security/filters/AuthFilterSelector.java
new file mode 100644
index 00000000..4d98ef1a
--- /dev/null
+++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/security/filters/AuthFilterSelector.java
@@ -0,0 +1,9 @@
+package com.netcracker.cloud.dbaas.security.filters;
+
+import jakarta.ws.rs.client.ClientRequestFilter;
+
+public interface AuthFilterSelector {
+ void selectAuthFilter(ClientRequestFilter authFilter);
+
+ ClientRequestFilter getAuthFilter();
+}
diff --git a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/rest/BasicAuthFilter.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/security/filters/BasicAuthFilter.java
similarity index 89%
rename from dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/rest/BasicAuthFilter.java
rename to dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/security/filters/BasicAuthFilter.java
index 7957e73d..c3b0a7ad 100644
--- a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/rest/BasicAuthFilter.java
+++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/security/filters/BasicAuthFilter.java
@@ -1,4 +1,4 @@
-package com.netcracker.cloud.dbaas.rest;
+package com.netcracker.cloud.dbaas.security.filters;
import jakarta.annotation.Priority;
import jakarta.ws.rs.client.ClientRequestContext;
@@ -13,7 +13,7 @@
@Priority(AUTHENTICATION)
public class BasicAuthFilter implements ClientRequestFilter {
- private String header;
+ private final String header;
public BasicAuthFilter(String username, String password) {
header = "Basic " + Base64.getEncoder().encodeToString((username + ":" + password).getBytes());
diff --git a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/security/filters/DynamicAuthFilter.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/security/filters/DynamicAuthFilter.java
new file mode 100644
index 00000000..49197251
--- /dev/null
+++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/security/filters/DynamicAuthFilter.java
@@ -0,0 +1,29 @@
+package com.netcracker.cloud.dbaas.security.filters;
+
+import jakarta.ws.rs.client.ClientRequestContext;
+import jakarta.ws.rs.client.ClientRequestFilter;
+
+import java.io.IOException;
+
+public class DynamicAuthFilter implements ClientRequestFilter, AuthFilterSelector {
+ private volatile ClientRequestFilter authFilter;
+
+ public DynamicAuthFilter(ClientRequestFilter defaultAuthFilter) {
+ this.authFilter = defaultAuthFilter;
+ }
+
+ @Override
+ public void filter(ClientRequestContext clientRequestContext) throws IOException {
+ authFilter.filter(clientRequestContext);
+ }
+
+ @Override
+ public void selectAuthFilter(ClientRequestFilter authFilter) {
+ this.authFilter = authFilter;
+ }
+
+ @Override
+ public ClientRequestFilter getAuthFilter() {
+ return this.authFilter;
+ }
+}
diff --git a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/security/filters/KubernetesTokenAuthFilter.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/security/filters/KubernetesTokenAuthFilter.java
new file mode 100644
index 00000000..a0653047
--- /dev/null
+++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/security/filters/KubernetesTokenAuthFilter.java
@@ -0,0 +1,23 @@
+package com.netcracker.cloud.dbaas.security.filters;
+
+import jakarta.ws.rs.client.ClientRequestContext;
+import jakarta.ws.rs.client.ClientRequestFilter;
+import jakarta.ws.rs.core.HttpHeaders;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.IOException;
+import java.util.function.Supplier;
+
+@Slf4j
+public class KubernetesTokenAuthFilter implements ClientRequestFilter {
+ private final Supplier tokenSupplier;
+
+ public KubernetesTokenAuthFilter(Supplier tokenSupplier) {
+ this.tokenSupplier = tokenSupplier;
+ }
+
+ @Override
+ public void filter(ClientRequestContext clientRequestContext) throws IOException {
+ clientRequestContext.getHeaders().add(HttpHeaders.AUTHORIZATION, "Bearer " + tokenSupplier.get());
+ }
+}
diff --git a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/security/validators/NamespaceValidationRequestFilter.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/security/validators/NamespaceValidationRequestFilter.java
new file mode 100644
index 00000000..b011384d
--- /dev/null
+++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/security/validators/NamespaceValidationRequestFilter.java
@@ -0,0 +1,36 @@
+package com.netcracker.cloud.dbaas.security.validators;
+
+import com.netcracker.cloud.core.error.runtime.ErrorCodeException;
+import com.netcracker.cloud.dbaas.DbaasApiPath;
+import com.netcracker.cloud.dbaas.controller.error.Utils;
+import com.netcracker.cloud.dbaas.exceptions.ErrorCodes;
+import com.netcracker.cloud.dbaas.exceptions.FailedNamespaceIsolationCheckException;
+import com.netcracker.cloud.dbaas.utils.JwtUtils;
+import io.smallrye.jwt.auth.principal.JWTCallerPrincipal;
+import jakarta.inject.Inject;
+import jakarta.ws.rs.container.ContainerRequestContext;
+import jakarta.ws.rs.container.ContainerRequestFilter;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.ext.Provider;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.IOException;
+
+@Provider
+@Slf4j
+public class NamespaceValidationRequestFilter implements ContainerRequestFilter {
+ @Inject
+ NamespaceValidator namespaceValidator;
+
+ @Override
+ public void filter(ContainerRequestContext requestContext) throws IOException {
+ String namespaceFromPath = requestContext.getUriInfo().getPathParameters().getFirst(DbaasApiPath.NAMESPACE_PARAMETER);
+ // Don't check namespace if not present or not token auth
+ if (namespaceFromPath == null || !(requestContext.getSecurityContext().getUserPrincipal() instanceof JWTCallerPrincipal)) {
+ return;
+ }
+ if (!namespaceValidator.checkNamespaceIsolation(namespaceFromPath, JwtUtils.getNamespace(requestContext.getSecurityContext()))) {
+ throw new FailedNamespaceIsolationCheckException();
+ }
+ }
+}
diff --git a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/security/validators/NamespaceValidator.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/security/validators/NamespaceValidator.java
new file mode 100644
index 00000000..a2f57a23
--- /dev/null
+++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/security/validators/NamespaceValidator.java
@@ -0,0 +1,68 @@
+package com.netcracker.cloud.dbaas.security.validators;
+
+import com.netcracker.cloud.dbaas.entity.pg.composite.CompositeStructure;
+import com.netcracker.cloud.dbaas.service.composite.CompositeNamespaceService;
+import com.netcracker.cloud.dbaas.utils.JwtUtils;
+import io.smallrye.jwt.auth.principal.JWTCallerPrincipal;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+import jakarta.ws.rs.core.SecurityContext;
+import lombok.extern.slf4j.Slf4j;
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+
+import java.util.Map;
+import java.util.Optional;
+
+@Slf4j
+@ApplicationScoped
+public class NamespaceValidator {
+ @ConfigProperty(name = "dbaas.security.namespace-isolation-enabled")
+ boolean namespaceIsolationEnabled;
+
+ @Inject
+ CompositeNamespaceService compositeNamespaceService;
+
+ public boolean checkNamespaceIsolation(String namespaceFromPath, String namespaceFromJwt) {
+ if (!namespaceIsolationEnabled) {
+ return true;
+ }
+ return checkNamespacesEqual(namespaceFromPath, namespaceFromJwt);
+ }
+
+ public boolean checkNamespaceFromClassifier(SecurityContext securityContext, Map classifier) {
+ if (!(securityContext.getUserPrincipal() instanceof JWTCallerPrincipal)) {
+ return true;
+ }
+ String namespaceFromClassifier = (String) classifier.get("namespace");
+ if (namespaceFromClassifier == null) {
+ return false;
+ }
+ return checkNamespacesEqual(namespaceFromClassifier, JwtUtils.getNamespace(securityContext));
+ }
+
+ private boolean checkNamespacesEqual(String namespace0, String namespace1) {
+ if (namespace0.equals(namespace1)) {
+ return true;
+ } else {
+ return inSameCompositeStructure(namespace0, namespace1);
+ }
+ }
+
+ private boolean inSameCompositeStructure(String namespace0, String namespace1) {
+ Optional baseLine = compositeNamespaceService.getBaselineByNamespace(namespace0);
+
+ if (baseLine.isEmpty()) {
+ return false;
+ }
+
+ if (baseLine.get().equals(namespace1)) {
+ return true;
+ }
+
+ Optional compositeStructure = compositeNamespaceService.getCompositeStructure(baseLine.get());
+
+ return compositeStructure.map(structure -> structure.getNamespaces().contains(namespace1))
+ .orElse(false);
+
+ }
+}
diff --git a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/service/DbaasAdapterRESTClientFactory.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/service/DbaasAdapterRESTClientFactory.java
index 81d8516b..f32b3e47 100644
--- a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/service/DbaasAdapterRESTClientFactory.java
+++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/service/DbaasAdapterRESTClientFactory.java
@@ -2,13 +2,19 @@
import com.netcracker.cloud.dbaas.dto.v3.ApiVersion;
import com.netcracker.cloud.dbaas.monitoring.interceptor.TimeMeasurementManager;
-import com.netcracker.cloud.dbaas.rest.BasicAuthFilter;
+import com.netcracker.cloud.dbaas.rest.SecureDbaasAdapterRestClientV2;
+import com.netcracker.cloud.dbaas.security.filters.BasicAuthFilter;
import com.netcracker.cloud.dbaas.rest.DbaasAdapterRestClient;
import com.netcracker.cloud.dbaas.rest.DbaasAdapterRestClientLoggingFilter;
import com.netcracker.cloud.dbaas.rest.DbaasAdapterRestClientV2;
+import com.netcracker.cloud.dbaas.security.filters.DynamicAuthFilter;
+import com.netcracker.cloud.dbaas.security.filters.KubernetesTokenAuthFilter;
+import com.netcracker.cloud.security.core.utils.k8s.KubernetesServiceAccountToken;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
+import jakarta.ws.rs.Priorities;
+import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.eclipse.microprofile.rest.client.RestClientBuilder;
import java.lang.reflect.Proxy;
@@ -17,6 +23,10 @@
@ApplicationScoped
public class DbaasAdapterRESTClientFactory {
+ @Inject
+ @ConfigProperty(name = "dbaas.security.k8s.jwt.enabled")
+ boolean jwtEnabled;
+
@Inject
TimeMeasurementManager timeMeasurementManager;
@@ -34,15 +44,24 @@ public DbaasAdapter createDbaasAdapterClient(String username, String password, S
public DbaasAdapter createDbaasAdapterClientV2(String username, String password, String adapterAddress, String type,
String identifier, AdapterActionTrackerClient tracker, ApiVersion apiVersions) {
- BasicAuthFilter authFilter = new BasicAuthFilter(username, password);
+ BasicAuthFilter basicAuthFilter = new BasicAuthFilter(username, password);
+ KubernetesTokenAuthFilter kubernetesTokenAuthFilter = null;
+ if (jwtEnabled) {
+ kubernetesTokenAuthFilter = new KubernetesTokenAuthFilter(KubernetesServiceAccountToken::getToken);
+ }
+ DynamicAuthFilter dynamicAuthFilter = new DynamicAuthFilter(kubernetesTokenAuthFilter != null ? kubernetesTokenAuthFilter : basicAuthFilter);
+
DbaasAdapterRestClientV2 restClient = RestClientBuilder.newBuilder().baseUri(URI.create(adapterAddress))
- .register(authFilter)
+ .register(dynamicAuthFilter, Priorities.AUTHENTICATION)
.register(new DbaasAdapterRestClientLoggingFilter())
.connectTimeout(3, TimeUnit.MINUTES)
.readTimeout(3, TimeUnit.MINUTES)
.build(DbaasAdapterRestClientV2.class);
+
+ SecureDbaasAdapterRestClientV2 secureRestClient = new SecureDbaasAdapterRestClientV2(restClient, basicAuthFilter, kubernetesTokenAuthFilter, dynamicAuthFilter, jwtEnabled);
+
return (DbaasAdapter) Proxy.newProxyInstance(DbaasAdapter.class.getClassLoader(), new Class[]{DbaasAdapter.class},
- timeMeasurementManager.provideTimeMeasurementInvocationHandler(new DbaasAdapterRESTClientV2(adapterAddress, type, restClient, identifier, tracker, apiVersions)));
+ timeMeasurementManager.provideTimeMeasurementInvocationHandler(new DbaasAdapterRESTClientV2(adapterAddress, type, secureRestClient, identifier, tracker, apiVersions)));
}
}
diff --git a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/utils/JwtUtils.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/utils/JwtUtils.java
new file mode 100644
index 00000000..cfbd6b1c
--- /dev/null
+++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/utils/JwtUtils.java
@@ -0,0 +1,27 @@
+package com.netcracker.cloud.dbaas.utils;
+
+import io.smallrye.jwt.auth.principal.DefaultJWTCallerPrincipal;
+import jakarta.json.JsonObject;
+import jakarta.json.JsonString;
+import jakarta.ws.rs.core.SecurityContext;
+import org.eclipse.microprofile.jwt.JsonWebToken;
+
+import java.util.Map;
+
+public class JwtUtils {
+ public static String getServiceAccountName(JsonWebToken token) {
+ Map kubernetesClaims = token.getClaim("kubernetes.io");
+ JsonObject serviceAccount = (JsonObject) kubernetesClaims.get("serviceaccount");
+ JsonString serviceAccountName = (JsonString) serviceAccount.get("name");
+ return serviceAccountName.getString();
+ }
+
+ public static String getNamespace(SecurityContext securityContext) {
+ if (securityContext.getUserPrincipal() instanceof DefaultJWTCallerPrincipal principal) {
+ Map kubernetesClaims = principal.getClaim("kubernetes.io");
+ JsonString namespaceFromJwt = (JsonString) kubernetesClaims.get("namespace");
+ return namespaceFromJwt.getString();
+ }
+ return "";
+ }
+}
diff --git a/dbaas/dbaas-aggregator/src/main/resources/application.properties b/dbaas/dbaas-aggregator/src/main/resources/application.properties
index 783ff065..fc33fed1 100644
--- a/dbaas/dbaas-aggregator/src/main/resources/application.properties
+++ b/dbaas/dbaas-aggregator/src/main/resources/application.properties
@@ -86,6 +86,14 @@ quarkus.http.auth.permission.logging-manager-update.paths=/q/logging-manager
quarkus.http.auth.permission.logging-manager-update.methods=POST
quarkus.http.auth.permission.logging-manager-update.policy=db-client-access
+# K8s projected volume tokens Authentication Configuration
+quarkus.smallrye-jwt.enabled=true
+
+dbaas.security.namespace-isolation-enabled=${DBAAS_SECURITY_NAMESPACE_ISOLATION_ENABLED:true}
+dbaas.security.k8s.service.account.roles.path=${DBAAS_SECURITY_CONFIGURATION_LOCATION:/etc/dbaas/security}/service-account-roles.yaml
+dbaas.security.k8s.jwt.enabled=${KUBERNETES_JWT_ENABLED:true}
+dbaas.security.k8s.jwt.audience=${KUBERNETES_JWT_AUDIENCE:dbaas}
+
# Open-API and Swagger
quarkus.swagger-ui.always-include=true
quarkus.swagger-ui.path=/swagger-ui
diff --git a/dbaas/dbaas-aggregator/src/test/java/com/netcracker/cloud/dbaas/config/security/BasicAndKubernetesAuthMechanismTest.java b/dbaas/dbaas-aggregator/src/test/java/com/netcracker/cloud/dbaas/config/security/BasicAndKubernetesAuthMechanismTest.java
new file mode 100644
index 00000000..84f35947
--- /dev/null
+++ b/dbaas/dbaas-aggregator/src/test/java/com/netcracker/cloud/dbaas/config/security/BasicAndKubernetesAuthMechanismTest.java
@@ -0,0 +1,156 @@
+package com.netcracker.cloud.dbaas.config.security;
+
+import io.quarkus.security.identity.IdentityProviderManager;
+import io.quarkus.security.identity.SecurityIdentity;
+import io.quarkus.security.identity.request.AuthenticationRequest;
+import io.quarkus.smallrye.jwt.runtime.auth.JWTAuthMechanism;
+import io.quarkus.vertx.http.runtime.security.BasicAuthenticationMechanism;
+import io.quarkus.vertx.http.runtime.security.ChallengeData;
+import io.quarkus.vertx.http.runtime.security.HttpCredentialTransport;
+import io.smallrye.mutiny.Uni;
+import io.vertx.core.http.HttpServerRequest;
+import io.vertx.ext.web.RoutingContext;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+class BasicAndKubernetesAuthMechanismTest {
+
+ private BasicAndKubernetesAuthMechanism mechanism;
+ private BasicAuthenticationMechanism basicAuth;
+ private JWTAuthMechanism jwtAuth;
+ private RoutingContext context;
+ private HttpServerRequest request;
+ private IdentityProviderManager idManager;
+
+ @BeforeEach
+ void setUp() {
+ mechanism = new BasicAndKubernetesAuthMechanism();
+ basicAuth = mock(BasicAuthenticationMechanism.class);
+ jwtAuth = mock(JWTAuthMechanism.class);
+ mechanism.basicAuth = basicAuth;
+ mechanism.jwtAuth = jwtAuth;
+
+ context = mock(RoutingContext.class);
+ request = mock(HttpServerRequest.class);
+ when(context.request()).thenReturn(request);
+ idManager = mock(IdentityProviderManager.class);
+ }
+
+ @Test
+ void testAuthenticate_withBearerToken() {
+ when(request.getHeader("Authorization")).thenReturn("Bearer token");
+ Uni expected = Uni.createFrom().nullItem();
+ when(jwtAuth.authenticate(any(), any())).thenReturn(expected);
+
+ Uni result = mechanism.authenticate(context, idManager);
+
+ verify(jwtAuth, times(1)).authenticate(any(), any());
+ verify(basicAuth, never()).authenticate(any(), any());
+ assertEquals(expected, result);
+ }
+
+ @Test
+ void testAuthenticate_withBasicToken() {
+ when(request.getHeader("Authorization")).thenReturn("Basic abc");
+ Uni expected = Uni.createFrom().nullItem();
+ when(basicAuth.authenticate(any(), any())).thenReturn(expected);
+
+ Uni result = mechanism.authenticate(context, idManager);
+
+ verify(basicAuth, times(1)).authenticate(any(), any());
+ verify(jwtAuth, never()).authenticate(any(), any());
+ assertEquals(expected, result);
+ }
+
+ @Test
+ void testGetChallenge_withBearerToken() {
+ when(request.getHeader("Authorization")).thenReturn("Bearer token");
+ Uni expected = Uni.createFrom().nullItem();
+ when(jwtAuth.getChallenge(any())).thenReturn(expected);
+
+ Uni result = mechanism.getChallenge(context);
+
+ verify(jwtAuth, times(1)).getChallenge(any());
+ assertEquals(expected, result);
+ }
+
+ @Test
+ void testGetChallenge_withBasicAuth() {
+ when(request.getHeader("Authorization")).thenReturn("Basic abc");
+ Uni expected = Uni.createFrom().nullItem();
+ when(basicAuth.getChallenge(any())).thenReturn(expected);
+
+ Uni result = mechanism.getChallenge(context);
+
+ verify(basicAuth, times(1)).getChallenge(any());
+ assertEquals(expected, result);
+ }
+
+ @Test
+ void testGetCredentialTypes() {
+ Set> basicTypes =
+ new HashSet<>(Collections.singleton(AuthenticationRequest.class));
+ Set> jwtTypes =
+ new HashSet<>(Collections.singleton(AuthenticationRequest.class));
+
+ when(basicAuth.getCredentialTypes()).thenReturn(basicTypes);
+ when(jwtAuth.getCredentialTypes()).thenReturn(jwtTypes);
+
+ Set> result = mechanism.getCredentialTypes();
+
+ assertNotNull(result);
+ assertTrue(result.containsAll(basicAuth.getCredentialTypes()));
+ assertTrue(result.containsAll(jwtAuth.getCredentialTypes()));
+
+ verify(basicAuth, times(2)).getCredentialTypes();
+ verify(jwtAuth, times(2)).getCredentialTypes();
+ }
+
+ @Test
+ void testGetCredentialTransport_withBearerToken() {
+ when(request.getHeader("Authorization")).thenReturn("Bearer token");
+ Uni expected = Uni.createFrom().nullItem();
+ when(jwtAuth.getCredentialTransport(any())).thenReturn(expected);
+
+ Uni result = mechanism.getCredentialTransport(context);
+
+ verify(jwtAuth, times(1)).getCredentialTransport(any());
+ assertEquals(expected, result);
+ }
+
+ @Test
+ void testGetCredentialTransport_withBasicAuth() {
+ when(request.getHeader("Authorization")).thenReturn("Basic abc");
+ Uni expected = Uni.createFrom().nullItem();
+ when(basicAuth.getCredentialTransport(any())).thenReturn(expected);
+
+ Uni result = mechanism.getCredentialTransport(context);
+
+ verify(basicAuth, times(1)).getCredentialTransport(any());
+ assertEquals(expected, result);
+ }
+
+ @Test
+ void testIsBearerTokenPresent_privateMethodCoverage() throws Exception {
+ // Reflectively test private method
+ var method = BasicAndKubernetesAuthMechanism.class.getDeclaredMethod("isBearerTokenPresent", RoutingContext.class);
+ method.setAccessible(true);
+
+ when(request.getHeader("Authorization")).thenReturn("Bearer something");
+ assertTrue((Boolean) method.invoke(mechanism, context));
+
+ when(request.getHeader("Authorization")).thenReturn("Basic something");
+ assertFalse((Boolean) method.invoke(mechanism, context));
+
+ when(request.getHeader("Authorization")).thenReturn(null);
+ assertFalse((Boolean) method.invoke(mechanism, context));
+ }
+}
diff --git a/dbaas/dbaas-aggregator/src/test/java/com/netcracker/cloud/dbaas/config/security/KubernetesJWTCallerPrincipalFactoryTest.java b/dbaas/dbaas-aggregator/src/test/java/com/netcracker/cloud/dbaas/config/security/KubernetesJWTCallerPrincipalFactoryTest.java
new file mode 100644
index 00000000..b16110aa
--- /dev/null
+++ b/dbaas/dbaas-aggregator/src/test/java/com/netcracker/cloud/dbaas/config/security/KubernetesJWTCallerPrincipalFactoryTest.java
@@ -0,0 +1,29 @@
+package com.netcracker.cloud.dbaas.config.security;
+
+import com.netcracker.cloud.security.core.utils.k8s.KubernetesTokenVerificationException;
+import com.netcracker.cloud.security.core.utils.k8s.KubernetesTokenVerifier;
+import io.smallrye.jwt.auth.principal.ParseException;
+import org.jose4j.jwt.JwtClaims;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+@ExtendWith(MockitoExtension.class)
+class KubernetesJWTCallerPrincipalFactoryTest {
+ @Mock
+ KubernetesTokenVerifier verifier;
+ KubernetesJWTCallerPrincipalFactory factory;
+
+ @Test
+ void parse() throws ParseException, KubernetesTokenVerificationException {
+ Mockito.when(verifier.verify("token")).thenReturn(new JwtClaims());
+ Mockito.when(verifier.verify("invalidToken")).thenThrow(new KubernetesTokenVerificationException("invalid token"));
+ factory = new KubernetesJWTCallerPrincipalFactory(verifier);
+ assertNotNull(factory.parse("token", null));
+ assertThrows(ParseException.class, () -> factory.parse("invalidToken", null));
+ }
+}
diff --git a/dbaas/dbaas-aggregator/src/test/java/com/netcracker/cloud/dbaas/controller/v3/AggregatedDatabaseAdministrationControllerV3Test.java b/dbaas/dbaas-aggregator/src/test/java/com/netcracker/cloud/dbaas/controller/v3/AggregatedDatabaseAdministrationControllerV3Test.java
index 878f5b60..99f24052 100644
--- a/dbaas/dbaas-aggregator/src/test/java/com/netcracker/cloud/dbaas/controller/v3/AggregatedDatabaseAdministrationControllerV3Test.java
+++ b/dbaas/dbaas-aggregator/src/test/java/com/netcracker/cloud/dbaas/controller/v3/AggregatedDatabaseAdministrationControllerV3Test.java
@@ -3,6 +3,7 @@
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.netcracker.cloud.dbaas.dto.ClassifierWithRolesRequest;
+import com.netcracker.cloud.dbaas.dto.role.Role;
import com.netcracker.cloud.dbaas.dto.v3.*;
import com.netcracker.cloud.dbaas.entity.pg.*;
import com.netcracker.cloud.dbaas.exceptions.ErrorCodes;
@@ -39,6 +40,11 @@
import static com.netcracker.cloud.dbaas.DatabaseType.POSTGRESQL;
import static com.netcracker.cloud.dbaas.DbaasApiPath.*;
import static com.netcracker.cloud.dbaas.dto.role.Role.ADMIN;
+import static com.netcracker.cloud.dbaas.Constants.*;
+import static com.netcracker.cloud.dbaas.DbaasApiPath.ASYNC_PARAMETER;
+import static com.netcracker.cloud.dbaas.DbaasApiPath.LIST_DATABASES_PATH;
+import static com.netcracker.cloud.dbaas.DbaasApiPath.NAMESPACE_PARAMETER;
+import static com.netcracker.cloud.framework.contexts.tenant.TenantContextObject.TENANT_HEADER;
import static io.restassured.RestAssured.given;
import static jakarta.ws.rs.core.Response.Status.*;
import static java.util.Collections.singletonList;
@@ -500,6 +506,53 @@ void testClassifierWithTenantScopeToCreateDatabase() throws JsonProcessingExcept
.statusCode(BAD_REQUEST.getStatusCode());
}
+ @Test
+ void testClassifierWithTenantIdToCreateDatabase() throws JsonProcessingException {
+ when(dBaaService.getConnectionPropertiesService()).thenReturn(processConnectionPropertiesService);
+ final DatabaseCreateRequestV3 databaseCreateRequest = getDatabaseCreateRequestSample();
+ when(declarativeConfigRepository.findFirstByClassifierAndType(any(), any())).thenReturn(Optional.empty());
+ Mockito.when(databaseRolesService.getSupportedRoleFromRequest(any(DatabaseCreateRequestV3.class), any(), any())).thenReturn(Role.ADMIN.toString());
+ when(databaseRegistryDbaasRepository.saveAnyTypeLogDb(any(DatabaseRegistry.class))).thenThrow(new ConstraintViolationException("constraint violation", new PSQLException("constraint violation", PSQLState.UNIQUE_VIOLATION), "database_registry_classifier_and_type_index"));
+ when(databaseRegistryDbaasRepository.getDatabaseByClassifierAndType(anyMap(), anyString())).thenReturn(Optional.of(Mockito.mock(DatabaseRegistry.class)));
+
+ final DatabaseRegistry database = getDatabaseSample();
+ when(dBaaService.findDatabaseByClassifierAndType(any(), any(), anyBoolean())).thenReturn(database.getDatabaseRegistry().get(0));
+
+ String tenantId = UUID.randomUUID().toString();
+ databaseCreateRequest.getClassifier().put("scope", "tenant");
+ databaseCreateRequest.getClassifier().put(TENANT_ID, tenantId);
+
+ when(dBaaService.detach(database)).thenReturn(database);
+ when(dBaaService.isModifiedFields(any(), any())).thenReturn(false);
+ DatabaseResponseV3 response = new DatabaseResponseV3SingleCP(database.getDatabaseRegistry().get(0), PHYSICAL_DATABASE_ID, Role.ADMIN.toString());
+ when(dBaaService.processConnectionPropertiesV3(any(DatabaseRegistry.class), any())).thenReturn(response);
+
+ given().auth().preemptive().basic("cluster-dba", "someDefaultPassword")
+ .pathParam(NAMESPACE_PARAMETER, TEST_NAMESPACE)
+ .contentType(MediaType.APPLICATION_JSON)
+ .header(TENANT_HEADER, tenantId)
+ .body(objectMapper.writeValueAsString(databaseCreateRequest))
+ .when().put()
+ .then()
+ .statusCode(OK.getStatusCode());
+ }
+
+ @Test
+ void testClassifierWithInvalidTenantIdToCreateDatabase() throws JsonProcessingException {
+ Map classifier = getSampleClassifier();
+ classifier.put("scope", "tenant");
+ classifier.put(TENANT_ID, UUID.randomUUID().toString());
+ DatabaseCreateRequestV3 databaseCreateRequest = getDatabaseCreateRequestSample(classifier);
+ given().auth().preemptive().basic("cluster-dba", "someDefaultPassword")
+ .pathParam(NAMESPACE_PARAMETER, TEST_NAMESPACE)
+ .contentType(MediaType.APPLICATION_JSON)
+ .header(TENANT_HEADER, UUID.randomUUID().toString())
+ .body(objectMapper.writeValueAsString(databaseCreateRequest))
+ .when().put()
+ .then()
+ .statusCode(FORBIDDEN.getStatusCode());
+ }
+
@Test
void testClassifierWithWrongScopeToCreateDatabase() throws JsonProcessingException {
Map classifier = getSampleClassifier();
diff --git a/dbaas/dbaas-aggregator/src/test/java/com/netcracker/cloud/dbaas/integration/SecurityTest.java b/dbaas/dbaas-aggregator/src/test/java/com/netcracker/cloud/dbaas/integration/SecurityTest.java
index f28848e9..2238e749 100644
--- a/dbaas/dbaas-aggregator/src/test/java/com/netcracker/cloud/dbaas/integration/SecurityTest.java
+++ b/dbaas/dbaas-aggregator/src/test/java/com/netcracker/cloud/dbaas/integration/SecurityTest.java
@@ -13,20 +13,29 @@
import com.netcracker.cloud.dbaas.entity.pg.DatabaseRegistry;
import com.netcracker.cloud.dbaas.entity.pg.ExternalAdapterRegistrationEntry;
import com.netcracker.cloud.dbaas.entity.pg.PhysicalDatabase;
+import com.netcracker.cloud.dbaas.integration.config.JwtUtilsTestResource;
import com.netcracker.cloud.dbaas.integration.config.PostgresqlContainerResource;
+import com.netcracker.cloud.dbaas.integration.config.SecurityTestProfile;
+import com.netcracker.cloud.dbaas.integration.utils.TestJwtUtils;
import com.netcracker.cloud.dbaas.repositories.dbaas.DatabaseDbaasRepository;
import com.netcracker.cloud.dbaas.repositories.dbaas.DatabaseRegistryDbaasRepository;
import com.netcracker.cloud.dbaas.repositories.pg.jpa.BgNamespaceRepository;
import com.netcracker.cloud.dbaas.repositories.pg.jpa.DatabaseDeclarativeConfigRepository;
import com.netcracker.cloud.dbaas.service.*;
+import com.netcracker.cloud.dbaas.utils.JwtUtils;
+import com.netcracker.cloud.security.core.utils.k8s.KubernetesServiceAccountToken;
import io.quarkus.test.InjectMock;
import io.quarkus.test.common.QuarkusTestResource;
import io.quarkus.test.junit.QuarkusTest;
+import io.quarkus.test.junit.TestProfile;
import io.quarkus.test.junit.mockito.MockitoConfig;
+import io.quarkus.test.oidc.server.OidcWiremockTestResource;
import io.restassured.response.ValidatableResponse;
import jakarta.inject.Inject;
import jakarta.ws.rs.core.MediaType;
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -49,6 +58,8 @@
@QuarkusTest
@QuarkusTestResource(PostgresqlContainerResource.class)
+@QuarkusTestResource(JwtUtilsTestResource.class)
+@TestProfile(SecurityTestProfile.class)
class SecurityTest {
private static final String TEST_TYPE = "mongodbtest";
@@ -77,8 +88,12 @@ class SecurityTest {
@Inject
PasswordEncryption passwordEncryption;
+ TestJwtUtils jwtUtils;
+
@BeforeEach
void prepareMock() {
+ jwtUtils = JwtUtilsTestResource.JWT_UTILS;
+
testDbaasAdapter = mock(DbaasAdapter.class);
when(dbaasAdapterRESTClientFactory.createDbaasAdapterClientV2(any(), any(), any(), any(), any(), any(), any())).thenReturn(testDbaasAdapter);
CreatedDatabaseV3 testDatabase = new CreatedDatabaseV3();
@@ -245,6 +260,18 @@ void testDiscrToolUserCanAccessGetByClassifier() throws JsonProcessingException
getByClassifier("discr-tool-user", "someDefaultPassword", classifier).statusCode(OK.getStatusCode());
}
+ @Test
+ void testCreateDatabaseWithKubernetesToken() {
+ createDatabaseWithKubernetesToken(jwtUtils.getJwt("test-name", "unit-test-namespace"))
+ .statusCode(CREATED.getStatusCode());
+ }
+
+ @Test
+ void testCreateDatabaseWithInvalidKubernetesToken() {
+ createDatabaseWithKubernetesToken(jwtUtils.getJwt("test-name", "unit-test-namespace")+"pad-to-make-invalid-signature")
+ .statusCode(UNAUTHORIZED.getStatusCode());
+ }
+
private ValidatableResponse updateClassifier(String user, String password, UpdateClassifierRequestV3 updateClassifierRequest, String namespace, String type) throws JsonProcessingException {
return given().auth().preemptive().basic(user, password)
.contentType(MediaType.APPLICATION_JSON)
@@ -272,6 +299,15 @@ private ValidatableResponse createDatabase(String user, String password) {
.then();
}
+ private ValidatableResponse createDatabaseWithKubernetesToken(String token) {
+ return given().auth().preemptive().oauth2(token)
+ .contentType(MediaType.APPLICATION_JSON)
+ .body("{\"type\":\"" + TEST_TYPE + "\", \"classifier\":{\"scope\":\"service\", \"microserviceName\":\"test-name\", \"namespace\":\"unit-test-namespace\"}, \"originService\":\"test-name\"}")
+ .accept(MediaType.APPLICATION_JSON)
+ .put(DBAAS_PATH_V3 + "/unit-test-namespace/databases")
+ .then();
+ }
+
private ValidatableResponse cleanNamespace(String user, String password) {
return given().auth().preemptive().basic(user, password)
diff --git a/dbaas/dbaas-aggregator/src/test/java/com/netcracker/cloud/dbaas/integration/config/JwtUtilsTestResource.java b/dbaas/dbaas-aggregator/src/test/java/com/netcracker/cloud/dbaas/integration/config/JwtUtilsTestResource.java
new file mode 100644
index 00000000..09df3630
--- /dev/null
+++ b/dbaas/dbaas-aggregator/src/test/java/com/netcracker/cloud/dbaas/integration/config/JwtUtilsTestResource.java
@@ -0,0 +1,31 @@
+package com.netcracker.cloud.dbaas.integration.config;
+
+import com.netcracker.cloud.dbaas.integration.utils.TestJwtUtils;
+import io.quarkus.test.common.QuarkusTestResourceLifecycleManager;
+import io.quarkus.test.oidc.server.OidcWiremockTestResource;
+import org.eclipse.microprofile.config.ConfigProvider;
+
+import java.time.Duration;
+import java.util.Map;
+
+public class JwtUtilsTestResource implements QuarkusTestResourceLifecycleManager {
+ public static final String tokenDir = "./src/test/resources/mock-oidc-token";
+ public static TestJwtUtils JWT_UTILS;
+ private final OidcWiremockTestResource oidcWiremock = new OidcWiremockTestResource();
+
+ @Override
+ public Map start() {
+ var conf = oidcWiremock.start();
+ String issuer = conf.get("keycloak.url")+"/realms/quarkus";
+ String audience = ConfigProvider.getConfig()
+ .getValue("dbaas.security.k8s.jwt.audience", String.class);
+ System.setProperty("smallrye.jwt.sign.key.location", conf.get("smallrye.jwt.sign.key.location"));
+ JWT_UTILS = new TestJwtUtils(issuer, audience, tokenDir);
+ return conf;
+ }
+
+ @Override
+ public void stop() {
+ JWT_UTILS = null;
+ }
+}
diff --git a/dbaas/dbaas-aggregator/src/test/java/com/netcracker/cloud/dbaas/integration/config/SecurityTestProfile.java b/dbaas/dbaas-aggregator/src/test/java/com/netcracker/cloud/dbaas/integration/config/SecurityTestProfile.java
new file mode 100644
index 00000000..127e6119
--- /dev/null
+++ b/dbaas/dbaas-aggregator/src/test/java/com/netcracker/cloud/dbaas/integration/config/SecurityTestProfile.java
@@ -0,0 +1,15 @@
+package com.netcracker.cloud.dbaas.integration.config;
+
+import io.quarkus.test.junit.QuarkusTestProfile;
+
+import java.util.Map;
+
+public class SecurityTestProfile implements QuarkusTestProfile {
+ @Override
+ public Map getConfigOverrides() {
+ return Map.of(
+ "dbaas.security.k8s.jwt.enabled", "true",
+ "com.netcracker.cloud.security.kubernetes.service.account.token.dir", JwtUtilsTestResource.tokenDir
+ );
+ }
+}
diff --git a/dbaas/dbaas-aggregator/src/test/java/com/netcracker/cloud/dbaas/integration/utils/TestJwtUtils.java b/dbaas/dbaas-aggregator/src/test/java/com/netcracker/cloud/dbaas/integration/utils/TestJwtUtils.java
new file mode 100644
index 00000000..5c606d62
--- /dev/null
+++ b/dbaas/dbaas-aggregator/src/test/java/com/netcracker/cloud/dbaas/integration/utils/TestJwtUtils.java
@@ -0,0 +1,46 @@
+package com.netcracker.cloud.dbaas.integration.utils;
+
+import io.smallrye.jwt.build.Jwt;
+import jakarta.enterprise.context.ApplicationScoped;
+import lombok.SneakyThrows;
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Map;
+
+public class TestJwtUtils {
+ private final String issuer;
+ private final String audience;
+
+ @SneakyThrows
+ public TestJwtUtils(String issuer, String audience, String tokenDir) {
+ this.issuer = issuer;
+ this.audience = audience;
+
+ Files.createDirectories(Path.of(tokenDir));
+ Files.writeString(
+ Path.of(tokenDir).resolve("token"),
+ getJwt("default", "test-namespace"),
+ StandardOpenOption.CREATE,
+ StandardOpenOption.TRUNCATE_EXISTING
+ );
+ }
+
+ public String getJwt(String serviceAccountName, String namespace) {
+ return Jwt.subject("some-service")
+ .issuer(issuer)
+ .audience(audience)
+ .claim("jti", java.util.UUID.randomUUID().toString())
+ .claim("kubernetes.io", Map.of(
+ "namespace", namespace,
+ "serviceaccount", Map.of("name", serviceAccountName)
+ ))
+ .expiresIn(Duration.ofDays(1))
+ .issuedAt(Instant.now())
+ .sign();
+ }
+}
diff --git a/dbaas/dbaas-aggregator/src/test/java/com/netcracker/cloud/dbaas/rest/SecureDbaasAdapterRestClientV2Test.java b/dbaas/dbaas-aggregator/src/test/java/com/netcracker/cloud/dbaas/rest/SecureDbaasAdapterRestClientV2Test.java
new file mode 100644
index 00000000..2df74a53
--- /dev/null
+++ b/dbaas/dbaas-aggregator/src/test/java/com/netcracker/cloud/dbaas/rest/SecureDbaasAdapterRestClientV2Test.java
@@ -0,0 +1,123 @@
+package com.netcracker.cloud.dbaas.rest;
+
+import com.netcracker.cloud.dbaas.monitoring.AdapterHealthStatus;
+import com.netcracker.cloud.dbaas.security.filters.AuthFilterSelector;
+import com.netcracker.cloud.dbaas.security.filters.BasicAuthFilter;
+import com.netcracker.cloud.dbaas.security.filters.KubernetesTokenAuthFilter;
+import jakarta.ws.rs.WebApplicationException;
+import jakarta.ws.rs.core.Response;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.*;
+
+@ExtendWith(MockitoExtension.class)
+class SecureDbaasAdapterRestClientV2Test {
+
+ @Mock
+ private DbaasAdapterRestClientV2 restClient;
+
+ @Mock
+ private BasicAuthFilter basicAuthFilter;
+
+ @Mock
+ private KubernetesTokenAuthFilter kubernetesTokenAuthFilter;
+
+ @Mock
+ private AuthFilterSelector authFilterSelector;
+
+ @Test
+ void shouldExecuteRequestWithBasicAuthWhenJwtDisabled() {
+ SecureDbaasAdapterRestClientV2 secureClient = new SecureDbaasAdapterRestClientV2(
+ restClient, basicAuthFilter, kubernetesTokenAuthFilter, authFilterSelector, false);
+ AdapterHealthStatus healthStatus = new AdapterHealthStatus("ok");
+ when(restClient.getHealth()).thenReturn(healthStatus);
+
+ AdapterHealthStatus result = secureClient.getHealth();
+
+ assertSame(healthStatus, result);
+ verify(restClient).getHealth();
+ verify(authFilterSelector, never()).selectAuthFilter(any());
+ }
+
+ @Test
+ void shouldSwitchToBasicAuthOn401WhenUsingTokenAuth() {
+ when(authFilterSelector.getAuthFilter()).thenReturn(kubernetesTokenAuthFilter);
+ SecureDbaasAdapterRestClientV2 secureClient = new SecureDbaasAdapterRestClientV2(
+ restClient, basicAuthFilter, kubernetesTokenAuthFilter, authFilterSelector, true);
+
+ Response unauthorizedResponse = mock(Response.class);
+ when(unauthorizedResponse.getStatus()).thenReturn(401);
+ when(unauthorizedResponse.getStatusInfo()).thenReturn(getStatusType(401));
+ WebApplicationException unauthorizedException = new WebApplicationException(unauthorizedResponse);
+
+ AdapterHealthStatus healthStatus = new AdapterHealthStatus("ok");
+ when(restClient.getHealth())
+ .thenThrow(unauthorizedException)
+ .thenReturn(healthStatus);
+
+ AdapterHealthStatus result = secureClient.getHealth();
+
+ assertSame(healthStatus, result);
+ verify(authFilterSelector).selectAuthFilter(basicAuthFilter);
+ verify(restClient, times(2)).getHealth();
+ }
+
+ @Test
+ void shouldRethrowExceptionWhenNotUnauthorizedOrNotTokenAuth() {
+ when(authFilterSelector.getAuthFilter()).thenReturn(basicAuthFilter);
+ SecureDbaasAdapterRestClientV2 secureClient = new SecureDbaasAdapterRestClientV2(
+ restClient, basicAuthFilter, kubernetesTokenAuthFilter, authFilterSelector, true);
+
+ Response forbiddenResponse = mock(Response.class);
+ when(forbiddenResponse.getStatus()).thenReturn(403);
+ when(forbiddenResponse.getStatusInfo()).thenReturn(getStatusType(403));
+ WebApplicationException forbiddenException = new WebApplicationException(forbiddenResponse);
+ when(restClient.getHealth()).thenThrow(forbiddenException);
+
+ assertThrows(WebApplicationException.class, secureClient::getHealth);
+ verify(authFilterSelector, never()).selectAuthFilter(any());
+ verify(restClient, times(1)).getHealth();
+ }
+
+ @Test
+ void shouldDelegateAllMethodsToRestClient() throws Exception {
+ SecureDbaasAdapterRestClientV2 secureClient = new SecureDbaasAdapterRestClientV2(
+ restClient, basicAuthFilter, kubernetesTokenAuthFilter, authFilterSelector, false);
+
+ secureClient.handshake("postgres");
+ verify(restClient).handshake("postgres");
+
+ secureClient.supports("postgres");
+ verify(restClient).supports("postgres");
+
+ secureClient.getDatabases("postgres");
+ verify(restClient).getDatabases("postgres");
+
+ secureClient.close();
+ verify(restClient).close();
+ }
+
+ private Response.StatusType getStatusType(int code) {
+ return new Response.StatusType() {
+ @Override
+ public int getStatusCode() {
+ return 403;
+ }
+
+ @Override
+ public Response.Status.Family getFamily() {
+ return null;
+ }
+
+ @Override
+ public String getReasonPhrase() {
+ return "";
+ }
+ };
+ }
+}
diff --git a/dbaas/dbaas-aggregator/src/test/java/com/netcracker/cloud/dbaas/security/ServiceAccountRolesAugmentorTest.java b/dbaas/dbaas-aggregator/src/test/java/com/netcracker/cloud/dbaas/security/ServiceAccountRolesAugmentorTest.java
new file mode 100644
index 00000000..633dded0
--- /dev/null
+++ b/dbaas/dbaas-aggregator/src/test/java/com/netcracker/cloud/dbaas/security/ServiceAccountRolesAugmentorTest.java
@@ -0,0 +1,63 @@
+package com.netcracker.cloud.dbaas.security;
+
+import com.netcracker.cloud.dbaas.Constants;
+import io.quarkus.security.identity.SecurityIdentity;
+import io.smallrye.mutiny.Uni;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.security.Principal;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class ServiceAccountRolesAugmentorTest {
+ @Mock
+ ServiceAccountRolesManager rolesManager;
+ @Mock
+ SecurityIdentity mockIdentity;
+ @Mock
+ Principal mockPrincipal;
+
+ @InjectMocks
+ ServiceAccountRolesAugmentor augmentor;
+
+ @Test
+ void augment() {
+ Map> serviceAccounts = new HashMap<>();
+
+ serviceAccounts.put(
+ "service-account-1",
+ Set.of("NAMESPACE_CLEANER", "DB_CLIENT", "MIGRATION_CLIENT")
+ );
+
+ serviceAccounts.put(
+ "service-account-2",
+ Set.of("NAMESPACE_CLEANER", "MIGRATION_CLIENT")
+ );
+
+ when(mockIdentity.getPrincipal()).thenReturn(mockPrincipal);
+
+ serviceAccounts.forEach((name, roles) -> {
+ when(rolesManager.getRolesByServiceAccountName(name)).thenReturn(roles);
+ when(mockPrincipal.getName()).thenReturn(name);
+
+ Uni identityUni = augmentor.augment(mockIdentity, null);
+ assertEquals(identityUni.await().indefinitely().getRoles(), roles);
+
+ when(mockPrincipal.getName()).thenReturn("someOtherName");
+ identityUni = augmentor.augment(mockIdentity, null);
+ assertEquals(identityUni.await().indefinitely().getRoles(), Set.of(Constants.DB_CLIENT));
+ });
+ }
+}
diff --git a/dbaas/dbaas-aggregator/src/test/java/com/netcracker/cloud/dbaas/security/ServiceAccountRolesManagerTest.java b/dbaas/dbaas-aggregator/src/test/java/com/netcracker/cloud/dbaas/security/ServiceAccountRolesManagerTest.java
new file mode 100644
index 00000000..4a324994
--- /dev/null
+++ b/dbaas/dbaas-aggregator/src/test/java/com/netcracker/cloud/dbaas/security/ServiceAccountRolesManagerTest.java
@@ -0,0 +1,27 @@
+package com.netcracker.cloud.dbaas.security;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+
+class ServiceAccountRolesManagerTest {
+ ServiceAccountRolesManager serviceAccountRolesManager;
+
+ @BeforeEach
+ void setUp() {
+ serviceAccountRolesManager = new ServiceAccountRolesManager();
+ serviceAccountRolesManager.onStart(null, "./src/test/resources/service-account-roles-secret.yaml");
+ }
+
+ @Test
+ void getRolesByServiceAccountName() {
+ Set roles0 = serviceAccountRolesManager.getRolesByServiceAccountName("service-account-1");
+ assertArrayEquals(new String[]{"NAMESPACE_CLEANER", "DB_CLIENT", "MIGRATION_CLIENT"}, roles0.toArray());
+
+ Set roles1 = serviceAccountRolesManager.getRolesByServiceAccountName("service-account-2");
+ assertArrayEquals(new String[]{"NAMESPACE_CLEANER", "MIGRATION_CLIENT"}, roles1.toArray());
+ }
+}
diff --git a/dbaas/dbaas-aggregator/src/test/java/com/netcracker/cloud/dbaas/security/filters/DynamicAuthFilterTest.java b/dbaas/dbaas-aggregator/src/test/java/com/netcracker/cloud/dbaas/security/filters/DynamicAuthFilterTest.java
new file mode 100644
index 00000000..303a157a
--- /dev/null
+++ b/dbaas/dbaas-aggregator/src/test/java/com/netcracker/cloud/dbaas/security/filters/DynamicAuthFilterTest.java
@@ -0,0 +1,49 @@
+package com.netcracker.cloud.dbaas.security.filters;
+
+import jakarta.ws.rs.client.ClientRequestContext;
+import jakarta.ws.rs.client.ClientRequestFilter;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.io.IOException;
+
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.mockito.Mockito.*;
+
+@ExtendWith(MockitoExtension.class)
+class DynamicAuthFilterTest {
+
+ @Mock
+ private ClientRequestFilter defaultFilter;
+
+ @Mock
+ private ClientRequestFilter newFilter;
+
+ @Mock
+ private ClientRequestContext requestContext;
+
+ @Test
+ void shouldConstructWithDefaultFilterAndAllowDynamicFilterSelection() throws IOException {
+ DynamicAuthFilter dynamicFilter = new DynamicAuthFilter(defaultFilter);
+
+ assertSame(defaultFilter, dynamicFilter.getAuthFilter());
+
+ dynamicFilter.filter(requestContext);
+
+ verify(defaultFilter).filter(requestContext);
+ verifyNoInteractions(newFilter);
+
+ dynamicFilter.selectAuthFilter(newFilter);
+
+ assertSame(newFilter, dynamicFilter.getAuthFilter());
+ assertNotSame(defaultFilter, dynamicFilter.getAuthFilter());
+
+ dynamicFilter.filter(requestContext);
+
+ verify(newFilter).filter(requestContext);
+ verify(defaultFilter, times(1)).filter(requestContext);
+ }
+}
diff --git a/dbaas/dbaas-aggregator/src/test/java/com/netcracker/cloud/dbaas/security/filters/KubernetesTokenAuthFilterTest.java b/dbaas/dbaas-aggregator/src/test/java/com/netcracker/cloud/dbaas/security/filters/KubernetesTokenAuthFilterTest.java
new file mode 100644
index 00000000..244ac265
--- /dev/null
+++ b/dbaas/dbaas-aggregator/src/test/java/com/netcracker/cloud/dbaas/security/filters/KubernetesTokenAuthFilterTest.java
@@ -0,0 +1,42 @@
+package com.netcracker.cloud.dbaas.security.filters;
+
+import jakarta.ws.rs.client.ClientRequestContext;
+import jakarta.ws.rs.core.HttpHeaders;
+import jakarta.ws.rs.core.MultivaluedHashMap;
+import jakarta.ws.rs.core.MultivaluedMap;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.io.IOException;
+import java.util.function.Supplier;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class KubernetesTokenAuthFilterTest {
+
+ @Mock
+ private ClientRequestContext requestContext;
+
+ @Test
+ void shouldAddBearerTokenToAuthorizationHeader() throws IOException {
+ String token = "test-k8s-token-12345";
+ Supplier tokenSupplier = () -> token;
+ MultivaluedMap headers = new MultivaluedHashMap<>();
+
+ when(requestContext.getHeaders()).thenReturn(headers);
+
+ KubernetesTokenAuthFilter filter = new KubernetesTokenAuthFilter(tokenSupplier);
+
+ filter.filter(requestContext);
+
+ verify(requestContext).getHeaders();
+ assertTrue(headers.containsKey(HttpHeaders.AUTHORIZATION));
+ assertEquals("Bearer " + token, headers.getFirst(HttpHeaders.AUTHORIZATION));
+ }
+}
diff --git a/dbaas/dbaas-aggregator/src/test/java/com/netcracker/cloud/dbaas/security/validators/NamespaceValidationRequestFilterTest.java b/dbaas/dbaas-aggregator/src/test/java/com/netcracker/cloud/dbaas/security/validators/NamespaceValidationRequestFilterTest.java
new file mode 100644
index 00000000..61393bbb
--- /dev/null
+++ b/dbaas/dbaas-aggregator/src/test/java/com/netcracker/cloud/dbaas/security/validators/NamespaceValidationRequestFilterTest.java
@@ -0,0 +1,105 @@
+package com.netcracker.cloud.dbaas.security.validators;
+
+import com.netcracker.cloud.dbaas.DbaasApiPath;
+import com.netcracker.cloud.dbaas.exceptions.FailedNamespaceIsolationCheckException;
+import com.netcracker.cloud.dbaas.utils.JwtUtils;
+import io.smallrye.jwt.auth.principal.JWTCallerPrincipal;
+import jakarta.ws.rs.container.ContainerRequestContext;
+import jakarta.ws.rs.core.MultivaluedMap;
+import jakarta.ws.rs.core.SecurityContext;
+import jakarta.ws.rs.core.UriInfo;
+import org.junit.Assert;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+
+import static org.mockito.Mockito.*;
+
+class NamespaceValidationRequestFilterTest {
+
+ private NamespaceValidationRequestFilter filter;
+ private NamespaceValidator namespaceValidator;
+ private ContainerRequestContext requestContext;
+ private UriInfo uriInfo;
+ private MultivaluedMap pathParams;
+ private SecurityContext securityContext;
+
+ @BeforeEach
+ void setUp() {
+ filter = new NamespaceValidationRequestFilter();
+ namespaceValidator = mock(NamespaceValidator.class);
+ filter.namespaceValidator = namespaceValidator;
+
+ requestContext = mock(ContainerRequestContext.class);
+ uriInfo = mock(UriInfo.class);
+ pathParams = mock(MultivaluedMap.class);
+ securityContext = mock(SecurityContext.class);
+
+ when(requestContext.getUriInfo()).thenReturn(uriInfo);
+ when(uriInfo.getPathParameters()).thenReturn(pathParams);
+ when(requestContext.getSecurityContext()).thenReturn(securityContext);
+ }
+
+ @Test
+ void testFilter_noNamespaceInPath() throws IOException {
+ when(pathParams.getFirst(DbaasApiPath.NAMESPACE_PARAMETER)).thenReturn(null);
+
+ filter.filter(requestContext);
+
+ verify(namespaceValidator, never()).checkNamespaceIsolation(any(), any());
+ verify(requestContext, never()).abortWith(any());
+ }
+
+ @Test
+ void testFilter_notJwtPrincipal() throws IOException {
+ when(pathParams.getFirst(DbaasApiPath.NAMESPACE_PARAMETER)).thenReturn("ns1");
+ when(securityContext.getUserPrincipal()).thenReturn(() -> "basicUser");
+
+ filter.filter(requestContext);
+
+ verify(namespaceValidator, never()).checkNamespaceIsolation(any(), any());
+ verify(requestContext, never()).abortWith(any());
+ }
+
+ @Test
+ void testFilter_validNamespaceIsolation() throws IOException {
+ String nsFromPath = "ns1";
+ when(pathParams.getFirst(DbaasApiPath.NAMESPACE_PARAMETER)).thenReturn(nsFromPath);
+
+ JWTCallerPrincipal jwtPrincipal = mock(JWTCallerPrincipal.class);
+ when(securityContext.getUserPrincipal()).thenReturn(jwtPrincipal);
+
+ try (var mocked = mockStatic(JwtUtils.class)) {
+ mocked.when(() -> JwtUtils.getNamespace(securityContext)).thenReturn("ns1");
+
+ when(namespaceValidator.checkNamespaceIsolation("ns1", "ns1")).thenReturn(true);
+
+ filter.filter(requestContext);
+
+ verify(namespaceValidator, times(1)).checkNamespaceIsolation("ns1", "ns1");
+ verify(requestContext, never()).abortWith(any());
+ }
+ }
+
+ @Test
+ void testFilter_invalidNamespaceIsolation() throws IOException {
+ String nsFromPath = "ns1";
+ when(pathParams.getFirst(DbaasApiPath.NAMESPACE_PARAMETER)).thenReturn(nsFromPath);
+
+ JWTCallerPrincipal jwtPrincipal = mock(JWTCallerPrincipal.class);
+ when(securityContext.getUserPrincipal()).thenReturn(jwtPrincipal);
+
+ try (var mocked = mockStatic(JwtUtils.class)) {
+ mocked.when(() -> JwtUtils.getNamespace(securityContext)).thenReturn("other-ns");
+
+ when(namespaceValidator.checkNamespaceIsolation("ns1", "other-ns")).thenReturn(false);
+
+ Assert.assertThrows(FailedNamespaceIsolationCheckException.class, () -> {
+ filter.filter(requestContext);
+ });
+
+ verify(namespaceValidator, times(1)).checkNamespaceIsolation("ns1", "other-ns");
+ }
+ }
+}
diff --git a/dbaas/dbaas-aggregator/src/test/java/com/netcracker/cloud/dbaas/security/validators/NamespaceValidatorTest.java b/dbaas/dbaas-aggregator/src/test/java/com/netcracker/cloud/dbaas/security/validators/NamespaceValidatorTest.java
new file mode 100644
index 00000000..2739aaf0
--- /dev/null
+++ b/dbaas/dbaas-aggregator/src/test/java/com/netcracker/cloud/dbaas/security/validators/NamespaceValidatorTest.java
@@ -0,0 +1,85 @@
+package com.netcracker.cloud.dbaas.security.validators;
+
+import com.netcracker.cloud.dbaas.entity.pg.composite.CompositeStructure;
+import com.netcracker.cloud.dbaas.service.composite.CompositeNamespaceService;
+import com.netcracker.cloud.dbaas.utils.JwtUtils;
+import io.smallrye.jwt.auth.principal.JWTCallerPrincipal;
+import jakarta.ws.rs.core.SecurityContext;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.util.Collections;
+import java.util.Optional;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class NamespaceValidatorTest {
+ private static final String defaultBaseLine = "default";
+ private static final String defaultNamespace = "default";
+ private static final String otherNamespaceInComposite = "otherNamespaceInComposite";
+ private static final String someOtherNamespace = "someOtherNamespace";
+
+ @Mock
+ CompositeNamespaceService compositeNamespaceService;
+
+ @Mock
+ SecurityContext securityContext;
+
+ @Mock
+ JWTCallerPrincipal principal;
+
+ @InjectMocks
+ NamespaceValidator namespaceValidator;
+
+ @BeforeEach
+ void setUp() {
+ namespaceValidator.namespaceIsolationEnabled = true;
+ }
+
+ @AfterEach
+ void tearDown() {
+ }
+
+ @Test
+ void checkNamespaceIsolation() {
+ Set namespaces = Set.of(defaultNamespace, otherNamespaceInComposite);
+ CompositeStructure defaultCompositeStructure = new CompositeStructure(defaultBaseLine, namespaces);
+
+ when(compositeNamespaceService.getCompositeStructure(defaultBaseLine)).thenReturn(Optional.of(defaultCompositeStructure));
+
+ when(compositeNamespaceService.getBaselineByNamespace(defaultNamespace)).thenReturn(Optional.of(defaultBaseLine));
+ when(compositeNamespaceService.getBaselineByNamespace(otherNamespaceInComposite)).thenReturn(Optional.of(defaultBaseLine));
+ when(compositeNamespaceService.getBaselineByNamespace(someOtherNamespace)).thenReturn(Optional.empty());
+
+ assertTrue(namespaceValidator.checkNamespaceIsolation(defaultNamespace, defaultNamespace));
+ assertTrue(namespaceValidator.checkNamespaceIsolation(someOtherNamespace, someOtherNamespace));
+ assertFalse(namespaceValidator.checkNamespaceIsolation(someOtherNamespace, "notEqualSomeOtherNamespace"));
+ assertFalse(namespaceValidator.checkNamespaceIsolation(defaultNamespace, someOtherNamespace));
+ assertFalse(namespaceValidator.checkNamespaceIsolation(otherNamespaceInComposite, someOtherNamespace));
+ assertTrue(namespaceValidator.checkNamespaceIsolation(otherNamespaceInComposite, defaultNamespace));
+ }
+
+ @Test
+ void checkNamespaceFromClassifier() {
+ when(securityContext.getUserPrincipal()).thenReturn(principal);
+ when(compositeNamespaceService.getBaselineByNamespace(otherNamespaceInComposite)).thenReturn(Optional.of(defaultBaseLine));
+ when(compositeNamespaceService.getBaselineByNamespace(someOtherNamespace)).thenReturn(Optional.empty());
+
+ try (var jwtMock = mockStatic(JwtUtils.class)) {
+ jwtMock.when(() -> JwtUtils.getNamespace(securityContext)).thenReturn(defaultNamespace);
+ assertTrue(namespaceValidator.checkNamespaceFromClassifier(securityContext, Collections.singletonMap("namespace", defaultNamespace)));
+ assertTrue(namespaceValidator.checkNamespaceFromClassifier(securityContext, Collections.singletonMap("namespace", otherNamespaceInComposite)));
+ assertFalse(namespaceValidator.checkNamespaceFromClassifier(securityContext, Collections.singletonMap("namespace", someOtherNamespace)));
+ }
+ }
+}
diff --git a/dbaas/dbaas-aggregator/src/test/java/com/netcracker/cloud/dbaas/utils/JwtUtilsTest.java b/dbaas/dbaas-aggregator/src/test/java/com/netcracker/cloud/dbaas/utils/JwtUtilsTest.java
new file mode 100644
index 00000000..270b4488
--- /dev/null
+++ b/dbaas/dbaas-aggregator/src/test/java/com/netcracker/cloud/dbaas/utils/JwtUtilsTest.java
@@ -0,0 +1,65 @@
+package com.netcracker.cloud.dbaas.utils;
+
+import io.smallrye.jwt.auth.principal.DefaultJWTCallerPrincipal;
+import jakarta.json.Json;
+import jakarta.json.JsonObject;
+import jakarta.json.JsonString;
+import jakarta.ws.rs.core.SecurityContext;
+import org.eclipse.microprofile.jwt.JsonWebToken;
+import org.junit.jupiter.api.Test;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.*;
+
+class JwtUtilsTest {
+
+ @Test
+ void testGetServiceAccountName() {
+ JsonWebToken token = mock(JsonWebToken.class);
+
+ JsonObject serviceAccount = Json.createObjectBuilder()
+ .add("name", "test-service-account")
+ .build();
+
+ Map kubernetesClaims = new HashMap<>();
+ kubernetesClaims.put("serviceaccount", serviceAccount);
+
+ when(token.getClaim("kubernetes.io")).thenReturn(kubernetesClaims);
+
+ String result = JwtUtils.getServiceAccountName(token);
+
+ assertEquals("test-service-account", result);
+ verify(token, times(1)).getClaim("kubernetes.io");
+ }
+
+ @Test
+ void testGetNamespace_withValidPrincipal() {
+ SecurityContext securityContext = mock(SecurityContext.class);
+ DefaultJWTCallerPrincipal principal = mock(DefaultJWTCallerPrincipal.class);
+
+ JsonString namespace = Json.createValue("test-namespace");
+ Map kubernetesClaims = new HashMap<>();
+ kubernetesClaims.put("namespace", namespace);
+
+ when(principal.getClaim("kubernetes.io")).thenReturn(kubernetesClaims);
+ when(securityContext.getUserPrincipal()).thenReturn(principal);
+
+ String result = JwtUtils.getNamespace(securityContext);
+
+ assertEquals("test-namespace", result);
+ verify(principal, times(1)).getClaim("kubernetes.io");
+ }
+
+ @Test
+ void testGetNamespace_withInvalidPrincipal() {
+ SecurityContext securityContext = mock(SecurityContext.class);
+ when(securityContext.getUserPrincipal()).thenReturn(() -> "someUser");
+
+ String result = JwtUtils.getNamespace(securityContext);
+
+ assertEquals("", result);
+ }
+}
diff --git a/dbaas/dbaas-aggregator/src/test/resources/application.properties b/dbaas/dbaas-aggregator/src/test/resources/application.properties
index e2122e0c..e1fca822 100644
--- a/dbaas/dbaas-aggregator/src/test/resources/application.properties
+++ b/dbaas/dbaas-aggregator/src/test/resources/application.properties
@@ -4,4 +4,6 @@ dbaas.process-orchestrator.enabled=false
dbaas.h2.sync.every=86400
dbaas.security.users.configuration.location=/users.json
+dbaas.security.k8s.service.account.roles.path=./src/test/resources/service-account-roles-secret.yaml
+dbaas.security.k8s.jwt.enabled=false
dbaas.global-permissions.configuration.location=/services.json
diff --git a/dbaas/dbaas-aggregator/src/test/resources/service-account-roles-secret.yaml b/dbaas/dbaas-aggregator/src/test/resources/service-account-roles-secret.yaml
new file mode 100644
index 00000000..823f14b6
--- /dev/null
+++ b/dbaas/dbaas-aggregator/src/test/resources/service-account-roles-secret.yaml
@@ -0,0 +1,7 @@
+service-account-1:
+ - NAMESPACE_CLEANER
+ - DB_CLIENT
+ - MIGRATION_CLIENT
+service-account-2:
+ - NAMESPACE_CLEANER
+ - MIGRATION_CLIENT
diff --git a/docs/OpenAPI.json b/docs/OpenAPI.json
index f45181c5..25167184 100644
--- a/docs/OpenAPI.json
+++ b/docs/OpenAPI.json
@@ -657,8 +657,7 @@
"ClassifierWithRolesRequest": {
"type": "object",
"required": [
- "classifier",
- "originService"
+ "classifier"
],
"properties": {
"classifier": {
@@ -921,8 +920,7 @@
"type": "object",
"required": [
"classifier",
- "type",
- "originService"
+ "type"
],
"description": "V3 Request model for adding database to DBaaS",
"properties": {
@@ -4153,6 +4151,14 @@
}
}
}
+ },
+ "securitySchemes": {
+ "SecurityScheme": {
+ "type": "http",
+ "scheme": "bearer",
+ "bearerFormat": "JWT",
+ "description": "Authentication"
+ }
}
},
"tags": [
diff --git a/helm-templates/dbaas-aggregator/data/service-account-roles.yaml b/helm-templates/dbaas-aggregator/data/service-account-roles.yaml
new file mode 100644
index 00000000..5bc59fcc
--- /dev/null
+++ b/helm-templates/dbaas-aggregator/data/service-account-roles.yaml
@@ -0,0 +1,17 @@
+{{ .Values.BACKUP_DAEMON_DBAAS_ACCESS_USERNAME | default "backup-daemon" }}:
+ - BACKUP_MANAGER
+
+{{ .Values.DBAAS_CLUSTER_DBA_CREDENTIALS_USERNAME | default "cluster-dba" }}:
+ - NAMESPACE_CLEANER
+ - DB_CLIENT
+ - MIGRATION_CLIENT
+
+{{ .Values.DBAAS_TENANT_USERNAME | default "dbaas-tenant" }}:
+ - NAMESPACE_CLEANER
+ - DB_CLIENT
+
+{{ .Values.DBAAS_DB_EDITOR_CREDENTIALS_USERNAME | default "dbaas-db-editor" }}:
+ - DBAAS_DB_EDITOR
+
+{{ .Values.DISCR_TOOL_USER_USERNAME | default "discr_tool_user" }}:
+ - DISCR_TOOL_CLIENT
diff --git a/helm-templates/dbaas-aggregator/templates/Deployment.yaml b/helm-templates/dbaas-aggregator/templates/Deployment.yaml
index 5e7736a2..3a054a4d 100644
--- a/helm-templates/dbaas-aggregator/templates/Deployment.yaml
+++ b/helm-templates/dbaas-aggregator/templates/Deployment.yaml
@@ -36,6 +36,9 @@ spec:
- name: dbaas-global-permissions-configuration-volume
secret:
secretName: 'dbaas-global-permissions-configuration-secret'
+ - name: dbaas-security-service-account-roles-volume
+ secret:
+ secretName: 'dbaas-security-service-account-roles-secret'
{{ if .Values.READONLY_CONTAINER_FILE_SYSTEM_ENABLED }}
- name: tmp
emptyDir: {}
@@ -59,6 +62,9 @@ spec:
- name: dbaas-security-configuration-volume
mountPath: '{{ .Values.DBAAS_SECURITY_CONFIGURATION_LOCATION }}'
readOnly: true
+ - name: dbaas-security-service-account-roles-volume
+ mountPath: '{{ .Values.DBAAS_SECURITY_CONFIGURATION_LOCATION }}'
+ readOnly: true
- name: dbaas-global-permissions-configuration-volume
mountPath: '{{ .Values.DBAAS_GLOBAL_PERMISSIONS_CONFIGURATION_LOCATION }}'
readOnly: true
diff --git a/helm-templates/dbaas-aggregator/templates/ServiceRolesSecret.yaml b/helm-templates/dbaas-aggregator/templates/ServiceRolesSecret.yaml
new file mode 100644
index 00000000..cd27d2b8
--- /dev/null
+++ b/helm-templates/dbaas-aggregator/templates/ServiceRolesSecret.yaml
@@ -0,0 +1,12 @@
+{{- $b64p := tpl (.Files.Get "data/service-account-roles.yaml") $ | b64enc }}
+---
+apiVersion: v1
+kind: Secret
+metadata:
+ name: dbaas-security-service-account-roles-secret
+ labels:
+ app.kubernetes.io/name: "{{ .Values.SERVICE_NAME }}"
+ deployment.qubership.org/sessionId: {{ .Values.DEPLOYMENT_SESSION_ID | default "unimplemented" }}
+type: Opaque
+data:
+ "service-account-roles.yaml": {{ $b64p }}