> resolveReadableNamespaces() {
- if (!userAccessValidatorInstance.isResolvable() || !jwtInstance.isResolvable()) {
+ if (!userAccessValidatorInstance.isResolvable()) {
return Optional.empty();
}
- String username = jwtInstance.get().getClaim("preferred_username");
+ String username = null;
+ if (jwtInstance.isResolvable()) {
+ username = jwtInstance.get().getClaim("preferred_username");
+ }
+ if (username == null && httpHeaders != null) {
+ username = httpHeaders.getHeaderString(proxyUsernameHeader);
+ }
if (username == null) {
return Optional.empty();
}
diff --git a/calm-hub/src/main/java/org/finos/calm/resources/StandardResource.java b/calm-hub/src/main/java/org/finos/calm/resources/StandardResource.java
index 2de36b583..d977253ac 100644
--- a/calm-hub/src/main/java/org/finos/calm/resources/StandardResource.java
+++ b/calm-hub/src/main/java/org/finos/calm/resources/StandardResource.java
@@ -1,5 +1,6 @@
package org.finos.calm.resources;
+import io.quarkus.security.PermissionsAllowed;
import jakarta.validation.constraints.Pattern;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
@@ -12,7 +13,6 @@
import org.finos.calm.domain.exception.StandardVersionNotFoundException;
import org.finos.calm.domain.standards.CreateStandardRequest;
import org.finos.calm.security.CalmHubScopes;
-import org.finos.calm.security.PermittedScopes;
import org.finos.calm.store.StandardStore;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -20,10 +20,7 @@
import java.net.URI;
import java.net.URISyntaxException;
-import static org.finos.calm.resources.ResourceValidationConstants.NAMESPACE_MESSAGE;
-import static org.finos.calm.resources.ResourceValidationConstants.NAMESPACE_REGEX;
-import static org.finos.calm.resources.ResourceValidationConstants.VERSION_MESSAGE;
-import static org.finos.calm.resources.ResourceValidationConstants.VERSION_REGEX;
+import static org.finos.calm.resources.ResourceValidationConstants.*;
@Path("/calm/namespaces")
public class StandardResource {
@@ -39,7 +36,7 @@ public StandardResource(StandardStore standardStore) {
@GET
@Path("{namespace}/standards")
@Produces(MediaType.APPLICATION_JSON)
- @PermittedScopes({CalmHubScopes.ARCHITECTURES_ALL, CalmHubScopes.ARCHITECTURES_READ})
+ @PermissionsAllowed(CalmHubScopes.READ)
public Response getStandardsForNamespace(
@PathParam("namespace") @Pattern(regexp = NAMESPACE_REGEX, message = NAMESPACE_MESSAGE) String namespace
) {
@@ -55,7 +52,7 @@ public Response getStandardsForNamespace(
@Path("{namespace}/standards")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
- @PermittedScopes({CalmHubScopes.ARCHITECTURES_ALL, CalmHubScopes.ARCHITECTURES_READ})
+ @PermissionsAllowed(CalmHubScopes.WRITE)
public Response createStandardForNamespace(
@PathParam("namespace") @Pattern(regexp = NAMESPACE_REGEX, message = NAMESPACE_MESSAGE) String namespace,
CreateStandardRequest standard
@@ -72,7 +69,7 @@ public Response createStandardForNamespace(
@GET
@Path("{namespace}/standards/{standardId}/versions")
@Produces(MediaType.APPLICATION_JSON)
- @PermittedScopes({CalmHubScopes.ARCHITECTURES_ALL, CalmHubScopes.ARCHITECTURES_READ})
+ @PermissionsAllowed(CalmHubScopes.READ)
public Response getStandardVersions(
@PathParam("namespace") @Pattern(regexp = NAMESPACE_REGEX, message = NAMESPACE_MESSAGE) String namespace,
@PathParam("standardId") Integer standardId
@@ -91,7 +88,7 @@ public Response getStandardVersions(
@GET
@Path("{namespace}/standards/{standardId}/versions/{version}")
@Produces(MediaType.APPLICATION_JSON)
- @PermittedScopes({CalmHubScopes.ARCHITECTURES_ALL, CalmHubScopes.ARCHITECTURES_READ})
+ @PermissionsAllowed(CalmHubScopes.READ)
public Response getStandardForVersion(
@PathParam("namespace") @Pattern(regexp = NAMESPACE_REGEX, message = NAMESPACE_MESSAGE) String namespace,
@PathParam("standardId") Integer standardId,
@@ -115,7 +112,7 @@ public Response getStandardForVersion(
@Path("{namespace}/standards/{standardId}/versions/{version}")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
- @PermittedScopes({CalmHubScopes.ARCHITECTURES_ALL, CalmHubScopes.ARCHITECTURES_READ})
+ @PermissionsAllowed(CalmHubScopes.WRITE)
public Response createStandardForVersion(
@PathParam("namespace") @Pattern(regexp = NAMESPACE_REGEX, message = NAMESPACE_MESSAGE) String namespace,
@PathParam("standardId") Integer standardId,
diff --git a/calm-hub/src/main/java/org/finos/calm/resources/TimelineResource.java b/calm-hub/src/main/java/org/finos/calm/resources/TimelineResource.java
index 64fc7097e..3f7767417 100644
--- a/calm-hub/src/main/java/org/finos/calm/resources/TimelineResource.java
+++ b/calm-hub/src/main/java/org/finos/calm/resources/TimelineResource.java
@@ -1,5 +1,6 @@
package org.finos.calm.resources;
+import io.quarkus.security.PermissionsAllowed;
import jakarta.inject.Inject;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
@@ -18,7 +19,6 @@
import org.finos.calm.domain.timeline.CreateTimelineRequest;
import org.finos.calm.domain.timeline.Timeline;
import org.finos.calm.security.CalmHubScopes;
-import org.finos.calm.security.PermittedScopes;
import org.finos.calm.store.TimelineStore;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -26,11 +26,7 @@
import java.net.URI;
import java.net.URISyntaxException;
-import static org.finos.calm.resources.ResourceValidationConstants.NAMESPACE_MESSAGE;
-import static org.finos.calm.resources.ResourceValidationConstants.NAMESPACE_REGEX;
-import static org.finos.calm.resources.ResourceValidationConstants.STRICT_SANITIZATION_POLICY;
-import static org.finos.calm.resources.ResourceValidationConstants.VERSION_MESSAGE;
-import static org.finos.calm.resources.ResourceValidationConstants.VERSION_REGEX;
+import static org.finos.calm.resources.ResourceValidationConstants.*;
/**
* Resource for managing explicit timelines in a given namespace.
@@ -57,7 +53,7 @@ public TimelineResource(TimelineStore store) {
summary = "Retrieve timelines in a given namespace",
description = "Timelines stored in a given namespace"
)
- @PermittedScopes({CalmHubScopes.ARCHITECTURES_ALL, CalmHubScopes.ARCHITECTURES_READ})
+ @PermissionsAllowed(CalmHubScopes.READ)
public Response getTimelinesForNamespace(
@PathParam("namespace") @Pattern(regexp = NAMESPACE_REGEX, message = NAMESPACE_MESSAGE) String namespace
) {
@@ -77,7 +73,7 @@ public Response getTimelinesForNamespace(
summary = "Create timeline for namespace",
description = "Creates a timeline for a given namespace with an allocated ID and version 1.0.0"
)
- @PermittedScopes({CalmHubScopes.ARCHITECTURES_ALL})
+ @PermissionsAllowed(CalmHubScopes.READ)
public Response createTimelineForNamespace(
@PathParam("namespace") @Pattern(regexp = NAMESPACE_REGEX, message = NAMESPACE_MESSAGE) String namespace,
@Valid @NotNull(message = "Request must not be null") CreateTimelineRequest timelineRequest
@@ -101,7 +97,7 @@ public Response createTimelineForNamespace(
summary = "Retrieve a list of versions for a given timeline",
description = "Timeline versions are not opinionated, outside of the first version created"
)
- @PermittedScopes({CalmHubScopes.ARCHITECTURES_ALL, CalmHubScopes.ARCHITECTURES_READ})
+ @PermissionsAllowed(CalmHubScopes.READ)
public Response getTimelineVersions(
@PathParam("namespace") @Pattern(regexp = NAMESPACE_REGEX, message = NAMESPACE_MESSAGE) String namespace,
@PathParam("timelineId") int timelineId
@@ -129,7 +125,7 @@ public Response getTimelineVersions(
summary = "Retrieve a specific timeline at a given version",
description = "Retrieve timelines at a specific version"
)
- @PermittedScopes({CalmHubScopes.ARCHITECTURES_ALL, CalmHubScopes.ARCHITECTURES_READ})
+ @PermissionsAllowed(CalmHubScopes.READ)
public Response getTimeline(
@PathParam("namespace") @Pattern(regexp = NAMESPACE_REGEX, message = NAMESPACE_MESSAGE) String namespace,
@PathParam("timelineId") int timelineId,
@@ -163,7 +159,7 @@ public Response getTimeline(
summary = "Create a new version of an existing timeline",
description = "Stores a new version of the timeline under the supplied {version}. The request body is an envelope containing `name`, optional `description`, and the inner `timelineJson` document; only the inner document is persisted as the version contents."
)
- @PermittedScopes({CalmHubScopes.ARCHITECTURES_ALL})
+ @PermissionsAllowed(CalmHubScopes.WRITE)
public Response createVersionedTimeline(
@PathParam("namespace") @Pattern(regexp = NAMESPACE_REGEX, message = NAMESPACE_MESSAGE) String namespace,
@PathParam("timelineId") int timelineId,
@@ -202,7 +198,7 @@ public Response createVersionedTimeline(
summary = "Updates a Timeline (if available)",
description = "In mutable version stores timeline updates are supported by this endpoint, operation unavailable returned in repositories without configuration specified. The request body uses the same envelope as POST."
)
- @PermittedScopes({CalmHubScopes.ARCHITECTURES_ALL})
+ @PermissionsAllowed(CalmHubScopes.WRITE)
public Response updateVersionedTimeline(
@PathParam("namespace") @Pattern(regexp = NAMESPACE_REGEX, message = NAMESPACE_MESSAGE) String namespace,
@PathParam("timelineId") int timelineId,
diff --git a/calm-hub/src/main/java/org/finos/calm/resources/UserAccessResource.java b/calm-hub/src/main/java/org/finos/calm/resources/UserAccessResource.java
index 6fc641fe2..2ec331cbe 100644
--- a/calm-hub/src/main/java/org/finos/calm/resources/UserAccessResource.java
+++ b/calm-hub/src/main/java/org/finos/calm/resources/UserAccessResource.java
@@ -1,11 +1,7 @@
package org.finos.calm.resources;
-import jakarta.ws.rs.Consumes;
-import jakarta.ws.rs.POST;
-import jakarta.ws.rs.GET;
-import jakarta.ws.rs.Path;
-import jakarta.ws.rs.Produces;
-import jakarta.ws.rs.PathParam;
+import io.quarkus.security.PermissionsAllowed;
+import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.openapi.annotations.Operation;
@@ -13,7 +9,6 @@
import org.finos.calm.domain.exception.NamespaceNotFoundException;
import org.finos.calm.domain.exception.UserAccessNotFoundException;
import org.finos.calm.security.CalmHubScopes;
-import org.finos.calm.security.PermittedScopes;
import org.finos.calm.store.UserAccessStore;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -40,7 +35,7 @@ public UserAccessResource(UserAccessStore userAccessStore) {
summary = "Create user access for namespace",
description = "Creates a user-access for a given namespace on a particular resource type"
)
- @PermittedScopes({CalmHubScopes.NAMESPACE_ADMIN})
+ @PermissionsAllowed(CalmHubScopes.ADMIN)
public Response createUserAccessForNamespace(@PathParam("namespace") String namespace,
UserAccess createUserAccessRequest) {
@@ -70,7 +65,7 @@ public Response createUserAccessForNamespace(@PathParam("namespace") String name
summary = "Get user-access for a given namespace",
description = "Get user-access details for a given namespace"
)
- @PermittedScopes({CalmHubScopes.NAMESPACE_ADMIN})
+ @PermissionsAllowed(CalmHubScopes.ADMIN)
public Response getUserAccessForNamespace(@PathParam("namespace") String namespace) {
try {
@@ -94,7 +89,7 @@ public Response getUserAccessForNamespace(@PathParam("namespace") String namespa
summary = "Get the user-access record for a given namespace and Id",
description = "Get user-access details for a given namespace and Id"
)
- @PermittedScopes({CalmHubScopes.NAMESPACE_ADMIN})
+ @PermissionsAllowed(CalmHubScopes.ADMIN)
public Response getUserAccessForNamespaceAndId(@PathParam("namespace") String namespace,
@PathParam("userAccessId") Integer userAccessId) {
diff --git a/calm-hub/src/main/java/org/finos/calm/security/AccessControlFilter.java b/calm-hub/src/main/java/org/finos/calm/security/AccessControlFilter.java
deleted file mode 100644
index bc79be2cc..000000000
--- a/calm-hub/src/main/java/org/finos/calm/security/AccessControlFilter.java
+++ /dev/null
@@ -1,125 +0,0 @@
-package org.finos.calm.security;
-
-
-import io.quarkus.arc.profile.IfBuildProfile;
-import io.quarkus.runtime.util.StringUtil;
-import jakarta.annotation.Priority;
-import jakarta.enterprise.context.ApplicationScoped;
-import jakarta.ws.rs.container.ContainerRequestContext;
-import jakarta.ws.rs.container.ContainerRequestFilter;
-import jakarta.ws.rs.container.ResourceInfo;
-import jakarta.ws.rs.core.Response;
-import jakarta.ws.rs.ext.Provider;
-import org.eclipse.microprofile.jwt.JsonWebToken;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.util.List;
-import java.util.Objects;
-
-
-/**
- * This filter is responsible for validating incoming JWT tokens and enforcing access control rules
- * based on OAuth 2.0 authorization flows and RBAC (Role-Based Access Control).
- *
- * The Resource Server supports:
- * 1. **Authorization Code Flow / Device Code Flow (End-User Authentication)**
- * - Tokens issued for an authenticated user **must undergo additional RBAC checks**
- * to ensure the user has the necessary permissions.
- * - OAuth scopes provide high-level access control, but **RBAC is required for fine-grained permissions**.
- *
- * 🔹 **TODO: Implement RBAC checks to enforce role-based access control on top of scopes.**
- *
- * Why RBAC?
- * - Scopes define **what** actions a user can perform but do not control **who** can perform them.
- * - RBAC ensures that even if a user has a valid scope, their role (e.g., Admin, Viewer)
- * dictates whether they can execute the request.
- *
- * This filter currently validates JWT tokens and scopes. **RBAC enforcement is a pending task.**
- */
-@ApplicationScoped
-@Provider
-@Priority(1)
-@IfBuildProfile("secure")
-public class AccessControlFilter implements ContainerRequestFilter {
-
- private final JsonWebToken jwt;
- private final ResourceInfo resourceInfo;
- private final UserAccessValidator userAccessValidator;
- private final Logger logger = LoggerFactory.getLogger(AccessControlFilter.class);
-
- public AccessControlFilter(JsonWebToken jwt, ResourceInfo resourceInfo,
- UserAccessValidator userAccessValidator) {
- this.jwt = jwt;
- this.resourceInfo = resourceInfo;
- this.userAccessValidator = userAccessValidator;
- }
-
- @Override
- public void filter(ContainerRequestContext requestContext) {
- PermittedScopes annotation = resourceInfo.getResourceMethod().getAnnotation(PermittedScopes.class);
- if (Objects.isNull(annotation)) {
- logger.warn("Unsecured endpoint accessed: {}", resourceInfo.getResourceMethod());
- return;
- }
-
- String[] requiredScopes = annotation.value();
- String tokenScopes = jwt.getClaim("scope");
-
- if (StringUtil.isNullOrEmpty(tokenScopes) || !hasRequiredScope(tokenScopes, requiredScopes)) {
- requestContext.abortWith(Response.status(Response.Status.FORBIDDEN)
- .entity("Forbidden: JWT does not have required scopes.")
- .build());
- return;
- }
-
- authorizeUserRequest(requestContext);
- }
-
- /**
- * Validates whether the requesting user has the required access permissions based on the request context.
- *
- * This method extracts the HTTP method, username (from JWT), request path, and namespace
- * from the incoming request and checks whether the user is authorized to access the requested resource.
- * If the user lacks the necessary permissions, the request is aborted with a 403 Forbidden response.
- *
- * @param requestContext the container request context containing request metadata and parameters
- */
- private void authorizeUserRequest(ContainerRequestContext requestContext) {
- String requestMethod = requestContext.getMethod();
- String username = jwt.getClaim("preferred_username");
- String path = requestContext.getUriInfo().getPath();
- String namespace = requestContext.getUriInfo().getPathParameters().getFirst("namespace");
-
- UserRequestAttributes userRequestAttributes = new UserRequestAttributes(requestMethod,
- username, path, namespace);
- logger.debug("User request attributes: {}", userRequestAttributes);
-
- if (!userAccessValidator.isUserAuthorized(userRequestAttributes)) {
- logger.warn("No access permissions assigned to the user: [{}]", userRequestAttributes.username());
- requestContext.abortWith(Response.status(Response.Status.FORBIDDEN)
- .entity("Forbidden: user does not have required access grants")
- .build());
- }
- }
-
- /**
- * Check if the JWT token has at least one of the required scopes.
- *
- * @param tokenScopes The scopes in the JWT token.
- * @param requiredScopes The scopes required for the resource.
- * @return true if the token contains one of the required scopes, false otherwise.
- */
- private boolean hasRequiredScope(String tokenScopes, String[] requiredScopes) {
- List requiredScopesList = List.of(requiredScopes);
- boolean hasMatch = requiredScopesList.stream()
- .anyMatch(tokenScopes::contains);
-
- if (hasMatch) {
- logger.debug("Request allowed, PermittedScopes are: {}, there is a matching scope found in accessToken: [{}]", requiredScopes, tokenScopes);
- } else {
- logger.error("Request denied, PermittedScopes are: {}, no matching scopes found in accessToken: [{}]", requiredScopes, tokenScopes);
- }
- return hasMatch;
- }
-}
\ No newline at end of file
diff --git a/calm-hub/src/main/java/org/finos/calm/security/CalmHubPermissionChecker.java b/calm-hub/src/main/java/org/finos/calm/security/CalmHubPermissionChecker.java
new file mode 100644
index 000000000..2a34ff698
--- /dev/null
+++ b/calm-hub/src/main/java/org/finos/calm/security/CalmHubPermissionChecker.java
@@ -0,0 +1,132 @@
+package org.finos.calm.security;
+
+import io.quarkus.security.PermissionChecker;
+import io.quarkus.security.identity.SecurityIdentity;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+import org.finos.calm.domain.UserAccess;
+import org.finos.calm.domain.UserAction;
+import org.finos.calm.domain.exception.UserAccessNotFoundException;
+import org.finos.calm.store.UserAccessStore;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.function.Predicate;
+
+
+@ApplicationScoped
+public class CalmHubPermissionChecker {
+ private static final Logger logger = LoggerFactory.getLogger(CalmHubPermissionChecker.class);
+
+ private final UserAccessStore userAccessStore;
+
+ @Inject
+ @ConfigProperty(name = "calm.hub.no.auth.enabled", defaultValue = "false")
+ boolean noAuthEnabled;
+
+ @Inject
+ @ConfigProperty(name = "calm.hub.allow.public.read", defaultValue = "false")
+ boolean allowPublicRead;
+
+ public CalmHubPermissionChecker(UserAccessStore userAccessStore) {
+ this.userAccessStore = userAccessStore;
+ }
+
+ @PermissionChecker(CalmHubScopes.ADMIN)
+ public boolean allowNamespaceAdmin(SecurityIdentity identity, String namespace) {
+ if (noAuthEnabled) return true;
+ return hasNamespaceAccess(identity, namespace, UserAction.ADMIN);
+ }
+
+ @PermissionChecker(CalmHubScopes.READ)
+ public boolean canRead(SecurityIdentity identity, String namespace) {
+ if (noAuthEnabled) return true;
+ if (allowPublicRead) return true;
+ return hasNamespaceAccess(identity, namespace, UserAction.READ);
+ }
+
+ @PermissionChecker(CalmHubScopes.DOMAIN_READ)
+ public boolean canReadByDomain(SecurityIdentity identity, String domain) {
+ if (noAuthEnabled) return true;
+ if (allowPublicRead) return true;
+ return hasDomainAccess(identity, domain, UserAction.READ);
+ }
+
+ @PermissionChecker(CalmHubScopes.WRITE)
+ public boolean canWrite(SecurityIdentity identity, String namespace) {
+ return noAuthEnabled
+ || hasNamespaceAccess(identity, namespace, UserAction.WRITE);
+ }
+
+ @PermissionChecker(CalmHubScopes.DOMAIN_WRITE)
+ public boolean canWriteByDomain(SecurityIdentity identity, String domain) {
+ return noAuthEnabled
+ || hasDomainAccess(identity, domain, UserAction.WRITE);
+ }
+
+ @PermissionChecker(CalmHubScopes.GLOBAL_ADMIN)
+ public boolean hasGlobalAdmin(SecurityIdentity identity) {
+ if (noAuthEnabled) {
+ logger.warn("CalmHub is running with no authentication. Granting user access unconditionally.");
+ return true;
+ }
+ String username = identity.getPrincipal().getName();
+ logger.debug("Checking global admin access for user [{}]", username);
+ try {
+ boolean granted =
+ userAccessStore.getUserAccessForUsername(username)
+ .stream()
+ .anyMatch(grant -> "GLOBAL".equals(grant.getNamespace())
+ && permissionSufficient(grant, UserAction.ADMIN));
+
+ if (granted) {
+ logger.info("User [{}] AUTHORIZED for GLOBAL admin privileges", username);
+ } else {
+ logger.warn("User [{}] DENIED for GLOBAL admin privileges", username);
+ }
+ return granted;
+ } catch (UserAccessNotFoundException e) {
+ logger.debug("No access grants found for user [{}]", username);
+ return false;
+ }
+ }
+
+ private boolean hasNamespaceAccess(SecurityIdentity identity, String namespace, UserAction action) {
+ return hasAccess(identity, "namespace", namespace, action,
+ grant -> namespace != null && namespace.equals(grant.getNamespace()));
+ }
+
+ private boolean hasDomainAccess(SecurityIdentity identity, String domain, UserAction action) {
+ return hasAccess(identity, "domain", domain, action,
+ grant -> domain != null && domain.equals(grant.getDomain()));
+ }
+
+ private boolean hasAccess(SecurityIdentity identity, String scopeType, String scopeValue,
+ UserAction action, Predicate grantMatcher) {
+ String username = identity.getPrincipal().getName();
+ logger.debug("Checking {} access for user [{}] on {} [{}] action=[{}]",
+ scopeType, username, scopeType, scopeValue, action);
+ try {
+ boolean result = userAccessStore.getUserAccessForUsername(username).stream()
+ .anyMatch(grant -> grantMatcher.test(grant) && permissionSufficient(grant, action));
+ if (result) {
+ logger.info("User [{}] AUTHORIZED for [{}] in {} [{}]", username, action, scopeType, scopeValue);
+ } else {
+ logger.warn("User [{}] DENIED for [{}] in {} [{}]", username, action, scopeType, scopeValue);
+ }
+ return result;
+ } catch (UserAccessNotFoundException e) {
+ logger.debug("No access grants found for user [{}]", username);
+ return false;
+ }
+ }
+
+ private boolean permissionSufficient(UserAccess grant, UserAction action) {
+ return switch (grant.getPermission()) {
+ case read -> action == UserAction.READ;
+ case write -> action == UserAction.READ || action == UserAction.WRITE;
+ case admin -> true;
+ };
+ }
+}
diff --git a/calm-hub/src/main/java/org/finos/calm/security/CalmHubScopes.java b/calm-hub/src/main/java/org/finos/calm/security/CalmHubScopes.java
index 78d307cee..0a52bbb63 100644
--- a/calm-hub/src/main/java/org/finos/calm/security/CalmHubScopes.java
+++ b/calm-hub/src/main/java/org/finos/calm/security/CalmHubScopes.java
@@ -9,33 +9,12 @@ private CalmHubScopes() {
throw new UnsupportedOperationException("This is a utility class and cannot be instantiated");
}
- /**
- * Allows read operations on Flows, Patterns, Namespaces, and Architectures resources.
- */
- public static final String ARCHITECTURES_READ = "architectures:read";
-
- /**
- * Allows full access (read, write, delete) on Flows, Patterns, Namespaces, and Architectures resources.
- */
- public static final String ARCHITECTURES_ALL = "architectures:all";
+ public static final String READ = "read";
+ public static final String WRITE = "write";
+ public static final String ADMIN = "admin";
- /**
- * Allows read operations on Adrs and Namespaces resources.
- */
- public static final String ADRS_READ = "adrs:read";
+ public static final String DOMAIN_READ = "domain_read";
+ public static final String DOMAIN_WRITE = "domain_write";
- /**
- * Allows full access (read, write, delete) on Adrs and read operation on Namespaces.
- */
- public static final String ADRS_ALL = "adrs:all";
-
- /**
- * Allows read operations on the Search endpoint. Results are filtered based on user access grants.
- */
- public static final String SEARCH_READ = "search:read";
-
- /**
- * Allow to grant access to users on namespace associated resources and for the admin operations.
- */
- public static final String NAMESPACE_ADMIN = "namespace:admin";
+ public static final String GLOBAL_ADMIN = "global_admin";
}
diff --git a/calm-hub/src/main/java/org/finos/calm/security/NoAuthAuthenticationMechanism.java b/calm-hub/src/main/java/org/finos/calm/security/NoAuthAuthenticationMechanism.java
new file mode 100644
index 000000000..c469fc262
--- /dev/null
+++ b/calm-hub/src/main/java/org/finos/calm/security/NoAuthAuthenticationMechanism.java
@@ -0,0 +1,48 @@
+package org.finos.calm.security;
+
+import io.quarkus.security.identity.IdentityProviderManager;
+import io.quarkus.security.identity.SecurityIdentity;
+import io.quarkus.security.identity.request.TrustedAuthenticationRequest;
+import io.quarkus.security.runtime.QuarkusPrincipal;
+import io.quarkus.security.runtime.QuarkusSecurityIdentity;
+import io.quarkus.vertx.http.runtime.security.ChallengeData;
+import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism;
+import io.smallrye.mutiny.Uni;
+import io.vertx.ext.web.RoutingContext;
+import jakarta.enterprise.context.ApplicationScoped;
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Optional;
+
+/**
+ * When calm.hub.no.auth.enabled=true (the default profile), every request is given a
+ * non-anonymous identity so that Quarkus calls the @PermissionChecker methods rather than
+ * short-circuiting with 401. The checkers then grant access unconditionally via the same flag.
+ *
+ * In secure and proxy-auth profiles this mechanism is disabled via config and is a no-op.
+ */
+@ApplicationScoped
+public class NoAuthAuthenticationMechanism implements HttpAuthenticationMechanism {
+
+ private static final Logger logger = LoggerFactory.getLogger(NoAuthAuthenticationMechanism.class);
+ static final String NO_AUTH_PRINCIPAL = "no-auth";
+
+ @ConfigProperty(name = "calm.hub.no.auth.enabled", defaultValue = "false")
+ boolean noAuthEnabled;
+
+ @Override
+ public Uni authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) {
+ if (!noAuthEnabled) {
+ return Uni.createFrom().optional(Optional.empty());
+ }
+ logger.debug("No-auth mode: granting open identity to request for {}", context.request().path());
+ return identityProviderManager.authenticate(new TrustedAuthenticationRequest(NO_AUTH_PRINCIPAL));
+ }
+
+ @Override
+ public Uni getChallenge(RoutingContext context) {
+ return Uni.createFrom().nullItem();
+ }
+}
diff --git a/calm-hub/src/main/java/org/finos/calm/security/PermittedScopes.java b/calm-hub/src/main/java/org/finos/calm/security/PermittedScopes.java
deleted file mode 100644
index b30afe83b..000000000
--- a/calm-hub/src/main/java/org/finos/calm/security/PermittedScopes.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package org.finos.calm.security;
-
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
-/**
- * Custom annotation to define required scopes for a REST endpoint.
- */
-@Target({ ElementType.METHOD, ElementType.TYPE })
-@Retention(RetentionPolicy.RUNTIME)
-public @interface PermittedScopes {
- String[] value();
-}
\ No newline at end of file
diff --git a/calm-hub/src/main/java/org/finos/calm/security/ProxyAuthenticationMechanism.java b/calm-hub/src/main/java/org/finos/calm/security/ProxyAuthenticationMechanism.java
new file mode 100644
index 000000000..6bb4090fb
--- /dev/null
+++ b/calm-hub/src/main/java/org/finos/calm/security/ProxyAuthenticationMechanism.java
@@ -0,0 +1,48 @@
+package org.finos.calm.security;
+
+import io.netty.handler.codec.http.HttpResponseStatus;
+import io.quarkus.arc.profile.IfBuildProfile;
+import io.quarkus.oidc.AccessTokenCredential;
+import io.quarkus.oidc.BearerTokenAuthentication;
+import io.quarkus.runtime.util.StringUtil;
+import io.quarkus.security.identity.IdentityProviderManager;
+import io.quarkus.security.identity.SecurityIdentity;
+import io.quarkus.security.identity.request.TrustedAuthenticationRequest;
+import io.quarkus.security.runtime.QuarkusPrincipal;
+import io.quarkus.security.runtime.QuarkusSecurityIdentity;
+import io.quarkus.vertx.http.runtime.security.ChallengeData;
+import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism;
+import io.smallrye.mutiny.Uni;
+import io.vertx.ext.web.RoutingContext;
+import jakarta.enterprise.context.ApplicationScoped;
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Optional;
+
+@ApplicationScoped
+@IfBuildProfile("proxy-auth")
+public class ProxyAuthenticationMechanism implements HttpAuthenticationMechanism {
+ private static final Logger logger = LoggerFactory.getLogger(ProxyAuthenticationMechanism.class);
+
+ @ConfigProperty(name = "calm.security.proxy.username-header", defaultValue = "Remote-User")
+ String usernameHeader;
+
+ @Override
+ public Uni authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) {
+ String username = context.request().getHeader(usernameHeader);
+ if (StringUtil.isNullOrEmpty(username)) {
+ logger.error("REJECTING request with missing proxy authentication header {}. Path: {}", usernameHeader, context.request().path());
+ return Uni.createFrom().optional(Optional.empty());
+ }
+ logger.info("Authenticating user {} via proxy authentication header {}", username, usernameHeader);
+ return identityProviderManager.authenticate(new TrustedAuthenticationRequest(username));
+ }
+
+ @Override
+ public Uni getChallenge(RoutingContext context) {
+ return Uni.createFrom().item(
+ new ChallengeData(HttpResponseStatus.UNAUTHORIZED.code(), null, null));
+ }
+}
diff --git a/calm-hub/src/main/java/org/finos/calm/security/ProxyIdentityProvider.java b/calm-hub/src/main/java/org/finos/calm/security/ProxyIdentityProvider.java
new file mode 100644
index 000000000..8baa71f3d
--- /dev/null
+++ b/calm-hub/src/main/java/org/finos/calm/security/ProxyIdentityProvider.java
@@ -0,0 +1,32 @@
+package org.finos.calm.security;
+
+import io.quarkus.security.identity.AuthenticationRequestContext;
+import io.quarkus.security.identity.IdentityProvider;
+import io.quarkus.security.identity.SecurityIdentity;
+import io.quarkus.security.identity.request.TrustedAuthenticationRequest;
+import io.quarkus.security.runtime.QuarkusPrincipal;
+import io.quarkus.security.runtime.QuarkusSecurityIdentity;
+import io.smallrye.mutiny.Uni;
+import jakarta.enterprise.context.ApplicationScoped;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@ApplicationScoped
+public class ProxyIdentityProvider implements IdentityProvider {
+ Logger logger = LoggerFactory.getLogger(ProxyIdentityProvider.class);
+
+ @Override
+ public Class getRequestType() {
+ return TrustedAuthenticationRequest.class;
+ }
+
+ @Override
+ public Uni authenticate(TrustedAuthenticationRequest request,
+ AuthenticationRequestContext context) {
+ logger.debug("Trusting principal [{}] — no external IDP configured for this mode.", request.getPrincipal());
+ return Uni.createFrom().item(QuarkusSecurityIdentity.builder()
+ .setPrincipal(new QuarkusPrincipal(request.getPrincipal()))
+ .setAnonymous(false)
+ .build());
+ }
+}
diff --git a/calm-hub/src/main/java/org/finos/calm/security/UserAccessValidator.java b/calm-hub/src/main/java/org/finos/calm/security/UserAccessValidator.java
index 900ff07bd..4bad37820 100644
--- a/calm-hub/src/main/java/org/finos/calm/security/UserAccessValidator.java
+++ b/calm-hub/src/main/java/org/finos/calm/security/UserAccessValidator.java
@@ -1,6 +1,5 @@
package org.finos.calm.security;
-import io.netty.util.internal.StringUtil;
import io.quarkus.arc.profile.IfBuildProfile;
import jakarta.enterprise.context.ApplicationScoped;
import org.finos.calm.domain.UserAccess;
@@ -13,78 +12,20 @@
import java.util.Set;
import java.util.stream.Collectors;
-/**
- * Validates whether a user is authorized to access a particular resource based on
- * their assigned permissions and namespaces.
- *
- * This validator is active only when the 'secure' profile is enabled.
- */
@ApplicationScoped
-@IfBuildProfile("secure")
+@IfBuildProfile(anyOf = {"secure", "proxy-auth"})
public class UserAccessValidator {
private static final Logger logger = LoggerFactory.getLogger(UserAccessValidator.class);
- private static final String READ_ACTION = "read";
- private static final String WRITE_ACTION = "write";
-
private final UserAccessStore userAccessStore;
- /**
- * Constructs a new UserAccessValidator with the provided UserAccessStore.
- *
- * @param userAccessStore the store used to retrieve user access permissions
- */
public UserAccessValidator(UserAccessStore userAccessStore) {
this.userAccessStore = userAccessStore;
}
/**
- * Determines whether the user is authorized to perform an action based on their request attributes.
- *
- *
If the user does not have any access entries or an exception is thrown during validation,
- * access is denied and the method returns {@code false}.
- *
- * @param userRequestAttributes encapsulates the HTTP method, username, resource path, and namespace
- * @return {@code true} if the user is authorized to perform the action; {@code false} otherwise
- */
- public boolean isUserAuthorized(UserRequestAttributes userRequestAttributes) {
- String action = mapHttpMethodToPermission(userRequestAttributes.requestMethod());
- if (isDefaultAccessibleResource(userRequestAttributes)) {
- logger.debug("The GET /calm/namespaces endpoint is accessible by default to all authenticated users");
- return true;
- }
- try {
- return hasAccessForActionOnResource(userRequestAttributes, action);
- } catch (UserAccessNotFoundException ex) {
- logger.error("No access permissions assigned to the user: [{}]", userRequestAttributes.username(), ex);
- return false;
- }
- }
-
- /**
- * Determines whether the user has sufficient access to perform the specified action
- * on a resource, based on the user's access grants, request path, and namespace.
- *
- * @param requestAttributes the user request attributes, including username, request path, and namespace
- * @param action the action the user is attempting to perform (e.g., "read", "write".)
- * @return true if the user has valid access for the action on the requested resource, false otherwise
- * @throws UserAccessNotFoundException if the user has no associated access records in the system
- */
- private boolean hasAccessForActionOnResource(UserRequestAttributes requestAttributes, String action) throws UserAccessNotFoundException {
- List userAccesses = userAccessStore.getUserAccessForUsername(requestAttributes.username());
- return userAccesses.stream().anyMatch(userAccess -> {
- boolean resourceMatches = (UserAccess.ResourceType.all == userAccess.getResourceType())
- || requestAttributes.path().contains(userAccess.getResourceType().name());
- boolean permissionSufficient = permissionAllows(userAccess.getPermission(), action);
- boolean namespaceMatches = !StringUtil.isNullOrEmpty(requestAttributes.namespace())
- && requestAttributes.namespace().equals(userAccess.getNamespace());
- return resourceMatches && permissionSufficient && namespaceMatches;
- });
- }
-
- /**
- * Returns the set of namespaces that the given user has read access to,
+ * Returns the set of namespaces the given user has read access to,
* based on their access grants. Both read and write permissions grant read access.
*
* @param username the username to check access for
@@ -101,45 +42,4 @@ public Set getReadableNamespaces(String username) {
return Set.of();
}
}
-
- /**
- * Checks whether the request targets a default-accessible endpoint.
- *
- * @param userRequestAttributes the attributes of the incoming user request
- * @return true if the endpoint is accessible by default, false otherwise
- */
- private boolean isDefaultAccessibleResource(UserRequestAttributes userRequestAttributes) {
- //TODO: How to protect GET - /calm/namespaces endpoint, by maintaining namespace specific user grants.
- String path = userRequestAttributes.path();
- String method = userRequestAttributes.requestMethod();
- return "get".equalsIgnoreCase(method) &&
- ("/calm/namespaces".equals(path) || "/calm/search".equals(path));
- }
-
- /**
- * Maps HTTP methods to access permissions.
- *
- * @param method the HTTP method
- * @return "write" for modifying methods, "read" otherwise
- */
- private String mapHttpMethodToPermission(String method) {
- return switch (method) {
- case "POST", "PUT", "PATCH", "DELETE" -> WRITE_ACTION;
- default -> READ_ACTION;
- };
- }
-
- /**
- * Checks whether the user's permission level allows the requested action.
- *
- * @param userPermission the user's assigned permission
- * @param requestedAction the action the user is attempting to perform
- * @return true if the permission is sufficient, false otherwise
- */
- private boolean permissionAllows(UserAccess.Permission userPermission, String requestedAction) {
- return switch (userPermission) {
- case write -> WRITE_ACTION.equals(requestedAction) || READ_ACTION.equals(requestedAction);
- case read -> READ_ACTION.equals(requestedAction);
- };
- }
-}
\ No newline at end of file
+}
diff --git a/calm-hub/src/main/java/org/finos/calm/security/UserRequestAttributes.java b/calm-hub/src/main/java/org/finos/calm/security/UserRequestAttributes.java
deleted file mode 100644
index 99002f978..000000000
--- a/calm-hub/src/main/java/org/finos/calm/security/UserRequestAttributes.java
+++ /dev/null
@@ -1,5 +0,0 @@
-package org.finos.calm.security;
-
-public record UserRequestAttributes(String requestMethod, String username, String path, String namespace) {
-
-}
diff --git a/calm-hub/src/main/java/org/finos/calm/store/UserAccessStore.java b/calm-hub/src/main/java/org/finos/calm/store/UserAccessStore.java
index 027957b83..cf862e805 100644
--- a/calm-hub/src/main/java/org/finos/calm/store/UserAccessStore.java
+++ b/calm-hub/src/main/java/org/finos/calm/store/UserAccessStore.java
@@ -6,6 +6,7 @@
import java.util.List;
+
/**
* Interface for managing user-access grants in the CALM system.
* Provides methods to retrieve and create user permissions through admin APIs.
@@ -45,4 +46,29 @@ public interface UserAccessStore {
* @return a list of UserAccess details
*/
UserAccess getUserAccessForNamespaceAndId(String namespace, Integer userAccessId) throws NamespaceNotFoundException, UserAccessNotFoundException;
+
+ /**
+ * Store a new domain-scoped UserAccess grant.
+ *
+ * @param userAccess the UserAccess details to create.
+ * @return the created UserAccess object.
+ */
+ UserAccess createUserAccessForDomain(UserAccess userAccess);
+
+ /**
+ * Retrieves all UserAccess grants for a given domain.
+ *
+ * @param domain the domain name to fetch associated UserAccess records.
+ * @return a list of UserAccess details
+ */
+ List getUserAccessForDomain(String domain) throws UserAccessNotFoundException;
+
+ /**
+ * Retrieve a specific domain-scoped UserAccess record by domain and id.
+ *
+ * @param domain the domain name.
+ * @param userAccessId the sequence number of a UserAccess record.
+ * @return the UserAccess record
+ */
+ UserAccess getUserAccessForDomainAndId(String domain, Integer userAccessId) throws UserAccessNotFoundException;
}
diff --git a/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoUserAccessStore.java b/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoUserAccessStore.java
index 447a48192..7880ada58 100644
--- a/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoUserAccessStore.java
+++ b/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoUserAccessStore.java
@@ -46,23 +46,43 @@ public UserAccess createUserAccessForNamespace(UserAccess userAccess)
Document userAccessDoc = new Document("username", userAccess.getUsername())
.append("permission", userAccess.getPermission().name())
.append("namespace", userAccess.getNamespace())
- .append("resourceType", userAccess.getResourceType().name())
.append("createdAt", userAccess.getCreationDateTime())
.append("lastUpdated", userAccess.getUpdateDateTime())
.append("userAccessId", userAccessId);
userAccessCollection.insertOne(userAccessDoc);
- log.info("UserAccess has been created for namespace: {}, resource: {}, permission: {}, username: {}",
- userAccess.getNamespace(), userAccess.getResourceType(), userAccess.getPermission(), userAccess.getUsername());
+ log.info("UserAccess has been created for namespace: {}, permission: {}, username: {}",
+ userAccess.getNamespace(), userAccess.getPermission(), userAccess.getUsername());
- UserAccess persistedUserAccess = new UserAccess.UserAccessBuilder()
+ return new UserAccess.UserAccessBuilder()
.setUserAccessId(userAccessId)
- .setResourceType(userAccess.getResourceType())
.setNamespace(userAccess.getNamespace())
.setPermission(userAccess.getPermission())
.setUsername(userAccess.getUsername())
.build();
- return persistedUserAccess;
+ }
+
+ @Override
+ public UserAccess createUserAccessForDomain(UserAccess userAccess) {
+ log.info("User-access details: {}", userAccess);
+ int userAccessId = counterStore.getNextUserAccessSequenceValue();
+ Document userAccessDoc = new Document("username", userAccess.getUsername())
+ .append("permission", userAccess.getPermission().name())
+ .append("domain", userAccess.getDomain())
+ .append("createdAt", userAccess.getCreationDateTime())
+ .append("lastUpdated", userAccess.getUpdateDateTime())
+ .append("userAccessId", userAccessId);
+
+ userAccessCollection.insertOne(userAccessDoc);
+ log.info("UserAccess has been created for domain: {}, permission: {}, username: {}",
+ userAccess.getDomain(), userAccess.getPermission(), userAccess.getUsername());
+
+ return new UserAccess.UserAccessBuilder()
+ .setUserAccessId(userAccessId)
+ .setDomain(userAccess.getDomain())
+ .setPermission(userAccess.getPermission())
+ .setUsername(userAccess.getUsername())
+ .build();
}
@Override
@@ -71,16 +91,7 @@ public List getUserAccessForUsername(String username)
List userAccessList = new ArrayList<>();
for (Document doc : userAccessCollection.find(Filters.eq("username", username))) {
- String namespace = doc.getString("namespace");
-
- UserAccess userAccess = new UserAccess.UserAccessBuilder()
- .setUsername(doc.getString("username"))
- .setPermission(UserAccess.Permission.valueOf(doc.getString("permission")))
- .setNamespace(namespace)
- .setResourceType(UserAccess.ResourceType.valueOf(doc.getString("resourceType")))
- .setUserAccessId(doc.getInteger("userAccessId"))
- .build();
- userAccessList.add(userAccess);
+ userAccessList.add(buildFromDocument(doc));
}
if (userAccessList.isEmpty()) {
@@ -98,14 +109,7 @@ public List getUserAccessForNamespace(String namespace)
}
List userAccessList = new ArrayList<>();
for (Document doc : userAccessCollection.find(Filters.eq("namespace", namespace))) {
- UserAccess userAccess = new UserAccess.UserAccessBuilder()
- .setUsername(doc.getString("username"))
- .setPermission(UserAccess.Permission.valueOf(doc.getString("permission")))
- .setNamespace(namespace)
- .setResourceType(UserAccess.ResourceType.valueOf(doc.getString("resourceType")))
- .setUserAccessId(doc.getInteger("userAccessId"))
- .build();
- userAccessList.add(userAccess);
+ userAccessList.add(buildFromDocument(doc));
}
if (userAccessList.isEmpty()) {
@@ -122,20 +126,54 @@ public UserAccess getUserAccessForNamespaceAndId(String namespace, Integer userA
throw new NamespaceNotFoundException();
}
- Document document = userAccessCollection.find(Filters.and(Filters.eq("namespace", namespace),
+ Document document = userAccessCollection.find(Filters.and(
+ Filters.eq("namespace", namespace),
+ Filters.eq("userAccessId", userAccessId)))
+ .first();
+
+ if (document == null) {
+ throw new UserAccessNotFoundException();
+ }
+ return buildFromDocument(document);
+ }
+
+ @Override
+ public List getUserAccessForDomain(String domain)
+ throws UserAccessNotFoundException {
+
+ List userAccessList = new ArrayList<>();
+ for (Document doc : userAccessCollection.find(Filters.eq("domain", domain))) {
+ userAccessList.add(buildFromDocument(doc));
+ }
+
+ if (userAccessList.isEmpty()) {
+ throw new UserAccessNotFoundException();
+ }
+ return userAccessList;
+ }
+
+ @Override
+ public UserAccess getUserAccessForDomainAndId(String domain, Integer userAccessId)
+ throws UserAccessNotFoundException {
+
+ Document document = userAccessCollection.find(Filters.and(
+ Filters.eq("domain", domain),
Filters.eq("userAccessId", userAccessId)))
.first();
- if (null == document) {
+ if (document == null) {
throw new UserAccessNotFoundException();
- } else {
- return new UserAccess.UserAccessBuilder()
- .setUsername(document.getString("username"))
- .setPermission(UserAccess.Permission.valueOf(document.getString("permission")))
- .setNamespace(namespace)
- .setResourceType(UserAccess.ResourceType.valueOf(document.getString("resourceType")))
- .setUserAccessId(document.getInteger("userAccessId"))
- .build();
}
+ return buildFromDocument(document);
+ }
+
+ private UserAccess buildFromDocument(Document doc) {
+ return new UserAccess.UserAccessBuilder()
+ .setUsername(doc.getString("username"))
+ .setPermission(UserAccess.Permission.valueOf(doc.getString("permission")))
+ .setNamespace(doc.getString("namespace"))
+ .setDomain(doc.getString("domain"))
+ .setUserAccessId(doc.getInteger("userAccessId"))
+ .build();
}
}
diff --git a/calm-hub/src/main/java/org/finos/calm/store/nitrite/NitriteUserAccessStore.java b/calm-hub/src/main/java/org/finos/calm/store/nitrite/NitriteUserAccessStore.java
index 711b65514..f7d739039 100644
--- a/calm-hub/src/main/java/org/finos/calm/store/nitrite/NitriteUserAccessStore.java
+++ b/calm-hub/src/main/java/org/finos/calm/store/nitrite/NitriteUserAccessStore.java
@@ -36,8 +36,8 @@ public class NitriteUserAccessStore implements UserAccessStore {
private static final String COLLECTION_NAME = "userAccess";
private static final String USERNAME_FIELD = "username";
private static final String NAMESPACE_FIELD = "namespace";
+ private static final String DOMAIN_FIELD = "domain";
private static final String PERMISSION_FIELD = "permission";
- private static final String RESOURCE_TYPE_FIELD = "resourceType";
private static final String USER_ACCESS_ID_FIELD = "userAccessId";
private static final String CREATED_AT_FIELD = "createdAt";
private static final String LAST_UPDATED_FIELD = "lastUpdated";
@@ -73,19 +73,17 @@ public UserAccess createUserAccessForNamespace(UserAccess userAccess) throws Nam
.put(USERNAME_FIELD, userAccess.getUsername())
.put(PERMISSION_FIELD, userAccess.getPermission().name())
.put(NAMESPACE_FIELD, userAccess.getNamespace())
- .put(RESOURCE_TYPE_FIELD, userAccess.getResourceType().name())
.put(CREATED_AT_FIELD, userAccess.getCreationDateTime())
.put(LAST_UPDATED_FIELD, userAccess.getUpdateDateTime())
.put(USER_ACCESS_ID_FIELD, userAccessId);
userAccessCollection.insert(userAccessDoc);
- LOG.info("UserAccess has been created for namespace: {}, resource: {}, permission: {}, username: {}",
- userAccess.getNamespace(), userAccess.getResourceType(), userAccess.getPermission(), userAccess.getUsername());
+ LOG.info("UserAccess has been created for namespace: {}, permission: {}, username: {}",
+ userAccess.getNamespace(), userAccess.getPermission(), userAccess.getUsername());
return new UserAccess.UserAccessBuilder()
.setUserAccessId(userAccessId)
- .setResourceType(userAccess.getResourceType())
.setNamespace(userAccess.getNamespace())
.setPermission(userAccess.getPermission())
.setUsername(userAccess.getUsername())
@@ -151,12 +149,71 @@ public UserAccess getUserAccessForNamespaceAndId(String namespace, Integer userA
return buildUserAccessFromDocument(document);
}
+ @Override
+ public UserAccess createUserAccessForDomain(UserAccess userAccess) {
+ LOG.info("User-access details: {}", userAccess);
+
+ lock.lock();
+ try {
+ int userAccessId = counterStore.getNextUserAccessSequenceValue();
+
+ Document userAccessDoc = Document.createDocument()
+ .put(USERNAME_FIELD, userAccess.getUsername())
+ .put(PERMISSION_FIELD, userAccess.getPermission().name())
+ .put(DOMAIN_FIELD, userAccess.getDomain())
+ .put(CREATED_AT_FIELD, userAccess.getCreationDateTime())
+ .put(LAST_UPDATED_FIELD, userAccess.getUpdateDateTime())
+ .put(USER_ACCESS_ID_FIELD, userAccessId);
+
+ userAccessCollection.insert(userAccessDoc);
+
+ LOG.info("UserAccess has been created for domain: {}, permission: {}, username: {}",
+ userAccess.getDomain(), userAccess.getPermission(), userAccess.getUsername());
+
+ return new UserAccess.UserAccessBuilder()
+ .setUserAccessId(userAccessId)
+ .setDomain(userAccess.getDomain())
+ .setPermission(userAccess.getPermission())
+ .setUsername(userAccess.getUsername())
+ .build();
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ @Override
+ public List getUserAccessForDomain(String domain) throws UserAccessNotFoundException {
+ Filter filter = where(DOMAIN_FIELD).eq(domain);
+ List userAccessList = new ArrayList<>();
+
+ for (Document doc : userAccessCollection.find(filter)) {
+ userAccessList.add(buildUserAccessFromDocument(doc));
+ }
+
+ if (userAccessList.isEmpty()) {
+ throw new UserAccessNotFoundException();
+ }
+ return userAccessList;
+ }
+
+ @Override
+ public UserAccess getUserAccessForDomainAndId(String domain, Integer userAccessId) throws UserAccessNotFoundException {
+ Filter filter = where(DOMAIN_FIELD).eq(domain).and(where(USER_ACCESS_ID_FIELD).eq(userAccessId));
+ Document document = userAccessCollection.find(filter).firstOrNull();
+
+ if (document == null) {
+ throw new UserAccessNotFoundException();
+ }
+
+ return buildUserAccessFromDocument(document);
+ }
+
private UserAccess buildUserAccessFromDocument(Document doc) {
return new UserAccess.UserAccessBuilder()
.setUsername(doc.get(USERNAME_FIELD, String.class))
.setPermission(UserAccess.Permission.valueOf(doc.get(PERMISSION_FIELD, String.class)))
.setNamespace(doc.get(NAMESPACE_FIELD, String.class))
- .setResourceType(UserAccess.ResourceType.valueOf(doc.get(RESOURCE_TYPE_FIELD, String.class)))
+ .setDomain(doc.get(DOMAIN_FIELD, String.class))
.setUserAccessId(doc.get(USER_ACCESS_ID_FIELD, Integer.class))
.build();
}
diff --git a/calm-hub/src/main/resources/application-proxy-auth.properties b/calm-hub/src/main/resources/application-proxy-auth.properties
new file mode 100644
index 000000000..2779207ea
--- /dev/null
+++ b/calm-hub/src/main/resources/application-proxy-auth.properties
@@ -0,0 +1,5 @@
+quarkus.oidc.tenant-enabled=false
+quarkus.oidc.enabled=false
+calm.hub.no.auth.enabled=false
+# Header injected by the upstream proxy carrying the authenticated username (default: Remote-User)
+#calm.security.proxy.username-header=Remote-User
\ No newline at end of file
diff --git a/calm-hub/src/main/resources/application-secure.properties b/calm-hub/src/main/resources/application-secure.properties
index 8a393fe7a..509b82742 100644
--- a/calm-hub/src/main/resources/application-secure.properties
+++ b/calm-hub/src/main/resources/application-secure.properties
@@ -16,4 +16,5 @@ quarkus.oidc.tls.verification=none
quarkus.oidc.auth-server-url=https://calm-hub.finos.org:9443/realms/calm-hub-realm
quarkus.oidc.client-id=calm-hub-producer-app
quarkus.oidc.token.audience=calm-hub-producer-app
-quarkus.oidc.tenant-enabled=true
\ No newline at end of file
+quarkus.oidc.tenant-enabled=true
+calm.hub.no.auth.enabled=false
\ No newline at end of file
diff --git a/calm-hub/src/main/resources/application.properties b/calm-hub/src/main/resources/application.properties
index 11a845776..3b444484b 100644
--- a/calm-hub/src/main/resources/application.properties
+++ b/calm-hub/src/main/resources/application.properties
@@ -33,6 +33,10 @@ quarkus.oidc.enabled=true
quarkus.keycloak.devservices.enabled=false
quarkus.oidc.tenant-enabled=false
+# Default profile is open — all requests are granted a non-anonymous identity and all
+# permission checks pass. Set to false in secure/proxy-auth profiles.
+calm.hub.no.auth.enabled=true
+
#quarkus.http.port=8081
# Security response headers — applied to all routes
@@ -47,4 +51,6 @@ quarkus.http.filter.security.header."X-Frame-Options"=DENY
# environment variable CALM_MCP_ENABLED=true to enable all MCP tools.
calm.mcp.enabled=false
# Log every JSON-RPC message in dev mode for easier debugging of MCP clients.
-%dev.quarkus.mcp.server.traffic-logging=true
\ No newline at end of file
+%dev.quarkus.mcp.server.traffic-logging=true
+quarkus.log.category."org.finos.calm".level=DEBUG
+quarkus.log.category."org.mongodb.driver".level=OFF
\ No newline at end of file
diff --git a/calm-hub/src/test/java/org/finos/calm/domain/TestUserAccessShould.java b/calm-hub/src/test/java/org/finos/calm/domain/TestUserAccessShould.java
index ec142311a..806f81c8c 100644
--- a/calm-hub/src/test/java/org/finos/calm/domain/TestUserAccessShould.java
+++ b/calm-hub/src/test/java/org/finos/calm/domain/TestUserAccessShould.java
@@ -2,9 +2,9 @@
import org.junit.jupiter.api.Test;
-import static org.hamcrest.Matchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
public class TestUserAccessShould {
@@ -13,12 +13,10 @@ void return_built_instance_values() {
Integer expectedUserAccessId = 100;
String expectedNamespace = "finos";
String expectedUsername = "test_user";
- UserAccess.ResourceType expectedResourceType = UserAccess.ResourceType.patterns;
UserAccess.Permission expectedPermissionType = UserAccess.Permission.read;
UserAccess actual = new UserAccess.UserAccessBuilder()
.setUserAccessId(100)
- .setResourceType(UserAccess.ResourceType.patterns)
.setNamespace("finos")
.setUsername("test_user")
.setPermission(UserAccess.Permission.read)
@@ -27,16 +25,14 @@ void return_built_instance_values() {
assertThat(actual.getUserAccessId(), equalTo(expectedUserAccessId));
assertThat(actual.getUsername(), equalTo(expectedUsername));
assertThat(actual.getNamespace(), equalTo(expectedNamespace));
- assertThat(actual.getResourceType(), equalTo(expectedResourceType));
assertThat(actual.getPermission(), equalTo(expectedPermissionType));
}
-
+
@Test
void return_true_for_same_user_access_instances() {
UserAccess userAccess1 = new UserAccess.UserAccessBuilder()
.setUserAccessId(100)
- .setResourceType(UserAccess.ResourceType.patterns)
.setNamespace("finos")
.setUsername("test_user")
.setPermission(UserAccess.Permission.read)
@@ -44,7 +40,6 @@ void return_true_for_same_user_access_instances() {
UserAccess userAccess2 = new UserAccess.UserAccessBuilder()
.setUserAccessId(100)
- .setResourceType(UserAccess.ResourceType.patterns)
.setNamespace("finos")
.setUsername("test_user")
.setPermission(UserAccess.Permission.read)
@@ -58,7 +53,6 @@ void return_false_for_different_user_access_instances() {
UserAccess userAccess1 = new UserAccess.UserAccessBuilder()
.setUserAccessId(100)
- .setResourceType(UserAccess.ResourceType.patterns)
.setNamespace("finos")
.setUsername("test_user1")
.setPermission(UserAccess.Permission.read)
@@ -66,7 +60,6 @@ void return_false_for_different_user_access_instances() {
UserAccess userAccess2 = new UserAccess.UserAccessBuilder()
.setUserAccessId(101)
- .setResourceType(UserAccess.ResourceType.patterns)
.setNamespace("finos")
.setUsername("test_user2")
.setPermission(UserAccess.Permission.read)
diff --git a/calm-hub/src/test/java/org/finos/calm/resources/TestAdrResourceShould.java b/calm-hub/src/test/java/org/finos/calm/resources/TestAdrResourceShould.java
index 114e54ce8..b0159aff9 100644
--- a/calm-hub/src/test/java/org/finos/calm/resources/TestAdrResourceShould.java
+++ b/calm-hub/src/test/java/org/finos/calm/resources/TestAdrResourceShould.java
@@ -2,15 +2,12 @@
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
+import io.quarkus.test.security.TestSecurity;
import org.finos.calm.domain.adr.Adr;
import org.finos.calm.domain.adr.AdrMeta;
import org.finos.calm.domain.adr.NamespaceAdrSummary;
import org.finos.calm.domain.adr.Status;
-import org.finos.calm.domain.exception.AdrNotFoundException;
-import org.finos.calm.domain.exception.AdrParseException;
-import org.finos.calm.domain.exception.AdrPersistenceException;
-import org.finos.calm.domain.exception.AdrRevisionNotFoundException;
-import org.finos.calm.domain.exception.NamespaceNotFoundException;
+import org.finos.calm.domain.exception.*;
import org.finos.calm.store.AdrStore;
import org.hamcrest.Matcher;
import org.junit.jupiter.api.Test;
@@ -26,18 +23,13 @@
import java.util.stream.Stream;
import static io.restassured.RestAssured.given;
-import static org.hamcrest.Matchers.containsString;
-import static org.hamcrest.Matchers.equalTo;
-import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.is;
-
+import static org.hamcrest.Matchers.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.*;
+@TestSecurity(authorizationEnabled = false)
@QuarkusTest
@ExtendWith(MockitoExtension.class)
public class TestAdrResourceShould {
diff --git a/calm-hub/src/test/java/org/finos/calm/resources/TestArchitectureResourcePutEnabledShould.java b/calm-hub/src/test/java/org/finos/calm/resources/TestArchitectureResourcePutEnabledShould.java
index e76cf5907..94ffe6341 100644
--- a/calm-hub/src/test/java/org/finos/calm/resources/TestArchitectureResourcePutEnabledShould.java
+++ b/calm-hub/src/test/java/org/finos/calm/resources/TestArchitectureResourcePutEnabledShould.java
@@ -1,11 +1,11 @@
package org.finos.calm.resources;
import com.fasterxml.jackson.core.JsonProcessingException;
-import static org.finos.calm.resources.ResourceValidationConstants.NAMESPACE_MESSAGE;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.TestProfile;
+import io.quarkus.test.security.TestSecurity;
import org.finos.calm.domain.Architecture;
import org.finos.calm.domain.architecture.ArchitectureRequest;
import org.finos.calm.domain.exception.ArchitectureNotFoundException;
@@ -22,12 +22,12 @@
import java.util.stream.Stream;
import static io.restassured.RestAssured.given;
+import static org.finos.calm.resources.ResourceValidationConstants.NAMESPACE_MESSAGE;
import static org.hamcrest.Matchers.containsString;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.*;
+@TestSecurity(authorizationEnabled = false)
@QuarkusTest
@ExtendWith(MockitoExtension.class)
@TestProfile(AllowPutProfile.class)
diff --git a/calm-hub/src/test/java/org/finos/calm/resources/TestArchitectureResourceShould.java b/calm-hub/src/test/java/org/finos/calm/resources/TestArchitectureResourceShould.java
index 78249267c..54e3ba40b 100644
--- a/calm-hub/src/test/java/org/finos/calm/resources/TestArchitectureResourceShould.java
+++ b/calm-hub/src/test/java/org/finos/calm/resources/TestArchitectureResourceShould.java
@@ -4,6 +4,7 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
+import io.quarkus.test.security.TestSecurity;
import org.bson.json.JsonParseException;
import org.finos.calm.domain.Architecture;
import org.finos.calm.domain.architecture.ArchitectureRequest;
@@ -21,7 +22,6 @@
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.junit.jupiter.MockitoExtension;
-import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
@@ -31,11 +31,9 @@
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.Mockito.any;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.*;
+@TestSecurity(authorizationEnabled = false)
@QuarkusTest
@ExtendWith(MockitoExtension.class)
public class TestArchitectureResourceShould {
diff --git a/calm-hub/src/test/java/org/finos/calm/resources/TestControlResourceShould.java b/calm-hub/src/test/java/org/finos/calm/resources/TestControlResourceShould.java
index a71d091e4..8daeffb23 100644
--- a/calm-hub/src/test/java/org/finos/calm/resources/TestControlResourceShould.java
+++ b/calm-hub/src/test/java/org/finos/calm/resources/TestControlResourceShould.java
@@ -2,17 +2,12 @@
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
+import io.quarkus.test.security.TestSecurity;
import org.bson.json.JsonParseException;
import org.finos.calm.domain.controls.ControlDetail;
import org.finos.calm.domain.controls.CreateControlConfiguration;
import org.finos.calm.domain.controls.CreateControlRequirement;
-import org.finos.calm.domain.exception.ControlConfigurationNotFoundException;
-import org.finos.calm.domain.exception.ControlConfigurationVersionExistsException;
-import org.finos.calm.domain.exception.ControlConfigurationVersionNotFoundException;
-import org.finos.calm.domain.exception.ControlNotFoundException;
-import org.finos.calm.domain.exception.ControlRequirementVersionExistsException;
-import org.finos.calm.domain.exception.ControlRequirementVersionNotFoundException;
-import org.finos.calm.domain.exception.DomainNotFoundException;
+import org.finos.calm.domain.exception.*;
import org.finos.calm.store.ControlStore;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -29,14 +24,10 @@
import static org.finos.calm.resources.ResourceValidationConstants.VERSION_MESSAGE;
import static org.hamcrest.Matchers.*;
import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.doThrow;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+@TestSecurity(authorizationEnabled = false)
@QuarkusTest
@ExtendWith(MockitoExtension.class)
public class TestControlResourceShould {
diff --git a/calm-hub/src/test/java/org/finos/calm/resources/TestCoreSchemaResourceShould.java b/calm-hub/src/test/java/org/finos/calm/resources/TestCoreSchemaResourceShould.java
index dfe42a134..e0654966c 100644
--- a/calm-hub/src/test/java/org/finos/calm/resources/TestCoreSchemaResourceShould.java
+++ b/calm-hub/src/test/java/org/finos/calm/resources/TestCoreSchemaResourceShould.java
@@ -2,6 +2,7 @@
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
+import io.quarkus.test.security.TestSecurity;
import org.finos.calm.store.CoreSchemaStore;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -12,9 +13,10 @@
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;
-import static org.mockito.Mockito.*;
import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.*;
+@TestSecurity(authorizationEnabled = false)
@QuarkusTest
@ExtendWith(MockitoExtension.class)
public class TestCoreSchemaResourceShould {
diff --git a/calm-hub/src/test/java/org/finos/calm/resources/TestDecoratorResourceShould.java b/calm-hub/src/test/java/org/finos/calm/resources/TestDecoratorResourceShould.java
index dd579e3e0..99d61788a 100644
--- a/calm-hub/src/test/java/org/finos/calm/resources/TestDecoratorResourceShould.java
+++ b/calm-hub/src/test/java/org/finos/calm/resources/TestDecoratorResourceShould.java
@@ -2,6 +2,7 @@
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
+import io.quarkus.test.security.TestSecurity;
import io.restassured.http.ContentType;
import org.bson.json.JsonParseException;
import org.finos.calm.domain.Decorator;
@@ -18,16 +19,11 @@
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.any;
-import static org.mockito.Mockito.doThrow;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.*;
+@TestSecurity(authorizationEnabled = false)
@QuarkusTest
@ExtendWith(MockitoExtension.class)
public class TestDecoratorResourceShould {
diff --git a/calm-hub/src/test/java/org/finos/calm/resources/TestDomainResourceShould.java b/calm-hub/src/test/java/org/finos/calm/resources/TestDomainResourceShould.java
index 86e543ea1..1a0af887d 100644
--- a/calm-hub/src/test/java/org/finos/calm/resources/TestDomainResourceShould.java
+++ b/calm-hub/src/test/java/org/finos/calm/resources/TestDomainResourceShould.java
@@ -2,6 +2,7 @@
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
+import io.quarkus.test.security.TestSecurity;
import org.finos.calm.domain.Domain;
import org.finos.calm.domain.exception.DomainAlreadyExistsException;
import org.finos.calm.store.DomainStore;
@@ -18,6 +19,7 @@
import static org.hamcrest.Matchers.is;
import static org.mockito.Mockito.*;
+@TestSecurity(authorizationEnabled = false)
@QuarkusTest
@ExtendWith(MockitoExtension.class)
public class TestDomainResourceShould {
diff --git a/calm-hub/src/test/java/org/finos/calm/resources/TestDomainUserAccessResourceShould.java b/calm-hub/src/test/java/org/finos/calm/resources/TestDomainUserAccessResourceShould.java
new file mode 100644
index 000000000..d3946f92f
--- /dev/null
+++ b/calm-hub/src/test/java/org/finos/calm/resources/TestDomainUserAccessResourceShould.java
@@ -0,0 +1,207 @@
+package org.finos.calm.resources;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.quarkus.test.InjectMock;
+import io.quarkus.test.junit.QuarkusTest;
+import io.quarkus.test.security.TestSecurity;
+import org.finos.calm.domain.UserAccess;
+import org.finos.calm.domain.exception.UserAccessNotFoundException;
+import org.finos.calm.store.UserAccessStore;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static io.restassured.RestAssured.given;
+import static org.hamcrest.Matchers.containsString;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+@TestSecurity(authorizationEnabled = false)
+@QuarkusTest
+public class TestDomainUserAccessResourceShould {
+
+ @InjectMock
+ UserAccessStore mockUserAccessStore;
+
+ private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
+ @Test
+ void return_201_created_with_location_header_when_domain_user_access_is_created() throws Exception {
+ UserAccess userAccess = new UserAccess();
+ userAccess.setDomain("payments");
+ userAccess.setPermission(UserAccess.Permission.write);
+ userAccess.setUsername("test_user");
+ String requestBody = OBJECT_MAPPER.writeValueAsString(userAccess);
+
+ UserAccess created = new UserAccess();
+ created.setDomain("payments");
+ created.setPermission(UserAccess.Permission.write);
+ created.setUsername("test_user");
+ created.setUserAccessId(201);
+ when(mockUserAccessStore.createUserAccessForDomain(any(UserAccess.class))).thenReturn(created);
+
+ given()
+ .header("Content-Type", "application/json")
+ .body(requestBody)
+ .when()
+ .post("/calm/domains/payments/user-access")
+ .then()
+ .statusCode(201)
+ .header("Location", containsString("/calm/domains/payments/user-access/201"));
+
+ verify(mockUserAccessStore, times(1)).createUserAccessForDomain(any(UserAccess.class));
+ }
+
+ @Test
+ void return_400_when_creating_user_access_with_mismatched_domain() throws Exception {
+ UserAccess userAccess = new UserAccess();
+ userAccess.setDomain("other");
+ userAccess.setPermission(UserAccess.Permission.write);
+ userAccess.setUsername("test_user");
+ String requestBody = OBJECT_MAPPER.writeValueAsString(userAccess);
+
+ given()
+ .header("Content-Type", "application/json")
+ .body(requestBody)
+ .when()
+ .post("/calm/domains/payments/user-access")
+ .then()
+ .statusCode(400)
+ .body(containsString("Bad Request"));
+
+ verify(mockUserAccessStore, times(0)).createUserAccessForDomain(any(UserAccess.class));
+ }
+
+ @Test
+ void return_500_when_store_returns_domain_that_causes_uri_syntax_error() throws Exception {
+ UserAccess invalid = new UserAccess();
+ invalid.setDomain("payments invalid");
+ invalid.setUserAccessId(1);
+ when(mockUserAccessStore.createUserAccessForDomain(any(UserAccess.class))).thenReturn(invalid);
+
+ UserAccess userAccess = new UserAccess();
+ userAccess.setDomain("payments");
+ userAccess.setPermission(UserAccess.Permission.write);
+ userAccess.setUsername("test_user");
+
+ given()
+ .header("Content-Type", "application/json")
+ .body(OBJECT_MAPPER.writeValueAsString(userAccess))
+ .when()
+ .post("/calm/domains/payments/user-access")
+ .then()
+ .statusCode(500)
+ .body(containsString("System Malfunction"));
+
+ verify(mockUserAccessStore, times(1)).createUserAccessForDomain(any(UserAccess.class));
+ }
+
+ @Test
+ void return_500_when_internal_error_occurs_during_domain_user_access_creation() throws Exception {
+ when(mockUserAccessStore.createUserAccessForDomain(any(UserAccess.class)))
+ .thenThrow(new RuntimeException("Unexpected error"));
+
+ UserAccess userAccess = new UserAccess();
+ userAccess.setDomain("payments");
+ userAccess.setPermission(UserAccess.Permission.write);
+ userAccess.setUsername("test_user");
+ String requestBody = OBJECT_MAPPER.writeValueAsString(userAccess);
+
+ given()
+ .header("Content-Type", "application/json")
+ .body(requestBody)
+ .when()
+ .post("/calm/domains/payments/user-access")
+ .then()
+ .statusCode(500);
+
+ verify(mockUserAccessStore, times(1)).createUserAccessForDomain(any(UserAccess.class));
+ }
+
+ @Test
+ void return_200_with_user_access_list_for_domain() throws Exception {
+ UserAccess ua1 = new UserAccess();
+ ua1.setUserAccessId(201);
+ ua1.setUsername("test_user1");
+ ua1.setDomain("payments");
+
+ UserAccess ua2 = new UserAccess();
+ ua2.setUserAccessId(202);
+ ua2.setUsername("test_user2");
+ ua2.setDomain("payments");
+
+ when(mockUserAccessStore.getUserAccessForDomain("payments")).thenReturn(List.of(ua1, ua2));
+
+ given()
+ .when()
+ .get("/calm/domains/payments/user-access")
+ .then()
+ .statusCode(200)
+ .body(containsString("test_user1"))
+ .body(containsString("test_user2"));
+
+ verify(mockUserAccessStore, times(1)).getUserAccessForDomain("payments");
+ }
+
+ @Test
+ void return_404_when_no_user_access_found_for_domain() throws Exception {
+ when(mockUserAccessStore.getUserAccessForDomain("payments"))
+ .thenThrow(new UserAccessNotFoundException());
+
+ given()
+ .when()
+ .get("/calm/domains/payments/user-access")
+ .then()
+ .statusCode(404)
+ .body(containsString("No access permissions found"));
+
+ verify(mockUserAccessStore, times(1)).getUserAccessForDomain("payments");
+ }
+
+ @Test
+ void return_500_when_internal_error_occurs_while_getting_domain_user_access() throws Exception {
+ when(mockUserAccessStore.getUserAccessForDomain("payments"))
+ .thenThrow(new RuntimeException("DB error"));
+
+ given()
+ .when()
+ .get("/calm/domains/payments/user-access")
+ .then()
+ .statusCode(500);
+
+ verify(mockUserAccessStore, times(1)).getUserAccessForDomain("payments");
+ }
+
+ @Test
+ void return_200_with_user_access_record_for_domain_and_id() throws Exception {
+ UserAccess ua = new UserAccess();
+ ua.setUserAccessId(201);
+ ua.setUsername("test_user");
+ ua.setDomain("payments");
+ when(mockUserAccessStore.getUserAccessForDomainAndId("payments", 201)).thenReturn(ua);
+
+ given()
+ .when()
+ .get("/calm/domains/payments/user-access/201")
+ .then()
+ .statusCode(200)
+ .body(containsString("test_user"));
+
+ verify(mockUserAccessStore, times(1)).getUserAccessForDomainAndId("payments", 201);
+ }
+
+ @Test
+ void return_404_when_user_access_for_domain_and_id_not_found() throws Exception {
+ when(mockUserAccessStore.getUserAccessForDomainAndId("payments", 999))
+ .thenThrow(new UserAccessNotFoundException());
+
+ given()
+ .when()
+ .get("/calm/domains/payments/user-access/999")
+ .then()
+ .statusCode(404)
+ .body(containsString("No access permissions found"));
+
+ verify(mockUserAccessStore, times(1)).getUserAccessForDomainAndId("payments", 999);
+ }
+}
diff --git a/calm-hub/src/test/java/org/finos/calm/resources/TestFlowResourcePutEnabledShould.java b/calm-hub/src/test/java/org/finos/calm/resources/TestFlowResourcePutEnabledShould.java
index ad85481d3..d1cfe2fae 100644
--- a/calm-hub/src/test/java/org/finos/calm/resources/TestFlowResourcePutEnabledShould.java
+++ b/calm-hub/src/test/java/org/finos/calm/resources/TestFlowResourcePutEnabledShould.java
@@ -3,6 +3,7 @@
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.TestProfile;
+import io.quarkus.test.security.TestSecurity;
import org.finos.calm.domain.Flow;
import org.finos.calm.domain.exception.FlowNotFoundException;
import org.finos.calm.domain.exception.NamespaceNotFoundException;
@@ -21,6 +22,7 @@
import static org.hamcrest.Matchers.containsString;
import static org.mockito.Mockito.when;
+@TestSecurity(authorizationEnabled = false)
@QuarkusTest
@ExtendWith(MockitoExtension.class)
@TestProfile(AllowPutProfile.class)
diff --git a/calm-hub/src/test/java/org/finos/calm/resources/TestFlowResourceShould.java b/calm-hub/src/test/java/org/finos/calm/resources/TestFlowResourceShould.java
index 28b855b2d..9c08bd38a 100644
--- a/calm-hub/src/test/java/org/finos/calm/resources/TestFlowResourceShould.java
+++ b/calm-hub/src/test/java/org/finos/calm/resources/TestFlowResourceShould.java
@@ -2,8 +2,9 @@
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
+import io.quarkus.test.security.TestSecurity;
import org.bson.json.JsonParseException;
-import org.finos.calm.domain.*;
+import org.finos.calm.domain.Flow;
import org.finos.calm.domain.exception.FlowNotFoundException;
import org.finos.calm.domain.exception.FlowVersionExistsException;
import org.finos.calm.domain.exception.FlowVersionNotFoundException;
@@ -28,6 +29,7 @@
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;
+@TestSecurity(authorizationEnabled = false)
@QuarkusTest
public class TestFlowResourceShould {
diff --git a/calm-hub/src/test/java/org/finos/calm/resources/TestFrontControllerResourceShould.java b/calm-hub/src/test/java/org/finos/calm/resources/TestFrontControllerResourceShould.java
index e07082dce..b79d64cde 100644
--- a/calm-hub/src/test/java/org/finos/calm/resources/TestFrontControllerResourceShould.java
+++ b/calm-hub/src/test/java/org/finos/calm/resources/TestFrontControllerResourceShould.java
@@ -2,6 +2,7 @@
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
+import io.quarkus.test.security.TestSecurity;
import org.finos.calm.domain.*;
import org.finos.calm.domain.exception.*;
import org.finos.calm.domain.flow.CreateFlowRequest;
@@ -22,6 +23,7 @@
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
+@TestSecurity(authorizationEnabled = false)
@QuarkusTest
@ExtendWith(MockitoExtension.class)
public class TestFrontControllerResourceShould {
diff --git a/calm-hub/src/test/java/org/finos/calm/resources/TestFrontControllerResourceWithBaseUrlShould.java b/calm-hub/src/test/java/org/finos/calm/resources/TestFrontControllerResourceWithBaseUrlShould.java
index 43dcdd6ba..41bd341a0 100644
--- a/calm-hub/src/test/java/org/finos/calm/resources/TestFrontControllerResourceWithBaseUrlShould.java
+++ b/calm-hub/src/test/java/org/finos/calm/resources/TestFrontControllerResourceWithBaseUrlShould.java
@@ -3,14 +3,11 @@
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.TestProfile;
+import io.quarkus.test.security.TestSecurity;
import org.finos.calm.domain.Pattern;
import org.finos.calm.domain.ResourceMapping;
import org.finos.calm.domain.ResourceType;
-import org.finos.calm.store.ArchitectureStore;
-import org.finos.calm.store.InterfaceStore;
-import org.finos.calm.store.PatternStore;
-import org.finos.calm.store.ResourceMappingStore;
-import org.finos.calm.store.StandardStore;
+import org.finos.calm.store.*;
import org.junit.jupiter.api.Test;
import java.util.List;
@@ -20,6 +17,7 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
+@TestSecurity(authorizationEnabled = false)
@QuarkusTest
@TestProfile(BaseUrlConfiguredProfile.class)
public class TestFrontControllerResourceWithBaseUrlShould {
diff --git a/calm-hub/src/test/java/org/finos/calm/resources/TestInterfaceResourceShould.java b/calm-hub/src/test/java/org/finos/calm/resources/TestInterfaceResourceShould.java
index da717c536..dd8c0bd55 100644
--- a/calm-hub/src/test/java/org/finos/calm/resources/TestInterfaceResourceShould.java
+++ b/calm-hub/src/test/java/org/finos/calm/resources/TestInterfaceResourceShould.java
@@ -1,20 +1,10 @@
package org.finos.calm.resources;
-import static io.restassured.RestAssured.given;
-import static org.finos.calm.resources.ResourceValidationConstants.NAMESPACE_MESSAGE;
-import static org.finos.calm.resources.ResourceValidationConstants.VERSION_MESSAGE;
-import static org.hamcrest.Matchers.containsString;
-import static org.hamcrest.Matchers.equalTo;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import java.util.List;
-import java.util.stream.Stream;
-
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.quarkus.test.InjectMock;
+import io.quarkus.test.junit.QuarkusTest;
+import io.quarkus.test.security.TestSecurity;
import org.bson.json.JsonParseException;
import org.finos.calm.domain.CalmInterface;
import org.finos.calm.domain.exception.InterfaceNotFoundException;
@@ -32,12 +22,18 @@
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.junit.jupiter.MockitoExtension;
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
+import java.util.List;
+import java.util.stream.Stream;
-import io.quarkus.test.InjectMock;
-import io.quarkus.test.junit.QuarkusTest;
+import static io.restassured.RestAssured.given;
+import static org.finos.calm.resources.ResourceValidationConstants.NAMESPACE_MESSAGE;
+import static org.finos.calm.resources.ResourceValidationConstants.VERSION_MESSAGE;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.equalTo;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+@TestSecurity(authorizationEnabled = false)
@QuarkusTest
@ExtendWith(MockitoExtension.class)
public class TestInterfaceResourceShould {
diff --git a/calm-hub/src/test/java/org/finos/calm/resources/TestNamespaceResourceShould.java b/calm-hub/src/test/java/org/finos/calm/resources/TestNamespaceResourceShould.java
index 9b1d8c2df..98594b1ff 100644
--- a/calm-hub/src/test/java/org/finos/calm/resources/TestNamespaceResourceShould.java
+++ b/calm-hub/src/test/java/org/finos/calm/resources/TestNamespaceResourceShould.java
@@ -2,6 +2,7 @@
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
+import io.quarkus.test.security.TestSecurity;
import org.finos.calm.domain.exception.NamespaceAlreadyExistsException;
import org.finos.calm.domain.namespaces.NamespaceInfo;
import org.finos.calm.store.NamespaceStore;
@@ -14,10 +15,11 @@
import static io.restassured.RestAssured.given;
import static org.finos.calm.resources.ResourceValidationConstants.NAMESPACE_MESSAGE;
-import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.equalTo;
import static org.mockito.Mockito.*;
+@TestSecurity(authorizationEnabled = false)
@QuarkusTest
@ExtendWith(MockitoExtension.class)
public class TestNamespaceResourceShould {
diff --git a/calm-hub/src/test/java/org/finos/calm/resources/TestPatternResourcePutEnabledShould.java b/calm-hub/src/test/java/org/finos/calm/resources/TestPatternResourcePutEnabledShould.java
index e82bcbff8..a8172fa8f 100644
--- a/calm-hub/src/test/java/org/finos/calm/resources/TestPatternResourcePutEnabledShould.java
+++ b/calm-hub/src/test/java/org/finos/calm/resources/TestPatternResourcePutEnabledShould.java
@@ -1,12 +1,9 @@
package org.finos.calm.resources;
-import static io.restassured.RestAssured.given;
-import static org.finos.calm.resources.ResourceValidationConstants.NAMESPACE_MESSAGE;
-import static org.hamcrest.Matchers.containsString;
-import static org.mockito.Mockito.when;
-
-import java.util.stream.Stream;
-
+import io.quarkus.test.InjectMock;
+import io.quarkus.test.junit.QuarkusTest;
+import io.quarkus.test.junit.TestProfile;
+import io.quarkus.test.security.TestSecurity;
import org.finos.calm.domain.Pattern;
import org.finos.calm.domain.exception.NamespaceNotFoundException;
import org.finos.calm.domain.exception.PatternNotFoundException;
@@ -18,10 +15,14 @@
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.junit.jupiter.MockitoExtension;
-import io.quarkus.test.InjectMock;
-import io.quarkus.test.junit.QuarkusTest;
-import io.quarkus.test.junit.TestProfile;
+import java.util.stream.Stream;
+
+import static io.restassured.RestAssured.given;
+import static org.finos.calm.resources.ResourceValidationConstants.NAMESPACE_MESSAGE;
+import static org.hamcrest.Matchers.containsString;
+import static org.mockito.Mockito.when;
+@TestSecurity(authorizationEnabled = false)
@QuarkusTest
@ExtendWith(MockitoExtension.class)
@TestProfile(AllowPutProfile.class)
diff --git a/calm-hub/src/test/java/org/finos/calm/resources/TestPatternResourceShould.java b/calm-hub/src/test/java/org/finos/calm/resources/TestPatternResourceShould.java
index 7dc959e56..4ab7a524b 100644
--- a/calm-hub/src/test/java/org/finos/calm/resources/TestPatternResourceShould.java
+++ b/calm-hub/src/test/java/org/finos/calm/resources/TestPatternResourceShould.java
@@ -2,8 +2,9 @@
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
+import io.quarkus.test.security.TestSecurity;
import org.bson.json.JsonParseException;
-import org.finos.calm.domain.*;
+import org.finos.calm.domain.Pattern;
import org.finos.calm.domain.exception.NamespaceNotFoundException;
import org.finos.calm.domain.exception.PatternNotFoundException;
import org.finos.calm.domain.exception.PatternVersionExistsException;
@@ -30,6 +31,7 @@
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;
+@TestSecurity(authorizationEnabled = false)
@QuarkusTest
@ExtendWith(MockitoExtension.class)
public class TestPatternResourceShould {
diff --git a/calm-hub/src/test/java/org/finos/calm/resources/TestSearchResourceFilteringShould.java b/calm-hub/src/test/java/org/finos/calm/resources/TestSearchResourceFilteringShould.java
index ddef6aa0d..07f30af30 100644
--- a/calm-hub/src/test/java/org/finos/calm/resources/TestSearchResourceFilteringShould.java
+++ b/calm-hub/src/test/java/org/finos/calm/resources/TestSearchResourceFilteringShould.java
@@ -77,7 +77,7 @@ void pass_resolved_readable_namespaces_to_store_in_secure_mode() {
);
when(mockSearchStore.search(eq("test"), any())).thenReturn(storeResults);
- SearchResource resource = new SearchResource(mockSearchStore, mockValidatorInstance, mockJwtInstance);
+ SearchResource resource = new SearchResource(mockSearchStore, mockValidatorInstance, mockJwtInstance, "Remote-User");
var response = resource.search("test");
assertEquals(200, response.getStatus());
@@ -95,7 +95,7 @@ void pass_empty_optional_when_validator_not_resolvable() {
when(mockSearchStore.search(eq("test"), any())).thenReturn(emptyResults());
- SearchResource resource = new SearchResource(mockSearchStore, mockValidatorInstance, mockJwtInstance);
+ SearchResource resource = new SearchResource(mockSearchStore, mockValidatorInstance, mockJwtInstance, "Remote-User");
resource.search("test");
ArgumentCaptor>> captor = namespacesCaptor();
@@ -112,7 +112,7 @@ void pass_empty_optional_when_jwt_has_no_username() {
when(mockSearchStore.search(eq("test"), any())).thenReturn(emptyResults());
- SearchResource resource = new SearchResource(mockSearchStore, mockValidatorInstance, mockJwtInstance);
+ SearchResource resource = new SearchResource(mockSearchStore, mockValidatorInstance, mockJwtInstance, "Remote-User");
resource.search("test");
ArgumentCaptor>> captor = namespacesCaptor();
@@ -131,7 +131,7 @@ void pass_empty_set_when_user_has_no_namespace_grants() {
when(mockSearchStore.search(eq("test"), any())).thenReturn(emptyResults());
- SearchResource resource = new SearchResource(mockSearchStore, mockValidatorInstance, mockJwtInstance);
+ SearchResource resource = new SearchResource(mockSearchStore, mockValidatorInstance, mockJwtInstance, "Remote-User");
resource.search("test");
ArgumentCaptor>> captor = namespacesCaptor();
diff --git a/calm-hub/src/test/java/org/finos/calm/resources/TestSearchResourceShould.java b/calm-hub/src/test/java/org/finos/calm/resources/TestSearchResourceShould.java
index b4b4ff8af..23cea9348 100644
--- a/calm-hub/src/test/java/org/finos/calm/resources/TestSearchResourceShould.java
+++ b/calm-hub/src/test/java/org/finos/calm/resources/TestSearchResourceShould.java
@@ -2,6 +2,7 @@
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
+import io.quarkus.test.security.TestSecurity;
import org.finos.calm.domain.search.GroupedSearchResults;
import org.finos.calm.domain.search.SearchResult;
import org.finos.calm.store.SearchStore;
@@ -18,15 +19,12 @@
import java.util.stream.Stream;
import static io.restassured.RestAssured.given;
-import static org.hamcrest.Matchers.containsString;
-import static org.hamcrest.Matchers.equalTo;
-import static org.hamcrest.Matchers.hasSize;
+import static org.hamcrest.Matchers.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.*;
+@TestSecurity(authorizationEnabled = false)
@QuarkusTest
@ExtendWith(MockitoExtension.class)
public class TestSearchResourceShould {
diff --git a/calm-hub/src/test/java/org/finos/calm/resources/TestStandardResourceShould.java b/calm-hub/src/test/java/org/finos/calm/resources/TestStandardResourceShould.java
index b9307ff4a..3f9687d1b 100644
--- a/calm-hub/src/test/java/org/finos/calm/resources/TestStandardResourceShould.java
+++ b/calm-hub/src/test/java/org/finos/calm/resources/TestStandardResourceShould.java
@@ -1,20 +1,10 @@
package org.finos.calm.resources;
-import static io.restassured.RestAssured.given;
-import static org.finos.calm.resources.ResourceValidationConstants.NAMESPACE_MESSAGE;
-import static org.finos.calm.resources.ResourceValidationConstants.VERSION_MESSAGE;
-import static org.hamcrest.Matchers.containsString;
-import static org.hamcrest.Matchers.equalTo;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import java.util.List;
-import java.util.stream.Stream;
-
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.quarkus.test.InjectMock;
+import io.quarkus.test.junit.QuarkusTest;
+import io.quarkus.test.security.TestSecurity;
import org.finos.calm.domain.Standard;
import org.finos.calm.domain.exception.NamespaceNotFoundException;
import org.finos.calm.domain.exception.StandardNotFoundException;
@@ -31,12 +21,18 @@
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.junit.jupiter.MockitoExtension;
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
+import java.util.List;
+import java.util.stream.Stream;
-import io.quarkus.test.InjectMock;
-import io.quarkus.test.junit.QuarkusTest;
+import static io.restassured.RestAssured.given;
+import static org.finos.calm.resources.ResourceValidationConstants.NAMESPACE_MESSAGE;
+import static org.finos.calm.resources.ResourceValidationConstants.VERSION_MESSAGE;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.equalTo;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+@TestSecurity(authorizationEnabled = false)
@QuarkusTest
@ExtendWith(MockitoExtension.class)
public class TestStandardResourceShould {
diff --git a/calm-hub/src/test/java/org/finos/calm/resources/TestTimelineResourcePutEnabledShould.java b/calm-hub/src/test/java/org/finos/calm/resources/TestTimelineResourcePutEnabledShould.java
index d93c55e73..13ff32411 100644
--- a/calm-hub/src/test/java/org/finos/calm/resources/TestTimelineResourcePutEnabledShould.java
+++ b/calm-hub/src/test/java/org/finos/calm/resources/TestTimelineResourcePutEnabledShould.java
@@ -3,6 +3,7 @@
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.TestProfile;
+import io.quarkus.test.security.TestSecurity;
import org.finos.calm.domain.exception.NamespaceNotFoundException;
import org.finos.calm.domain.exception.TimelineNotFoundException;
import org.finos.calm.domain.timeline.Timeline;
@@ -24,6 +25,7 @@
@QuarkusTest
@ExtendWith(MockitoExtension.class)
@TestProfile(AllowPutProfile.class)
+@TestSecurity(authorizationEnabled = false)
public class TestTimelineResourcePutEnabledShould {
@InjectMock
diff --git a/calm-hub/src/test/java/org/finos/calm/resources/TestTimelineResourceShould.java b/calm-hub/src/test/java/org/finos/calm/resources/TestTimelineResourceShould.java
index 16f03e4c0..4c104e82c 100644
--- a/calm-hub/src/test/java/org/finos/calm/resources/TestTimelineResourceShould.java
+++ b/calm-hub/src/test/java/org/finos/calm/resources/TestTimelineResourceShould.java
@@ -2,6 +2,7 @@
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
+import io.quarkus.test.security.TestSecurity;
import org.bson.json.JsonParseException;
import org.finos.calm.domain.exception.NamespaceNotFoundException;
import org.finos.calm.domain.exception.TimelineNotFoundException;
@@ -29,6 +30,7 @@
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;
+@TestSecurity(authorizationEnabled = false)
@QuarkusTest
public class TestTimelineResourceShould {
diff --git a/calm-hub/src/test/java/org/finos/calm/resources/TestUserAccessResourceShould.java b/calm-hub/src/test/java/org/finos/calm/resources/TestUserAccessResourceShould.java
index 3709d1599..96c03d9ab 100644
--- a/calm-hub/src/test/java/org/finos/calm/resources/TestUserAccessResourceShould.java
+++ b/calm-hub/src/test/java/org/finos/calm/resources/TestUserAccessResourceShould.java
@@ -3,18 +3,21 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
+import io.quarkus.test.security.TestSecurity;
import org.finos.calm.domain.UserAccess;
import org.finos.calm.domain.exception.NamespaceNotFoundException;
import org.finos.calm.domain.exception.UserAccessNotFoundException;
import org.finos.calm.store.UserAccessStore;
import org.junit.jupiter.api.Test;
+
import java.util.List;
import static io.restassured.RestAssured.given;
-import static org.hamcrest.Matchers.*;
+import static org.hamcrest.Matchers.containsString;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
+@TestSecurity(authorizationEnabled = false)
@QuarkusTest
public class TestUserAccessResourceShould {
@@ -29,7 +32,6 @@ void return_201_created_with_location_header_when_user_access_is_created() throw
UserAccess userAccess = new UserAccess();
userAccess.setNamespace("finos");
userAccess.setPermission(UserAccess.Permission.read);
- userAccess.setResourceType(UserAccess.ResourceType.patterns);
userAccess.setUsername("test_user");
String requestBody = OBJECT_MAPPER.writeValueAsString(userAccess);
@@ -58,7 +60,6 @@ void return_404_when_creating_user_access_with_invalid_namespace() throws Except
UserAccess userAccess = new UserAccess();
userAccess.setNamespace("invalid");
userAccess.setPermission(UserAccess.Permission.read);
- userAccess.setResourceType(UserAccess.ResourceType.all);
userAccess.setUsername("test_user");
String requestBody = OBJECT_MAPPER.writeValueAsString(userAccess);
@@ -83,7 +84,6 @@ void return_400_when_creating_user_access_with_invalid_namespace() throws Except
UserAccess userAccess = new UserAccess();
userAccess.setNamespace("invalid");
userAccess.setPermission(UserAccess.Permission.read);
- userAccess.setResourceType(UserAccess.ResourceType.all);
userAccess.setUsername("test_user");
String requestBody = OBJECT_MAPPER.writeValueAsString(userAccess);
@@ -108,7 +108,6 @@ void return_500_when_internal_error_occurs_during_user_access_creation() throws
UserAccess userAccess = new UserAccess();
userAccess.setNamespace("finos");
userAccess.setPermission(UserAccess.Permission.read);
- userAccess.setResourceType(UserAccess.ResourceType.all);
userAccess.setUsername("test_user");
String requestBody = OBJECT_MAPPER.writeValueAsString(userAccess);
diff --git a/calm-hub/src/test/java/org/finos/calm/security/TestAccessControlFilterShould.java b/calm-hub/src/test/java/org/finos/calm/security/TestAccessControlFilterShould.java
deleted file mode 100644
index 0fab452e6..000000000
--- a/calm-hub/src/test/java/org/finos/calm/security/TestAccessControlFilterShould.java
+++ /dev/null
@@ -1,122 +0,0 @@
-package org.finos.calm.security;
-
-import io.quarkus.test.junit.QuarkusTest;
-import jakarta.ws.rs.container.ContainerRequestContext;
-import jakarta.ws.rs.container.ResourceInfo;
-import jakarta.ws.rs.core.MultivaluedHashMap;
-import jakarta.ws.rs.core.MultivaluedMap;
-import jakarta.ws.rs.core.UriInfo;
-import org.apache.http.HttpStatus;
-import org.eclipse.microprofile.jwt.JsonWebToken;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-import java.lang.reflect.Method;
-import java.util.List;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.argThat;
-import static org.mockito.Mockito.*;
-
-@QuarkusTest
-public class TestAccessControlFilterShould {
-
- @Mock
- JsonWebToken jwt;
- @Mock
- ContainerRequestContext requestContext;
- @Mock
- ResourceInfo resourceInfo;
- @Mock
- UserAccessValidator userAccessValidator;
-
- private AccessControlFilter accessControlFilter;
-
- @BeforeEach
- void setup() {
- MockitoAnnotations.openMocks(this);
- accessControlFilter = new AccessControlFilter(jwt, resourceInfo, userAccessValidator);
- }
-
- @Test
- void allow_the_request_when_scopes_not_defined_on_resource() throws NoSuchMethodException {
- Method method = TestNamespaceResource.class.getMethod("getNamespacesUnsecured");
- when(resourceInfo.getResourceMethod()).thenReturn(method);
-
- accessControlFilter.filter(requestContext);
- verify(requestContext, never()).abortWith(any());
- }
-
- @Test
- void allow_the_request_when_token_scopes_matching_and_user_has_required_permissions() throws NoSuchMethodException {
- Method method = TestNamespaceResource.class.getMethod("createNamespace");
-
- when(resourceInfo.getResourceMethod()).thenReturn(method);
- when(jwt.getClaim("scope")).thenReturn("openid architectures:all");
- when(jwt.getClaim("preferred_username")).thenReturn("test");
-
- UriInfo mockUriInfo = mock(UriInfo.class);
- when(mockUriInfo.getPath()).thenReturn("/calm/namespaces");
- MultivaluedMap multivaluedMap = new MultivaluedHashMap<>();
- multivaluedMap.add("namespace", "test");
- when(mockUriInfo.getPathParameters()).thenReturn(multivaluedMap);
- when(requestContext.getUriInfo()).thenReturn(mockUriInfo);
- when(userAccessValidator.isUserAuthorized(any(UserRequestAttributes.class))).thenReturn(true);
-
- accessControlFilter.filter(requestContext);
- verify(requestContext, never()).abortWith(any());
- }
-
- @Test
- void abort_the_request_when_token_scopes_matching_and_user_has_no_required_access() throws NoSuchMethodException {
- Method method = TestNamespaceResource.class.getMethod("createNamespace");
-
- when(resourceInfo.getResourceMethod()).thenReturn(method);
- when(jwt.getClaim("scope")).thenReturn("openid architectures:all");
- when(jwt.getClaim("preferred_username")).thenReturn("test");
-
- UriInfo mockUriInfo = mock(UriInfo.class);
- when(mockUriInfo.getPath()).thenReturn("/calm/namespaces");
- MultivaluedMap multivaluedMap = new MultivaluedHashMap<>();
- multivaluedMap.add("namespace", "test");
- when(mockUriInfo.getPathParameters()).thenReturn(multivaluedMap);
- when(requestContext.getUriInfo()).thenReturn(mockUriInfo);
- when(userAccessValidator.isUserAuthorized(any(UserRequestAttributes.class)))
- .thenReturn(false);
-
- accessControlFilter.filter(requestContext);
- verify(requestContext)
- .abortWith(argThat(response -> response.getStatus() == HttpStatus.SC_FORBIDDEN));
- }
-
- @Test
- void abort_the_request_when_token_scopes_not_matching() throws NoSuchMethodException {
- Method method = TestNamespaceResource.class.getMethod("createNamespace");
- when(resourceInfo.getResourceMethod()).thenReturn(method);
- when(jwt.getClaim("scope")).thenReturn("openid architectures:read");
-
- UriInfo mockUriInfo = mock(UriInfo.class);
- when(mockUriInfo.getPath()).thenReturn("/calm/namespaces");
- MultivaluedMap multivaluedMap = new MultivaluedHashMap<>();
- multivaluedMap.add("namespace", "test");
- when(mockUriInfo.getPathParameters()).thenReturn(multivaluedMap);
- when(requestContext.getUriInfo()).thenReturn(mockUriInfo);
-
- accessControlFilter.filter(requestContext);
- verify(requestContext)
- .abortWith(argThat(response -> response.getStatus() == HttpStatus.SC_FORBIDDEN));
- }
-
- private static class TestNamespaceResource {
- @SuppressWarnings("unused")
- public List getNamespacesUnsecured() {
- return List.of("test", "dev");
- }
-
- @PermittedScopes({CalmHubScopes.ARCHITECTURES_ALL})
- public void createNamespace() {
- }
- }
-}
\ No newline at end of file
diff --git a/calm-hub/src/test/java/org/finos/calm/security/TestCalmHubPermissionCheckerShould.java b/calm-hub/src/test/java/org/finos/calm/security/TestCalmHubPermissionCheckerShould.java
new file mode 100644
index 000000000..03077e7e1
--- /dev/null
+++ b/calm-hub/src/test/java/org/finos/calm/security/TestCalmHubPermissionCheckerShould.java
@@ -0,0 +1,337 @@
+package org.finos.calm.security;
+
+import io.quarkus.security.identity.SecurityIdentity;
+import org.finos.calm.domain.UserAccess;
+import org.finos.calm.domain.exception.UserAccessNotFoundException;
+import org.finos.calm.store.UserAccessStore;
+import org.junit.jupiter.api.BeforeEach;
+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.security.Principal;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class TestCalmHubPermissionCheckerShould {
+
+ @Mock
+ UserAccessStore mockUserAccessStore;
+
+ @Mock
+ SecurityIdentity mockIdentity;
+
+ @Mock
+ Principal mockPrincipal;
+
+ CalmHubPermissionChecker checker;
+
+ @BeforeEach
+ void setUp() {
+ checker = new CalmHubPermissionChecker(mockUserAccessStore);
+ }
+
+ private void givenAuthenticatedUser(String username) throws UserAccessNotFoundException {
+ when(mockIdentity.getPrincipal()).thenReturn(mockPrincipal);
+ when(mockPrincipal.getName()).thenReturn(username);
+ }
+
+ // --- READ checks ---
+
+ @Test
+ void read_grant_allows_read() throws UserAccessNotFoundException {
+ givenAuthenticatedUser("alice");
+ UserAccess grant = new UserAccess("alice", UserAccess.Permission.read, "foo");
+ when(mockUserAccessStore.getUserAccessForUsername("alice")).thenReturn(List.of(grant));
+
+ assertTrue(checker.canRead(mockIdentity, "foo"));
+ }
+
+ @Test
+ void write_grant_allows_read() throws UserAccessNotFoundException {
+ givenAuthenticatedUser("alice");
+ UserAccess grant = new UserAccess("alice", UserAccess.Permission.write, "foo");
+ when(mockUserAccessStore.getUserAccessForUsername("alice")).thenReturn(List.of(grant));
+
+ assertTrue(checker.canRead(mockIdentity, "foo"));
+ }
+
+ @Test
+ void grant_for_different_namespace_denies_read() throws UserAccessNotFoundException {
+ givenAuthenticatedUser("alice");
+ UserAccess grant = new UserAccess("alice", UserAccess.Permission.write, "bar");
+ when(mockUserAccessStore.getUserAccessForUsername("alice")).thenReturn(List.of(grant));
+
+ assertFalse(checker.canRead(mockIdentity, "foo"));
+ }
+
+ @Test
+ void user_with_no_grants_is_denied_read() throws UserAccessNotFoundException {
+ givenAuthenticatedUser("alice");
+ when(mockUserAccessStore.getUserAccessForUsername("alice")).thenThrow(new UserAccessNotFoundException());
+
+ assertFalse(checker.canRead(mockIdentity, "foo"));
+ }
+
+ @Test
+ void any_namespace_grant_satisfies_read_regardless_of_resource_type() throws UserAccessNotFoundException {
+ givenAuthenticatedUser("alice");
+ UserAccess grant = new UserAccess("alice", UserAccess.Permission.read, "foo");
+ when(mockUserAccessStore.getUserAccessForUsername("alice")).thenReturn(List.of(grant));
+
+ assertTrue(checker.canRead(mockIdentity, "foo"));
+ }
+
+ // --- WRITE checks ---
+
+ @Test
+ void read_grant_denies_write() throws UserAccessNotFoundException {
+ givenAuthenticatedUser("alice");
+ UserAccess grant = new UserAccess("alice", UserAccess.Permission.read, "foo");
+ when(mockUserAccessStore.getUserAccessForUsername("alice")).thenReturn(List.of(grant));
+
+ assertFalse(checker.canWrite(mockIdentity, "foo"));
+ }
+
+ @Test
+ void write_grant_allows_write() throws UserAccessNotFoundException {
+ givenAuthenticatedUser("alice");
+ UserAccess grant = new UserAccess("alice", UserAccess.Permission.write, "foo");
+ when(mockUserAccessStore.getUserAccessForUsername("alice")).thenReturn(List.of(grant));
+
+ assertTrue(checker.canWrite(mockIdentity, "foo"));
+ }
+
+ @Test
+ void admin_grant_allows_read_and_write() throws UserAccessNotFoundException {
+ givenAuthenticatedUser("alice");
+ UserAccess grant = new UserAccess("alice", UserAccess.Permission.admin, "foo");
+ when(mockUserAccessStore.getUserAccessForUsername("alice")).thenReturn(List.of(grant));
+
+ assertTrue(checker.canRead(mockIdentity, "foo"));
+ assertTrue(checker.canWrite(mockIdentity, "foo"));
+ }
+
+ // --- ADMIN checks ---
+
+ @Test
+ void write_grant_denies_namespace_admin() throws UserAccessNotFoundException {
+ givenAuthenticatedUser("alice");
+ UserAccess grant = new UserAccess("alice", UserAccess.Permission.write, "foo");
+ when(mockUserAccessStore.getUserAccessForUsername("alice")).thenReturn(List.of(grant));
+
+ assertFalse(checker.allowNamespaceAdmin(mockIdentity, "foo"));
+ }
+
+ @Test
+ void admin_grant_allows_namespace_admin() throws UserAccessNotFoundException {
+ givenAuthenticatedUser("alice");
+ UserAccess grant = new UserAccess("alice", UserAccess.Permission.admin, "foo");
+ when(mockUserAccessStore.getUserAccessForUsername("alice")).thenReturn(List.of(grant));
+
+ assertTrue(checker.allowNamespaceAdmin(mockIdentity, "foo"));
+ }
+
+ // --- allowPublicRead ---
+
+ @Test
+ void public_read_disabled_denies_user_without_grants() throws UserAccessNotFoundException {
+ givenAuthenticatedUser("alice");
+ when(mockUserAccessStore.getUserAccessForUsername("alice")).thenThrow(new UserAccessNotFoundException());
+
+ assertFalse(checker.canRead(mockIdentity, "foo"));
+ }
+
+ @Test
+ void public_read_enabled_allows_any_authenticated_user_to_read() {
+ checker.allowPublicRead = true;
+
+ assertTrue(checker.canRead(mockIdentity, "foo"));
+ }
+
+ @Test
+ void public_read_enabled_does_not_grant_write_access() throws UserAccessNotFoundException {
+ checker.allowPublicRead = true;
+ givenAuthenticatedUser("alice");
+ when(mockUserAccessStore.getUserAccessForUsername("alice")).thenThrow(new UserAccessNotFoundException());
+
+ assertFalse(checker.canWrite(mockIdentity, "foo"));
+ }
+
+ @Test
+ void public_read_enabled_does_not_grant_namespace_admin_access() throws UserAccessNotFoundException {
+ checker.allowPublicRead = true;
+ givenAuthenticatedUser("alice");
+ when(mockUserAccessStore.getUserAccessForUsername("alice")).thenThrow(new UserAccessNotFoundException());
+
+ assertFalse(checker.allowNamespaceAdmin(mockIdentity, "foo"));
+ }
+
+ @Test
+ void public_read_enabled_allows_read_without_store_lookup() {
+ checker.allowPublicRead = true;
+ // No isAnonymous() stub and no store stub — confirms both are bypassed when allowPublicRead is true
+
+ assertTrue(checker.canRead(mockIdentity, "foo"));
+ }
+
+ // --- Domain READ checks ---
+
+ @Test
+ void read_grant_for_domain_allows_domain_read() throws UserAccessNotFoundException {
+ givenAuthenticatedUser("alice");
+ UserAccess grant = new UserAccess.UserAccessBuilder()
+ .setUsername("alice").setPermission(UserAccess.Permission.read).setDomain("payments").build();
+ when(mockUserAccessStore.getUserAccessForUsername("alice")).thenReturn(List.of(grant));
+
+ assertTrue(checker.canReadByDomain(mockIdentity, "payments"));
+ }
+
+ @Test
+ void write_grant_for_domain_allows_domain_read() throws UserAccessNotFoundException {
+ givenAuthenticatedUser("alice");
+ UserAccess grant = new UserAccess.UserAccessBuilder()
+ .setUsername("alice").setPermission(UserAccess.Permission.write).setDomain("payments").build();
+ when(mockUserAccessStore.getUserAccessForUsername("alice")).thenReturn(List.of(grant));
+
+ assertTrue(checker.canReadByDomain(mockIdentity, "payments"));
+ }
+
+ @Test
+ void grant_for_different_domain_denies_domain_read() throws UserAccessNotFoundException {
+ givenAuthenticatedUser("alice");
+ UserAccess grant = new UserAccess.UserAccessBuilder()
+ .setUsername("alice").setPermission(UserAccess.Permission.read).setDomain("orders").build();
+ when(mockUserAccessStore.getUserAccessForUsername("alice")).thenReturn(List.of(grant));
+
+ assertFalse(checker.canReadByDomain(mockIdentity, "payments"));
+ }
+
+ @Test
+ void namespace_grant_does_not_satisfy_domain_read() throws UserAccessNotFoundException {
+ givenAuthenticatedUser("alice");
+ UserAccess grant = new UserAccess("alice", UserAccess.Permission.read, "payments");
+ when(mockUserAccessStore.getUserAccessForUsername("alice")).thenReturn(List.of(grant));
+
+ assertFalse(checker.canReadByDomain(mockIdentity, "payments"));
+ }
+
+ @Test
+ void user_with_no_grants_is_denied_domain_read() throws UserAccessNotFoundException {
+ givenAuthenticatedUser("alice");
+ when(mockUserAccessStore.getUserAccessForUsername("alice")).thenThrow(new UserAccessNotFoundException());
+
+ assertFalse(checker.canReadByDomain(mockIdentity, "payments"));
+ }
+
+ @Test
+ void public_read_enabled_allows_domain_read_without_store_lookup() {
+ checker.allowPublicRead = true;
+
+ assertTrue(checker.canReadByDomain(mockIdentity, "payments"));
+ }
+
+ // --- Domain WRITE checks ---
+
+ @Test
+ void read_grant_for_domain_denies_domain_write() throws UserAccessNotFoundException {
+ givenAuthenticatedUser("alice");
+ UserAccess grant = new UserAccess.UserAccessBuilder()
+ .setUsername("alice").setPermission(UserAccess.Permission.read).setDomain("payments").build();
+ when(mockUserAccessStore.getUserAccessForUsername("alice")).thenReturn(List.of(grant));
+
+ assertFalse(checker.canWriteByDomain(mockIdentity, "payments"));
+ }
+
+ @Test
+ void write_grant_for_domain_allows_domain_write() throws UserAccessNotFoundException {
+ givenAuthenticatedUser("alice");
+ UserAccess grant = new UserAccess.UserAccessBuilder()
+ .setUsername("alice").setPermission(UserAccess.Permission.write).setDomain("payments").build();
+ when(mockUserAccessStore.getUserAccessForUsername("alice")).thenReturn(List.of(grant));
+
+ assertTrue(checker.canWriteByDomain(mockIdentity, "payments"));
+ }
+
+ // --- noAuthEnabled ---
+
+ @Test
+ void no_auth_mode_grants_read_without_store_lookup() {
+ checker.noAuthEnabled = true;
+
+ assertTrue(checker.canRead(mockIdentity, "foo"));
+ }
+
+ @Test
+ void no_auth_mode_grants_write_without_store_lookup() {
+ checker.noAuthEnabled = true;
+
+ assertTrue(checker.canWrite(mockIdentity, "foo"));
+ }
+
+ @Test
+ void no_auth_mode_grants_namespace_admin_without_store_lookup() {
+ checker.noAuthEnabled = true;
+
+ assertTrue(checker.allowNamespaceAdmin(mockIdentity, "foo"));
+ }
+
+ @Test
+ void no_auth_mode_grants_global_admin_without_store_lookup() {
+ checker.noAuthEnabled = true;
+
+ assertTrue(checker.hasGlobalAdmin(mockIdentity));
+ }
+
+ @Test
+ void no_auth_mode_grants_domain_read_and_write_without_store_lookup() {
+ checker.noAuthEnabled = true;
+
+ assertTrue(checker.canReadByDomain(mockIdentity, "payments"));
+ assertTrue(checker.canWriteByDomain(mockIdentity, "payments"));
+ }
+
+ // --- GLOBAL ADMIN checks ---
+
+ @Test
+ void global_admin_grant_on_global_namespace_grants_global_admin() throws UserAccessNotFoundException {
+ givenAuthenticatedUser("alice");
+ UserAccess grant = new UserAccess.UserAccessBuilder()
+ .setUsername("alice").setPermission(UserAccess.Permission.admin).setNamespace("GLOBAL").build();
+ when(mockUserAccessStore.getUserAccessForUsername("alice")).thenReturn(List.of(grant));
+
+ assertTrue(checker.hasGlobalAdmin(mockIdentity));
+ }
+
+ @Test
+ void write_grant_on_global_namespace_denies_global_admin() throws UserAccessNotFoundException {
+ givenAuthenticatedUser("alice");
+ UserAccess grant = new UserAccess.UserAccessBuilder()
+ .setUsername("alice").setPermission(UserAccess.Permission.write).setNamespace("GLOBAL").build();
+ when(mockUserAccessStore.getUserAccessForUsername("alice")).thenReturn(List.of(grant));
+
+ assertFalse(checker.hasGlobalAdmin(mockIdentity));
+ }
+
+ @Test
+ void admin_grant_on_non_global_namespace_denies_global_admin() throws UserAccessNotFoundException {
+ givenAuthenticatedUser("alice");
+ UserAccess grant = new UserAccess("alice", UserAccess.Permission.admin, "finos");
+ when(mockUserAccessStore.getUserAccessForUsername("alice")).thenReturn(List.of(grant));
+
+ assertFalse(checker.hasGlobalAdmin(mockIdentity));
+ }
+
+ @Test
+ void user_with_no_grants_is_denied_global_admin() throws UserAccessNotFoundException {
+ givenAuthenticatedUser("alice");
+ when(mockUserAccessStore.getUserAccessForUsername("alice")).thenThrow(new UserAccessNotFoundException());
+
+ assertFalse(checker.hasGlobalAdmin(mockIdentity));
+ }
+}
diff --git a/calm-hub/src/test/java/org/finos/calm/security/TestCalmHubScopesShould.java b/calm-hub/src/test/java/org/finos/calm/security/TestCalmHubScopesShould.java
index 2f02511c3..b012e0360 100644
--- a/calm-hub/src/test/java/org/finos/calm/security/TestCalmHubScopesShould.java
+++ b/calm-hub/src/test/java/org/finos/calm/security/TestCalmHubScopesShould.java
@@ -18,22 +18,22 @@ void prevent_instantiation() throws Exception {
}
@Test
- void match_architectures_read_constant() {
- assertEquals("architectures:read", CalmHubScopes.ARCHITECTURES_READ);
+ void match_read_constant() {
+ assertEquals("read", CalmHubScopes.READ);
}
@Test
- void match_architectures_all_constant() {
- assertEquals("architectures:all", CalmHubScopes.ARCHITECTURES_ALL);
+ void match_write_constant() {
+ assertEquals("write", CalmHubScopes.WRITE);
}
@Test
- void match_adrs_all_constant() {
- assertEquals("adrs:all", CalmHubScopes.ADRS_ALL);
+ void match_admin_constant() {
+ assertEquals("admin", CalmHubScopes.ADMIN);
}
@Test
- void match_adrs_read_constant() {
- assertEquals("adrs:read", CalmHubScopes.ADRS_READ);
+ void match_global_admin_constant() {
+ assertEquals("global_admin", CalmHubScopes.GLOBAL_ADMIN);
}
}
diff --git a/calm-hub/src/test/java/org/finos/calm/security/TestNoAuthAuthenticationMechanismShould.java b/calm-hub/src/test/java/org/finos/calm/security/TestNoAuthAuthenticationMechanismShould.java
new file mode 100644
index 000000000..8a4ef80d0
--- /dev/null
+++ b/calm-hub/src/test/java/org/finos/calm/security/TestNoAuthAuthenticationMechanismShould.java
@@ -0,0 +1,77 @@
+package org.finos.calm.security;
+
+import io.quarkus.security.identity.IdentityProviderManager;
+import io.quarkus.security.identity.SecurityIdentity;
+import io.quarkus.security.identity.request.TrustedAuthenticationRequest;
+import io.quarkus.vertx.http.runtime.security.ChallengeData;
+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 org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class TestNoAuthAuthenticationMechanismShould {
+
+ @Mock
+ IdentityProviderManager mockIdentityProviderManager;
+
+ @Mock
+ RoutingContext mockRoutingContext;
+
+ @Mock
+ HttpServerRequest mockRequest;
+
+ @Mock
+ SecurityIdentity mockIdentity;
+
+ NoAuthAuthenticationMechanism mechanism;
+
+ @BeforeEach
+ void setUp() {
+ mechanism = new NoAuthAuthenticationMechanism();
+ }
+
+ @Test
+ void return_empty_when_no_auth_disabled() {
+ mechanism.noAuthEnabled = false;
+
+ SecurityIdentity result = mechanism.authenticate(mockRoutingContext, mockIdentityProviderManager)
+ .await().indefinitely();
+
+ assertNull(result);
+ verifyNoInteractions(mockIdentityProviderManager);
+ }
+
+ @Test
+ void authenticate_with_no_auth_principal_when_no_auth_enabled() {
+ mechanism.noAuthEnabled = true;
+ when(mockRoutingContext.request()).thenReturn(mockRequest);
+ when(mockRequest.path()).thenReturn("/test");
+ when(mockIdentityProviderManager.authenticate(any(TrustedAuthenticationRequest.class)))
+ .thenReturn(Uni.createFrom().item(mockIdentity));
+
+ SecurityIdentity result = mechanism.authenticate(mockRoutingContext, mockIdentityProviderManager)
+ .await().indefinitely();
+
+ assertNotNull(result);
+ verify(mockIdentityProviderManager).authenticate(any(TrustedAuthenticationRequest.class));
+ }
+
+ @Test
+ void return_null_challenge() {
+ ChallengeData challenge = mechanism.getChallenge(mockRoutingContext).await().indefinitely();
+
+ assertNull(challenge);
+ }
+}
diff --git a/calm-hub/src/test/java/org/finos/calm/security/TestProxyAuthenticationMechanismShould.java b/calm-hub/src/test/java/org/finos/calm/security/TestProxyAuthenticationMechanismShould.java
new file mode 100644
index 000000000..c051290e9
--- /dev/null
+++ b/calm-hub/src/test/java/org/finos/calm/security/TestProxyAuthenticationMechanismShould.java
@@ -0,0 +1,95 @@
+package org.finos.calm.security;
+
+import io.netty.handler.codec.http.HttpResponseStatus;
+import io.quarkus.security.identity.IdentityProviderManager;
+import io.quarkus.security.identity.SecurityIdentity;
+import io.quarkus.security.identity.request.TrustedAuthenticationRequest;
+import io.quarkus.vertx.http.runtime.security.ChallengeData;
+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 org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class TestProxyAuthenticationMechanismShould {
+
+ @Mock
+ IdentityProviderManager mockIdentityProviderManager;
+
+ @Mock
+ RoutingContext mockRoutingContext;
+
+ @Mock
+ HttpServerRequest mockRequest;
+
+ @Mock
+ SecurityIdentity mockIdentity;
+
+ ProxyAuthenticationMechanism mechanism;
+
+ @BeforeEach
+ void setUp() {
+ mechanism = new ProxyAuthenticationMechanism();
+ mechanism.usernameHeader = "Remote-User";
+ }
+
+ @Test
+ void return_empty_when_username_header_is_missing() {
+ when(mockRoutingContext.request()).thenReturn(mockRequest);
+ when(mockRequest.getHeader("Remote-User")).thenReturn(null);
+ when(mockRequest.path()).thenReturn("/test");
+
+ SecurityIdentity result = mechanism.authenticate(mockRoutingContext, mockIdentityProviderManager)
+ .await().indefinitely();
+
+ assertNull(result);
+ verifyNoInteractions(mockIdentityProviderManager);
+ }
+
+ @Test
+ void return_empty_when_username_header_is_empty() {
+ when(mockRoutingContext.request()).thenReturn(mockRequest);
+ when(mockRequest.getHeader("Remote-User")).thenReturn("");
+ when(mockRequest.path()).thenReturn("/test");
+
+ SecurityIdentity result = mechanism.authenticate(mockRoutingContext, mockIdentityProviderManager)
+ .await().indefinitely();
+
+ assertNull(result);
+ verifyNoInteractions(mockIdentityProviderManager);
+ }
+
+ @Test
+ void authenticate_user_when_valid_username_header_is_present() {
+ when(mockRoutingContext.request()).thenReturn(mockRequest);
+ when(mockRequest.getHeader("Remote-User")).thenReturn("alice");
+ when(mockIdentityProviderManager.authenticate(any(TrustedAuthenticationRequest.class)))
+ .thenReturn(Uni.createFrom().item(mockIdentity));
+
+ SecurityIdentity result = mechanism.authenticate(mockRoutingContext, mockIdentityProviderManager)
+ .await().indefinitely();
+
+ assertNotNull(result);
+ verify(mockIdentityProviderManager).authenticate(any(TrustedAuthenticationRequest.class));
+ }
+
+ @Test
+ void return_401_challenge() {
+ ChallengeData challenge = mechanism.getChallenge(mockRoutingContext).await().indefinitely();
+
+ assertNotNull(challenge);
+ assertEquals(HttpResponseStatus.UNAUTHORIZED.code(), challenge.status);
+ }
+}
diff --git a/calm-hub/src/test/java/org/finos/calm/security/TestSecurityResponseHeadersShould.java b/calm-hub/src/test/java/org/finos/calm/security/TestSecurityResponseHeadersShould.java
index d3aae0e77..46dda162a 100644
--- a/calm-hub/src/test/java/org/finos/calm/security/TestSecurityResponseHeadersShould.java
+++ b/calm-hub/src/test/java/org/finos/calm/security/TestSecurityResponseHeadersShould.java
@@ -2,6 +2,7 @@
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
+import io.quarkus.test.security.TestSecurity;
import org.finos.calm.store.NamespaceStore;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -13,6 +14,7 @@
import static org.hamcrest.Matchers.equalTo;
import static org.mockito.Mockito.when;
+@TestSecurity(authorizationEnabled = false)
@QuarkusTest
@ExtendWith(MockitoExtension.class)
public class TestSecurityResponseHeadersShould {
diff --git a/calm-hub/src/test/java/org/finos/calm/security/TestUserAccessValidatorShould.java b/calm-hub/src/test/java/org/finos/calm/security/TestUserAccessValidatorShould.java
index 98042c0ee..ed31445f0 100644
--- a/calm-hub/src/test/java/org/finos/calm/security/TestUserAccessValidatorShould.java
+++ b/calm-hub/src/test/java/org/finos/calm/security/TestUserAccessValidatorShould.java
@@ -1,122 +1,51 @@
package org.finos.calm.security;
import org.finos.calm.domain.UserAccess;
-import org.finos.calm.domain.UserAccess.Permission;
-import org.finos.calm.domain.UserAccess.ResourceType;
import org.finos.calm.domain.exception.UserAccessNotFoundException;
import org.finos.calm.store.UserAccessStore;
import org.junit.jupiter.api.BeforeEach;
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.util.List;
import java.util.Set;
-import static org.junit.jupiter.api.Assertions.*;
-import static org.mockito.Mockito.*;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
class TestUserAccessValidatorShould {
- private UserAccessStore userAccessStore;
- private UserAccessValidator validator;
+ @Mock
+ UserAccessStore mockUserAccessStore;
+
+ UserAccessValidator validator;
@BeforeEach
void setUp() {
- userAccessStore = mock(UserAccessStore.class);
- validator = new UserAccessValidator(userAccessStore);
- }
-
- @Test
- void return_true_when_user_has_sufficient_permissions() throws Exception {
- UserRequestAttributes requestAttributes = new UserRequestAttributes("GET", "testuser",
- "/calm/namespace/finos/patterns", "finos");
- UserAccess userAccess = new UserAccess("testuser", Permission.read, "finos", ResourceType.patterns);
- when(userAccessStore.getUserAccessForUsername("testuser"))
- .thenReturn(List.of(userAccess));
-
- boolean actual = validator.isUserAuthorized(requestAttributes);
- assertTrue(actual);
- }
-
- @Test
- void return_false_when_user_has_no_matching_permission() throws Exception {
- UserRequestAttributes requestAttributes = new UserRequestAttributes("GET", "testuser",
- "/calm/namespace/finos/patterns", "finos");
- UserAccess userAccess = new UserAccess("testuser", Permission.read, "workshop", ResourceType.patterns);
- when(userAccessStore.getUserAccessForUsername("testuser"))
- .thenReturn(List.of(userAccess));
-
- boolean actual = validator.isUserAuthorized(requestAttributes);
- assertFalse(actual);
- }
-
- @Test
- void return_true_when_user_has_write_permission() throws Exception {
- UserRequestAttributes requestAttributes = new UserRequestAttributes("GET", "testuser",
- "/calm/namespace/finos/patterns", "finos");
- UserAccess userAccess = new UserAccess("testuser", Permission.write, "finos", ResourceType.patterns);
- when(userAccessStore.getUserAccessForUsername("testuser"))
- .thenReturn(List.of(userAccess));
-
- boolean actual = validator.isUserAuthorized(requestAttributes);
- assertTrue(actual);
+ validator = new UserAccessValidator(mockUserAccessStore);
}
@Test
- void return_true_when_user_accessing_default_get_namespaces_endpoint() throws Exception {
- UserRequestAttributes requestAttributes = new UserRequestAttributes("GET", "testuser",
- "/calm/namespaces", null);
+ void return_namespaces_for_user_with_access_grants() throws UserAccessNotFoundException {
+ UserAccess ua1 = new UserAccess("alice", UserAccess.Permission.read, "finos");
+ UserAccess ua2 = new UserAccess("alice", UserAccess.Permission.write, "workshop");
+ when(mockUserAccessStore.getUserAccessForUsername("alice")).thenReturn(List.of(ua1, ua2));
- boolean actual = validator.isUserAuthorized(requestAttributes);
- assertTrue(actual);
- }
-
- @Test
- void return_false_when_no_permissions_are_mapped_to_user() throws Exception {
- UserRequestAttributes requestAttributes = new UserRequestAttributes("GET", "testuser",
- "/calm/namespaces/test/finos", "finos");
- when(userAccessStore.getUserAccessForUsername("testuser"))
- .thenThrow(new UserAccessNotFoundException());
-
- boolean actual = validator.isUserAuthorized(requestAttributes);
- assertFalse(actual);
- }
-
- @Test
- void return_true_when_user_accessing_search_endpoint() throws Exception {
- UserRequestAttributes requestAttributes = new UserRequestAttributes("GET", "testuser",
- "/calm/search", null);
+ Set result = validator.getReadableNamespaces("alice");
- boolean actual = validator.isUserAuthorized(requestAttributes);
- assertTrue(actual);
+ assertEquals(Set.of("finos", "workshop"), result);
}
@Test
- void return_readable_namespaces_for_user_with_grants() throws Exception {
- UserAccess grant1 = new UserAccess("testuser", Permission.read, "finos", ResourceType.patterns);
- UserAccess grant2 = new UserAccess("testuser", Permission.write, "workshop", ResourceType.architectures);
- when(userAccessStore.getUserAccessForUsername("testuser"))
- .thenReturn(List.of(grant1, grant2));
+ void return_empty_set_when_user_has_no_access_grants() throws UserAccessNotFoundException {
+ when(mockUserAccessStore.getUserAccessForUsername("alice")).thenThrow(new UserAccessNotFoundException());
- Set namespaces = validator.getReadableNamespaces("testuser");
- assertEquals(Set.of("finos", "workshop"), namespaces);
- }
-
- @Test
- void return_empty_set_when_user_has_no_grants() throws Exception {
- when(userAccessStore.getUserAccessForUsername("testuser"))
- .thenThrow(new UserAccessNotFoundException());
-
- Set namespaces = validator.getReadableNamespaces("testuser");
- assertTrue(namespaces.isEmpty());
- }
-
- @Test
- void return_deduplicated_namespaces_when_user_has_multiple_grants_for_same_namespace() throws Exception {
- UserAccess grant1 = new UserAccess("testuser", Permission.read, "finos", ResourceType.patterns);
- UserAccess grant2 = new UserAccess("testuser", Permission.write, "finos", ResourceType.architectures);
- when(userAccessStore.getUserAccessForUsername("testuser"))
- .thenReturn(List.of(grant1, grant2));
+ Set result = validator.getReadableNamespaces("alice");
- Set namespaces = validator.getReadableNamespaces("testuser");
- assertEquals(Set.of("finos"), namespaces);
+ assertTrue(result.isEmpty());
}
}
diff --git a/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoUserAccessStoreShould.java b/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoUserAccessStoreShould.java
index 408188649..e874787d6 100644
--- a/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoUserAccessStoreShould.java
+++ b/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoUserAccessStoreShould.java
@@ -1,13 +1,15 @@
package org.finos.calm.store.mongo;
-import com.mongodb.client.*;
+import com.mongodb.client.FindIterable;
+import com.mongodb.client.MongoCollection;
+import com.mongodb.client.MongoCursor;
+import com.mongodb.client.MongoDatabase;
import com.mongodb.client.model.Filters;
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
import org.bson.Document;
import org.finos.calm.domain.UserAccess;
import org.finos.calm.domain.UserAccess.Permission;
-import org.finos.calm.domain.UserAccess.ResourceType;
import org.finos.calm.domain.exception.NamespaceNotFoundException;
import org.finos.calm.domain.exception.UserAccessNotFoundException;
import org.junit.jupiter.api.BeforeEach;
@@ -66,7 +68,6 @@ void return_user_access_for_valid_username() throws Exception {
Document doc = new Document("username", username)
.append("namespace", namespace)
.append("permission", Permission.read.name())
- .append("resourceType", ResourceType.patterns.name())
.append("userAccessId", 101);
when(namespaceStore.namespaceExists(namespace)).thenReturn(true);
@@ -107,7 +108,6 @@ void throw_exception_when_namespace_does_not_exist_on_create() {
.setNamespace("invalid")
.setUsername("test")
.setPermission(Permission.read)
- .setResourceType(ResourceType.architectures)
.build();
assertThrows(NamespaceNotFoundException.class,
@@ -123,7 +123,6 @@ void create_user_access_when_namespace_is_exists() throws NamespaceNotFoundExcep
.setNamespace("finos")
.setUsername("test")
.setPermission(Permission.write)
- .setResourceType(ResourceType.patterns)
.build();
UserAccess actual = mongoUserAccessStore.createUserAccessForNamespace(userAccess);
@@ -137,7 +136,6 @@ void return_user_access_list_for_namespace() throws Exception {
Document doc = new Document("username", "test")
.append("namespace", namespace)
.append("permission", Permission.read.name())
- .append("resourceType", ResourceType.flows.name())
.append("userAccessId", 111);
DocumentFindIterable findIterable = mock(DocumentFindIterable.class);
@@ -155,7 +153,6 @@ void return_user_access_list_for_namespace() throws Exception {
assertThat(actual, hasSize(1));
assertThat(actual.getFirst().getUsername(), is("test"));
assertThat(actual.getFirst().getPermission(), is(Permission.read));
- assertThat(actual.getFirst().getResourceType(), is(ResourceType.flows));
}
@Test
@@ -186,7 +183,6 @@ void return_user_access_for_namespace_and_user_access_id() throws Exception {
Document document = new Document("username", "test")
.append("namespace", namespace)
.append("permission", Permission.read.name())
- .append("resourceType", ResourceType.flows.name())
.append("userAccessId", userAccessId);
DocumentFindIterable mockFindIterable = mock(DocumentFindIterable.class);
@@ -201,11 +197,96 @@ void return_user_access_for_namespace_and_user_access_id() throws Exception {
assertThat(actual.getUsername(), is("test"));
assertThat(actual.getPermission(), is(Permission.read));
- assertThat(actual.getResourceType(), is(ResourceType.flows));
assertThat(actual.getNamespace(), is(namespace));
assertThat(actual.getUserAccessId(), is(userAccessId));
}
+ @Test
+ void create_user_access_for_domain() {
+ when(counterStore.getNextUserAccessSequenceValue()).thenReturn(201);
+
+ UserAccess userAccess = new UserAccess.UserAccessBuilder()
+ .setDomain("payments")
+ .setUsername("test")
+ .setPermission(Permission.write)
+ .build();
+
+ UserAccess actual = mongoUserAccessStore.createUserAccessForDomain(userAccess);
+ assertThat(actual.getUserAccessId(), is(201));
+ assertThat(actual.getDomain(), is("payments"));
+ verify(userAccessCollection).insertOne(ArgumentMatchers.any(Document.class));
+ }
+
+ @Test
+ void throw_exception_if_no_user_access_found_for_domain() {
+ String domain = "payments";
+ DocumentFindIterable findIterable = mock(DocumentFindIterable.class);
+ DocumentMongoCursor mockMongoCursor = mock(DocumentMongoCursor.class);
+ when(mockMongoCursor.hasNext()).thenReturn(false);
+ when(findIterable.iterator()).thenReturn(mockMongoCursor);
+ when(userAccessCollection.find(Filters.eq("domain", domain))).thenReturn(findIterable);
+
+ assertThrows(UserAccessNotFoundException.class,
+ () -> mongoUserAccessStore.getUserAccessForDomain(domain));
+ }
+
+ @Test
+ void return_user_access_list_for_domain() throws Exception {
+ String domain = "payments";
+ Document doc = new Document("username", "test")
+ .append("domain", domain)
+ .append("permission", Permission.read.name())
+ .append("userAccessId", 201);
+
+ DocumentFindIterable findIterable = mock(DocumentFindIterable.class);
+ DocumentMongoCursor cursor = mock(DocumentMongoCursor.class);
+ when(cursor.hasNext()).thenReturn(true, false);
+ when(cursor.next()).thenReturn(doc);
+ when(findIterable.iterator()).thenReturn(cursor);
+ when(userAccessCollection.find(Filters.eq("domain", domain))).thenReturn(findIterable);
+
+ List actual = mongoUserAccessStore.getUserAccessForDomain(domain);
+
+ assertThat(actual, hasSize(1));
+ assertThat(actual.getFirst().getDomain(), is(domain));
+ }
+
+ @Test
+ void throw_exception_if_no_user_access_found_for_domain_and_id() {
+ String domain = "payments";
+ Integer userAccessId = 201;
+ DocumentFindIterable findIterable = mock(DocumentFindIterable.class);
+ when(userAccessCollection.find(Filters.and(
+ Filters.eq("domain", domain),
+ Filters.eq("userAccessId", userAccessId)))).thenReturn(findIterable);
+ when(findIterable.first()).thenReturn(null);
+
+ assertThrows(UserAccessNotFoundException.class,
+ () -> mongoUserAccessStore.getUserAccessForDomainAndId(domain, userAccessId));
+ }
+
+ @Test
+ void return_user_access_for_domain_and_id() throws Exception {
+ String domain = "payments";
+ Integer userAccessId = 201;
+
+ Document document = new Document("username", "test")
+ .append("domain", domain)
+ .append("permission", Permission.read.name())
+ .append("userAccessId", userAccessId);
+
+ DocumentFindIterable mockFindIterable = mock(DocumentFindIterable.class);
+ when(userAccessCollection.find(Filters.and(
+ Filters.eq("domain", domain),
+ Filters.eq("userAccessId", userAccessId)))).thenReturn(mockFindIterable);
+ when(mockFindIterable.first()).thenReturn(document);
+
+ UserAccess actual = mongoUserAccessStore.getUserAccessForDomainAndId(domain, userAccessId);
+
+ assertThat(actual.getDomain(), is(domain));
+ assertThat(actual.getUserAccessId(), is(userAccessId));
+ }
+
private interface DocumentFindIterable extends FindIterable {
}
diff --git a/calm-hub/src/test/java/org/finos/calm/store/nitrite/TestNitriteUserAccessStoreShould.java b/calm-hub/src/test/java/org/finos/calm/store/nitrite/TestNitriteUserAccessStoreShould.java
index 1f20bc3ee..055e76e9a 100644
--- a/calm-hub/src/test/java/org/finos/calm/store/nitrite/TestNitriteUserAccessStoreShould.java
+++ b/calm-hub/src/test/java/org/finos/calm/store/nitrite/TestNitriteUserAccessStoreShould.java
@@ -57,7 +57,6 @@ public void testCreateUserAccessForNamespace() throws NamespaceNotFoundException
.setNamespace("finos")
.setUsername("testuser")
.setPermission(UserAccess.Permission.read)
- .setResourceType(UserAccess.ResourceType.patterns)
.build();
userAccess.setCreationDateTime(LocalDateTime.now());
userAccess.setUpdateDateTime(LocalDateTime.now());
@@ -83,7 +82,6 @@ public void testCreateUserAccessForNamespace_ThrowsExceptionWhenNamespaceNotFoun
.setNamespace("nonexistent")
.setUsername("testuser")
.setPermission(UserAccess.Permission.read)
- .setResourceType(UserAccess.ResourceType.patterns)
.build();
when(mockNamespaceStore.namespaceExists("nonexistent")).thenReturn(false);
@@ -101,8 +99,8 @@ public void testGetUserAccessForUsername() throws UserAccessNotFoundException {
when(mockDoc.get("username", String.class)).thenReturn("testuser");
when(mockDoc.get("namespace", String.class)).thenReturn("finos");
+ when(mockDoc.get("domain", String.class)).thenReturn(null);
when(mockDoc.get("permission", String.class)).thenReturn("read");
- when(mockDoc.get("resourceType", String.class)).thenReturn("patterns");
when(mockDoc.get("userAccessId", Integer.class)).thenReturn(1);
// Act
@@ -134,8 +132,8 @@ public void testGetUserAccessForNamespace() throws NamespaceNotFoundException, U
when(mockDoc.get("username", String.class)).thenReturn("testuser");
when(mockDoc.get("namespace", String.class)).thenReturn("finos");
+ when(mockDoc.get("domain", String.class)).thenReturn(null);
when(mockDoc.get("permission", String.class)).thenReturn("read");
- when(mockDoc.get("resourceType", String.class)).thenReturn("patterns");
when(mockDoc.get("userAccessId", Integer.class)).thenReturn(1);
// Act
@@ -165,8 +163,8 @@ public void testGetUserAccessForNamespaceAndId() throws NamespaceNotFoundExcepti
when(mockDoc.get("username", String.class)).thenReturn("testuser");
when(mockDoc.get("namespace", String.class)).thenReturn("finos");
+ when(mockDoc.get("domain", String.class)).thenReturn(null);
when(mockDoc.get("permission", String.class)).thenReturn("read");
- when(mockDoc.get("resourceType", String.class)).thenReturn("patterns");
when(mockDoc.get("userAccessId", Integer.class)).thenReturn(1);
// Act
@@ -188,4 +186,78 @@ public void testGetUserAccessForNamespaceAndId_ThrowsExceptionWhenNotFound() {
// Act & Assert
assertThrows(UserAccessNotFoundException.class, () -> userAccessStore.getUserAccessForNamespaceAndId("finos", 1));
}
+
+ @Test
+ public void testCreateUserAccessForDomain() {
+ UserAccess userAccess = new UserAccess.UserAccessBuilder()
+ .setDomain("payments")
+ .setUsername("testuser")
+ .setPermission(UserAccess.Permission.write)
+ .build();
+ userAccess.setCreationDateTime(java.time.LocalDateTime.now());
+ userAccess.setUpdateDateTime(java.time.LocalDateTime.now());
+
+ when(mockCounterStore.getNextUserAccessSequenceValue()).thenReturn(2);
+
+ UserAccess result = userAccessStore.createUserAccessForDomain(userAccess);
+
+ assertThat(result, is(notNullValue()));
+ assertThat(result.getUserAccessId(), is(2));
+ assertThat(result.getDomain(), is("payments"));
+ assertThat(result.getUsername(), is("testuser"));
+ verify(mockCollection).insert(any(Document.class));
+ }
+
+ @Test
+ public void testGetUserAccessForDomain() throws UserAccessNotFoundException {
+ Document mockDoc = mock(Document.class);
+ when(mockCollection.find(any(Filter.class))).thenReturn(mockCursor);
+ when(mockCursor.iterator()).thenReturn(Collections.singletonList(mockDoc).iterator());
+
+ when(mockDoc.get("username", String.class)).thenReturn("testuser");
+ when(mockDoc.get("namespace", String.class)).thenReturn(null);
+ when(mockDoc.get("domain", String.class)).thenReturn("payments");
+ when(mockDoc.get("permission", String.class)).thenReturn("write");
+ when(mockDoc.get("userAccessId", Integer.class)).thenReturn(2);
+
+ List result = userAccessStore.getUserAccessForDomain("payments");
+
+ assertThat(result, hasSize(1));
+ assertThat(result.getFirst().getDomain(), is("payments"));
+ }
+
+ @Test
+ public void testGetUserAccessForDomain_ThrowsExceptionWhenNotFound() {
+ when(mockCollection.find(any(Filter.class))).thenReturn(mockCursor);
+ when(mockCursor.iterator()).thenReturn(Collections.emptyIterator());
+
+ assertThrows(UserAccessNotFoundException.class, () -> userAccessStore.getUserAccessForDomain("payments"));
+ }
+
+ @Test
+ public void testGetUserAccessForDomainAndId() throws UserAccessNotFoundException {
+ Document mockDoc = mock(Document.class);
+ when(mockCollection.find(any(Filter.class))).thenReturn(mockCursor);
+ when(mockCursor.firstOrNull()).thenReturn(mockDoc);
+
+ when(mockDoc.get("username", String.class)).thenReturn("testuser");
+ when(mockDoc.get("namespace", String.class)).thenReturn(null);
+ when(mockDoc.get("domain", String.class)).thenReturn("payments");
+ when(mockDoc.get("permission", String.class)).thenReturn("write");
+ when(mockDoc.get("userAccessId", Integer.class)).thenReturn(2);
+
+ UserAccess result = userAccessStore.getUserAccessForDomainAndId("payments", 2);
+
+ assertThat(result, is(notNullValue()));
+ assertThat(result.getDomain(), is("payments"));
+ assertThat(result.getUserAccessId(), is(2));
+ }
+
+ @Test
+ public void testGetUserAccessForDomainAndId_ThrowsExceptionWhenNotFound() {
+ when(mockCollection.find(any(Filter.class))).thenReturn(mockCursor);
+ when(mockCursor.firstOrNull()).thenReturn(null);
+
+ assertThrows(UserAccessNotFoundException.class, () -> userAccessStore.getUserAccessForDomainAndId("payments", 2));
+ }
}
diff --git a/calm-hub/src/test/resources/secure-profile/realm.json b/calm-hub/src/test/resources/secure-profile/realm.json
index 911ae2c6b..cbf91ea3f 100644
--- a/calm-hub/src/test/resources/secure-profile/realm.json
+++ b/calm-hub/src/test/resources/secure-profile/realm.json
@@ -8,185 +8,40 @@
"protocol": "openid-connect",
"publicClient": false,
"secret": "calm-hub-client-app-secret",
- "authorizationServicesEnabled": true,
"directAccessGrantsEnabled": true,
- "attributes": {
- "access.token.lifespan": "300",
- "refresh.token.lifespan": "1800"
- },
"defaultClientScopes": [
"openid",
"profile",
"email"
],
- "optionalClientScopes": [
- "address",
- "phone",
- "architectures:read",
- "architectures:all",
- "namespace:admin",
- "adrs:read",
- "adrs:all",
- "deny:all"
- ]
- },
- {
- "clientId": "calm-hub-producer-app",
- "enabled": true,
- "protocol": "openid-connect",
- "publicClient": false,
- "authorizationServicesEnabled": true
- }
- ],
- "roles": {
- "realm": [
- {
- "name": "admin",
- "clientRole": false
- }
- ],
- "client": {
- "calm-hub-client-app": [
- {
- "name": "architectures:read"
- },
- {
- "name": "architectures:all"
- },
- {
- "name": "adrs:all"
- },
- {
- "name": "adrs:read"
- },
- {
- "name": "deny:all"
- }
- ]
- }
- },
- "clientScopes": [
- {
- "name": "architectures:read",
- "protocol": "openid-connect",
- "attributes": {
- "include.in.token.scope": "true"
- },
- "protocolMappers": [
- {
- "name": "audience",
- "protocol": "openid-connect",
- "protocolMapper": "oidc-audience-mapper",
- "config": {
- "included.client.audience": "calm-hub-producer-app",
- "id.token.claim": "true",
- "access.token.claim": "true"
- }
- }
- ]
- },
- {
- "name": "architectures:all",
- "protocol": "openid-connect",
- "attributes": {
- "include.in.token.scope": "true"
- },
- "protocolMappers": [
- {
- "name": "audience",
- "protocol": "openid-connect",
- "protocolMapper": "oidc-audience-mapper",
- "config": {
- "included.client.audience": "calm-hub-producer-app",
- "id.token.claim": "true",
- "access.token.claim": "true"
- }
- }
- ]
- },
- {
- "name": "adrs:all",
- "protocol": "openid-connect",
- "attributes": {
- "include.in.token.scope": "true"
- },
- "protocolMappers": [
- {
- "name": "audience",
- "protocol": "openid-connect",
- "protocolMapper": "oidc-audience-mapper",
- "config": {
- "included.client.audience": "calm-hub-producer-app",
- "id.token.claim": "true",
- "access.token.claim": "true"
- }
- }
- ]
- },
- {
- "name": "adrs:read",
- "protocol": "openid-connect",
- "attributes": {
- "include.in.token.scope": "true"
- },
- "protocolMappers": [
- {
- "name": "audience",
- "protocol": "openid-connect",
- "protocolMapper": "oidc-audience-mapper",
- "config": {
- "included.client.audience": "calm-hub-producer-app",
- "id.token.claim": "true",
- "access.token.claim": "true"
- }
- }
- ]
- },
- {
- "name": "deny:all",
- "protocol": "openid-connect",
- "attributes": {
- "include.in.token.scope": "true"
- },
"protocolMappers": [
{
"name": "audience",
"protocol": "openid-connect",
"protocolMapper": "oidc-audience-mapper",
+ "consentRequired": false,
"config": {
"included.client.audience": "calm-hub-producer-app",
- "id.token.claim": "true",
+ "id.token.claim": "false",
"access.token.claim": "true"
}
}
]
},
{
- "name": "namespace:admin",
+ "clientId": "calm-hub-producer-app",
+ "enabled": true,
"protocol": "openid-connect",
- "attributes": {
- "include.in.token.scope": "true"
- },
- "protocolMappers": [
- {
- "name": "audience",
- "protocol": "openid-connect",
- "protocolMapper": "oidc-audience-mapper",
- "config": {
- "included.client.audience": "calm-hub-producer-app",
- "id.token.claim": "true",
- "access.token.claim": "true"
- }
- }
- ]
- },
+ "publicClient": false
+ }
+ ],
+ "clientScopes": [
{
"name": "profile",
"description": "OpenID Connect built-in scope: profile",
"protocol": "openid-connect",
"attributes": {
"include.in.token.scope": "true",
- "consent.screen.text": "${profileScopeConsentText}",
"display.on.consent.screen": "true"
},
"protocolMappers": [
@@ -221,43 +76,6 @@
]
}
],
- "scopeMappings": [
- {
- "client": "calm-hub-client-app",
- "clientScope": "architectures:read",
- "roles": [
- "architectures:read"
- ]
- },
- {
- "client": "calm-hub-client-app",
- "clientScope": "architectures:all",
- "roles": [
- "architectures:all"
- ]
- },
- {
- "client": "calm-hub-client-app",
- "clientScope": "adrs:read",
- "roles": [
- "adrs:read"
- ]
- },
- {
- "client": "calm-hub-client-app",
- "clientScope": "adrs:all",
- "roles": [
- "adrs:all"
- ]
- },
- {
- "client": "calm-hub-client-app",
- "clientScope": "deny:all",
- "roles": [
- "deny:all"
- ]
- }
- ],
"users": [
{
"username": "test-user",
@@ -272,15 +90,7 @@
"value": "changeme",
"temporary": false
}
- ],
- "realmRoles": [
- "admin"
- ],
- "clientRoles": {
- "calm-hub-client-app": [
- "architectures:read"
- ]
- }
+ ]
}
]
-}
\ No newline at end of file
+}
diff --git a/package-lock.json b/package-lock.json
index d3e92f28b..09a3e46ad 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15222,11 +15222,13 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"darwin"
],
+ "peer": true,
"engines": {
"node": ">=10"
}
@@ -15238,11 +15240,13 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"darwin"
],
+ "peer": true,
"engines": {
"node": ">=10"
}
@@ -15254,11 +15258,13 @@
"cpu": [
"arm"
],
+ "dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
+ "peer": true,
"engines": {
"node": ">=10"
}
@@ -15270,11 +15276,13 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"linux"
],
+ "peer": true,
"engines": {
"node": ">=10"
}
@@ -15286,11 +15294,13 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"linux"
],
+ "peer": true,
"engines": {
"node": ">=10"
}
@@ -15302,11 +15312,13 @@
"cpu": [
"ppc64"
],
+ "dev": true,
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"linux"
],
+ "peer": true,
"engines": {
"node": ">=10"
}
@@ -15318,11 +15330,13 @@
"cpu": [
"s390x"
],
+ "dev": true,
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"linux"
],
+ "peer": true,
"engines": {
"node": ">=10"
}
@@ -15334,11 +15348,13 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"linux"
],
+ "peer": true,
"engines": {
"node": ">=10"
}
@@ -15350,11 +15366,13 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"linux"
],
+ "peer": true,
"engines": {
"node": ">=10"
}
@@ -15366,11 +15384,13 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"win32"
],
+ "peer": true,
"engines": {
"node": ">=10"
}
@@ -15382,11 +15402,13 @@
"cpu": [
"ia32"
],
+ "dev": true,
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"win32"
],
+ "peer": true,
"engines": {
"node": ">=10"
}
@@ -15398,11 +15420,13 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"win32"
],
+ "peer": true,
"engines": {
"node": ">=10"
}
@@ -37963,6 +37987,7 @@
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,