From b8a5c74db6be253ccecd7431231832c63b8a215b Mon Sep 17 00:00:00 2001
From: Andres Contreras
Date: Thu, 18 Jun 2026 21:58:15 +0200
Subject: [PATCH 1/2] feat: deliver the security platform through the
application starter (secure-by-default resource server + method security)
---
pom.xml | 14 ++++++++++++++
1 file changed, 14 insertions(+)
diff --git a/pom.xml b/pom.xml
index b1524b4..ec69798 100644
--- a/pom.xml
+++ b/pom.xml
@@ -43,6 +43,20 @@
spring-boot-starter-security
+
+
+ org.fireflyframework
+ fireflyframework-security-resource-server
+ ${project.version}
+
+
+ org.fireflyframework
+ fireflyframework-security-method-policy
+ ${project.version}
+
+
org.springframework.boot
From 73e563ef017e58bb1e5f1b365ad6078225d2bf81 Mon Sep 17 00:00:00 2001
From: Andres Contreras
Date: Thu, 18 Jun 2026 23:02:16 +0200
Subject: [PATCH 2/2] =?UTF-8?q?refactor!:=20de-domain=20the=20application?=
=?UTF-8?q?=20layer=20=E2=80=94=20remove=20firefly-oss=20party/contract/pr?=
=?UTF-8?q?oduct=20+=20X-Party-Id=20+=20Security=20Center?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
AppContext is now product-agnostic (subject/tenantId/roles/permissions/attributes); identity is
projected from the validated SecurityContextPort (fireflyframework-security), not the trusted
X-Party-Id header. Deletes the Security Center SPI (SessionManager/SessionContext/SessionContextMapper
+ Contract/Product/Role/RoleScope DTOs) and @RequireContext; genericizes the resolvers and the
resource controller. 180 tests green. BREAKING (clean break, no shims).
---
.../application/aop/SecurityAspect.java | 53 ++--
.../ApplicationLayerAutoConfiguration.java | 35 ++-
.../config/ApplicationLayerProperties.java | 32 +--
.../application/context/AppContext.java | 155 +++--------
.../context/AppSecurityContext.java | 88 +++----
.../context/ApplicationExecutionContext.java | 133 +++-------
.../AbstractApplicationController.java | 97 ++++---
.../AbstractResourceController.java | 206 ++++++---------
.../resolver/AbstractContextResolver.java | 202 ++------------
.../application/resolver/ContextResolver.java | 94 +------
.../resolver/DefaultContextResolver.java | 242 +++--------------
.../AbstractSecurityAuthorizationService.java | 135 +++++-----
.../AbstractSecurityConfiguration.java | 150 +++++------
.../DefaultSecurityAuthorizationService.java | 139 ++--------
.../SecurityAuthorizationService.java | 38 +--
.../security/annotation/RequireContext.java | 78 ------
.../security/annotation/Secure.java | 84 +++---
.../service/AbstractApplicationService.java | 74 ++----
.../application/spi/SessionContext.java | 47 ----
.../application/spi/SessionManager.java | 73 ------
.../application/spi/dto/ContractInfoDTO.java | 27 --
.../application/spi/dto/ProductInfoDTO.java | 24 --
.../application/spi/dto/RoleInfoDTO.java | 28 --
.../application/spi/dto/RoleScopeInfoDTO.java | 26 --
.../util/SessionContextMapper.java | 108 --------
.../ApplicationLayerPropertiesTest.java | 58 ++---
.../application/context/AppContextTest.java | 150 +++++------
.../context/AppSecurityContextTest.java | 98 +++----
.../ApplicationExecutionContextTest.java | 159 +++++------
.../AbstractApplicationControllerTest.java | 96 ++++---
.../AbstractResourceControllerTest.java | 174 +++++--------
.../ControllerIntegrationTest.java | 171 ++++++------
.../SecurityAspectIntegrationTest.java | 120 ++++-----
.../SecurityAuthorizationIntegrationTest.java | 246 +++++++++---------
.../resolver/AbstractContextResolverTest.java | 228 +++++-----------
...tractSecurityAuthorizationServiceTest.java | 79 +++---
.../AbstractApplicationServiceTest.java | 151 +++--------
37 files changed, 1340 insertions(+), 2758 deletions(-)
delete mode 100644 src/main/java/org/fireflyframework/common/application/security/annotation/RequireContext.java
delete mode 100644 src/main/java/org/fireflyframework/common/application/spi/SessionContext.java
delete mode 100644 src/main/java/org/fireflyframework/common/application/spi/SessionManager.java
delete mode 100644 src/main/java/org/fireflyframework/common/application/spi/dto/ContractInfoDTO.java
delete mode 100644 src/main/java/org/fireflyframework/common/application/spi/dto/ProductInfoDTO.java
delete mode 100644 src/main/java/org/fireflyframework/common/application/spi/dto/RoleInfoDTO.java
delete mode 100644 src/main/java/org/fireflyframework/common/application/spi/dto/RoleScopeInfoDTO.java
delete mode 100644 src/main/java/org/fireflyframework/common/application/util/SessionContextMapper.java
diff --git a/src/main/java/org/fireflyframework/common/application/aop/SecurityAspect.java b/src/main/java/org/fireflyframework/common/application/aop/SecurityAspect.java
index ff41c2f..298b238 100644
--- a/src/main/java/org/fireflyframework/common/application/aop/SecurityAspect.java
+++ b/src/main/java/org/fireflyframework/common/application/aop/SecurityAspect.java
@@ -17,7 +17,6 @@
package org.fireflyframework.common.application.aop;
import org.fireflyframework.common.application.config.ApplicationLayerProperties;
-import org.fireflyframework.common.application.context.AppContext;
import org.fireflyframework.common.application.context.AppSecurityContext;
import org.fireflyframework.common.application.context.ApplicationExecutionContext;
import org.fireflyframework.common.application.security.EndpointSecurityRegistry;
@@ -41,10 +40,10 @@
/**
* Aspect for intercepting and processing @Secure annotations.
* Handles security checks before method execution.
- *
+ *
* This aspect intercepts methods annotated with @Secure and performs
* authorization checks using the SecurityAuthorizationService.
- *
+ *
* @author Firefly Development Team
* @since 1.0.0
*/
@@ -52,14 +51,14 @@
@RequiredArgsConstructor
@Slf4j
public class SecurityAspect {
-
+
private final SecurityAuthorizationService authorizationService;
private final EndpointSecurityRegistry endpointSecurityRegistry;
private final ApplicationLayerProperties properties;
-
+
/**
* Intercepts methods annotated with @Secure.
- *
+ *
* @param joinPoint the join point
* @param secure the secure annotation
* @return the method result
@@ -68,18 +67,18 @@ public class SecurityAspect {
@Around("@annotation(secure)")
public Object secureMethod(ProceedingJoinPoint joinPoint, Secure secure) throws Throwable {
log.debug("Intercepting @Secure method: {}", joinPoint.getSignature().getName());
-
+
// Extract ApplicationExecutionContext from method arguments
ApplicationExecutionContext executionContext = findExecutionContext(joinPoint.getArgs());
if (executionContext == null) {
log.warn("No ApplicationExecutionContext found in method arguments, skipping security check");
return joinPoint.proceed();
}
-
+
// Check EndpointSecurityRegistry first (explicit configuration overrides annotations)
String endpoint = extractEndpoint(joinPoint);
String httpMethod = extractHttpMethod(joinPoint);
-
+
AppSecurityContext securityContext = endpointSecurityRegistry
.getEndpointSecurity(endpoint, httpMethod)
.map(explicitSecurity -> {
@@ -90,7 +89,7 @@ public Object secureMethod(ProceedingJoinPoint joinPoint, Secure secure) throws
log.debug("Using ANNOTATION security configuration for {} {}", httpMethod, endpoint);
return buildSecurityContext(secure, joinPoint, endpoint, httpMethod);
});
-
+
// Check if security is disabled
if (!properties.getSecurity().isEnabled()) {
log.debug("Security is disabled, allowing access to {}", endpoint);
@@ -102,13 +101,13 @@ public Object secureMethod(ProceedingJoinPoint joinPoint, Secure secure) throws
.flatMap(authorizedContext -> {
if (!authorizedContext.isAuthorized()) {
if (!properties.getSecurity().isEnforce()) {
- log.warn("ACCESS WOULD BE DENIED (enforce=false) for party: {} to endpoint: {}, reason: {}",
- executionContext.getPartyId(),
+ log.warn("ACCESS WOULD BE DENIED (enforce=false) for subject: {} to endpoint: {}, reason: {}",
+ executionContext.getSubject(),
securityContext.getEndpoint(),
authorizedContext.getAuthorizationFailureReason());
} else {
- log.warn("Access denied for party: {} to endpoint: {}, reason: {}",
- executionContext.getPartyId(),
+ log.warn("Access denied for subject: {} to endpoint: {}, reason: {}",
+ executionContext.getSubject(),
securityContext.getEndpoint(),
authorizedContext.getAuthorizationFailureReason());
return Mono.error(new AccessDeniedException(
@@ -140,10 +139,10 @@ public Object secureMethod(ProceedingJoinPoint joinPoint, Secure secure) throws
.subscribeOn(reactor.core.scheduler.Schedulers.boundedElastic())
.block();
}
-
+
/**
* Intercepts classes annotated with @Secure.
- *
+ *
* @param joinPoint the join point
* @param secure the secure annotation
* @return the method result
@@ -153,10 +152,10 @@ public Object secureMethod(ProceedingJoinPoint joinPoint, Secure secure) throws
public Object secureClass(ProceedingJoinPoint joinPoint, Secure secure) throws Throwable {
return secureMethod(joinPoint, secure);
}
-
+
/**
* Finds ApplicationExecutionContext in method arguments.
- *
+ *
* @param args the method arguments
* @return the execution context or null if not found
*/
@@ -164,19 +163,19 @@ private ApplicationExecutionContext findExecutionContext(Object[] args) {
if (args == null) {
return null;
}
-
+
for (Object arg : args) {
if (arg instanceof ApplicationExecutionContext) {
return (ApplicationExecutionContext) arg;
}
}
-
+
return null;
}
-
+
/**
* Builds AppSecurityContext from @Secure annotation.
- *
+ *
* @param secure the annotation
* @param joinPoint the join point
* @param endpoint the endpoint path
@@ -188,11 +187,9 @@ private AppSecurityContext buildSecurityContext(Secure secure, ProceedingJoinPoi
Set roles = new HashSet<>(Arrays.asList(secure.roles()));
Set permissions = new HashSet<>(Arrays.asList(secure.permissions()));
- // Use SECURITY_CENTER only if both the global property and annotation agree
- boolean useSecurityCenter = properties.getSecurity().isUseSecurityCenter() && secure.useSecurityCenter();
- AppSecurityContext.SecurityConfigSource configSource = useSecurityCenter
- ? AppSecurityContext.SecurityConfigSource.SECURITY_CENTER
- : AppSecurityContext.SecurityConfigSource.ANNOTATION;
+ // Authorization is driven by the annotation's declared roles/permissions evaluated against
+ // the validated principal's authorities (no external Security Center).
+ AppSecurityContext.SecurityConfigSource configSource = AppSecurityContext.SecurityConfigSource.ANNOTATION;
return AppSecurityContext.builder()
.endpoint(endpoint)
@@ -204,7 +201,7 @@ private AppSecurityContext buildSecurityContext(Secure secure, ProceedingJoinPoi
.configSource(configSource)
.build();
}
-
+
/**
* Extracts endpoint path from join point by reading Spring MVC mapping annotations.
* Falls back to class.method signature if no mapping annotations found.
diff --git a/src/main/java/org/fireflyframework/common/application/config/ApplicationLayerAutoConfiguration.java b/src/main/java/org/fireflyframework/common/application/config/ApplicationLayerAutoConfiguration.java
index 06fb8b5..d85e0bf 100644
--- a/src/main/java/org/fireflyframework/common/application/config/ApplicationLayerAutoConfiguration.java
+++ b/src/main/java/org/fireflyframework/common/application/config/ApplicationLayerAutoConfiguration.java
@@ -28,11 +28,10 @@
import org.fireflyframework.common.application.security.EndpointSecurityRegistry;
import org.fireflyframework.common.application.security.JwtClaimsRoleExtractor;
import org.fireflyframework.common.application.security.SecurityAuthorizationService;
-import org.fireflyframework.common.application.spi.SessionContext;
-import org.fireflyframework.common.application.spi.SessionManager;
+import org.fireflyframework.security.spi.SecurityContextPort;
import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
@@ -59,6 +58,11 @@
* Banner (Firefly Application Layer banner)
*
*
+ * The request context is projected from the validated security principal
+ * exposed by the {@code fireflyframework-security} platform ({@link SecurityContextPort}). When a
+ * {@link SecurityContextPort} bean is present the library wires a {@link DefaultContextResolver};
+ * otherwise an application may contribute its own {@link ContextResolver}.
+ *
* @author Firefly Development Team
* @since 1.0.0
*/
@@ -152,17 +156,20 @@ public SecurityAspect securityAspect(SecurityAuthorizationService authorizationS
/**
* Creates the default context resolver bean.
- * Uses {@link ObjectProvider} for the optional {@link SessionManager} dependency.
*
- * @param sessionManagerProvider optional session manager provider
+ * Wired only when a {@link SecurityContextPort} bean is available (typically contributed by the
+ * {@code fireflyframework-security} platform). The resolver projects the request context from the
+ * validated security principal — no trusted transport header is read.
+ *
+ * @param securityContextPort the platform-provided accessor for the current validated principal
* @return DefaultContextResolver instance
*/
@Bean
+ @ConditionalOnBean(SecurityContextPort.class)
@ConditionalOnMissingBean(ContextResolver.class)
- public DefaultContextResolver defaultContextResolver(
- ObjectProvider> sessionManagerProvider) {
- log.info("Creating DefaultContextResolver bean");
- return new DefaultContextResolver(sessionManagerProvider.getIfAvailable());
+ public DefaultContextResolver defaultContextResolver(SecurityContextPort securityContextPort) {
+ log.info("Creating DefaultContextResolver bean (backed by SecurityContextPort)");
+ return new DefaultContextResolver(securityContextPort);
}
/**
@@ -179,17 +186,17 @@ public DefaultConfigResolver defaultConfigResolver() {
/**
* Creates the default security authorization service bean.
- * Uses {@link ObjectProvider} for the optional {@link SessionManager} dependency.
*
- * @param sessionManagerProvider optional session manager provider
+ * Authorization is evaluated solely from the roles and permissions already resolved into the
+ * {@link org.fireflyframework.common.application.context.AppContext}.
+ *
* @return DefaultSecurityAuthorizationService instance
*/
@Bean
@ConditionalOnMissingBean(SecurityAuthorizationService.class)
- public DefaultSecurityAuthorizationService defaultSecurityAuthorizationService(
- ObjectProvider> sessionManagerProvider) {
+ public DefaultSecurityAuthorizationService defaultSecurityAuthorizationService() {
log.info("Creating DefaultSecurityAuthorizationService bean");
- return new DefaultSecurityAuthorizationService(sessionManagerProvider.getIfAvailable());
+ return new DefaultSecurityAuthorizationService();
}
/**
diff --git a/src/main/java/org/fireflyframework/common/application/config/ApplicationLayerProperties.java b/src/main/java/org/fireflyframework/common/application/config/ApplicationLayerProperties.java
index fe0d55b..c64c681 100644
--- a/src/main/java/org/fireflyframework/common/application/config/ApplicationLayerProperties.java
+++ b/src/main/java/org/fireflyframework/common/application/config/ApplicationLayerProperties.java
@@ -22,19 +22,19 @@
/**
* Configuration properties for the Application Layer.
- *
+ *
* Configure in application.yml:
*
* firefly:
* application:
* security:
* enabled: true
- * use-security-center: true
+ * use-policy-engine: true
* context:
* cache-enabled: true
* cache-ttl: 300
*
- *
+ *
* @author Firefly Development Team
* @since 1.0.0
*/
@@ -42,22 +42,22 @@
@Validated
@ConfigurationProperties(prefix = "firefly.application")
public class ApplicationLayerProperties {
-
+
/**
* Security configuration
*/
private Security security = new Security();
-
+
/**
* Context resolution configuration
*/
private Context context = new Context();
-
+
/**
* Configuration management settings
*/
private Config config = new Config();
-
+
@Data
public static class Security {
/**
@@ -72,9 +72,11 @@ public static class Security {
private boolean enforce = true;
/**
- * Whether to use SecurityCenter for authorization
+ * Whether to delegate complex authorization decisions to an external policy engine.
+ * When {@code false}, authorization is evaluated solely from the roles and permissions
+ * already resolved into the request {@code AppContext}.
*/
- private boolean useSecurityCenter = true;
+ private boolean usePolicyEngine = true;
/**
* Claim path in JWT token for extracting roles.
@@ -97,37 +99,37 @@ public static class Security {
*/
private boolean failOnMissing = false;
}
-
+
@Data
public static class Context {
/**
* Whether context caching is enabled
*/
private boolean cacheEnabled = true;
-
+
/**
* Context cache TTL in seconds
*/
private int cacheTtl = 300;
-
+
/**
* Maximum cache size
*/
private int cacheMaxSize = 1000;
}
-
+
@Data
public static class Config {
/**
* Whether config caching is enabled
*/
private boolean cacheEnabled = true;
-
+
/**
* Config cache TTL in seconds
*/
private int cacheTtl = 600;
-
+
/**
* Whether to refresh config on startup
*/
diff --git a/src/main/java/org/fireflyframework/common/application/context/AppContext.java b/src/main/java/org/fireflyframework/common/application/context/AppContext.java
index 282f358..77fabef 100644
--- a/src/main/java/org/fireflyframework/common/application/context/AppContext.java
+++ b/src/main/java/org/fireflyframework/common/application/context/AppContext.java
@@ -20,158 +20,71 @@
import lombok.Value;
import lombok.With;
-import jakarta.validation.constraints.NotNull;
+import java.util.Map;
import java.util.Set;
import java.util.UUID;
/**
- * Immutable business context container for application requests.
- * Contains information about the party (customer), contract, and product involved in the request.
- *
- * This class represents the "who", "what", and "where" of a business operation:
- *
- * - partyId: Who is making the request (customer/user)
- * - contractId: What contract/agreement is involved
- * - productId: What product is being accessed/modified
- *
- *
- *
- * This context is used for authorization decisions and domain logic execution.
- *
- * @author Firefly Development Team
- * @since 1.0.0
+ * Immutable, product-agnostic request context. It carries the authenticated {@code subject}, an
+ * optional generic {@code tenantId}, the granted {@code roles}/{@code permissions}, and an open
+ * {@code attributes} map. It deliberately holds no product-domain concepts —
+ * products carry their own model (e.g. party/contract/product references) inside {@link #attributes}.
+ *
+ * The {@code subject} and authorities are a projection of the validated security principal
+ * (see the {@code fireflyframework-security} platform), not of any trusted transport header.
*/
@Value
@Builder(toBuilder = true)
@With
public class AppContext {
-
- /**
- * Unique identifier of the party (customer) making the request.
- * This comes from common-platform-customer-mgmt.
- */
- @NotNull
- UUID partyId;
-
- /**
- * Unique identifier of the contract associated with this request.
- * This comes from common-platform-contract-mgmt.
- * Optional for operations that don't require a contract context.
- */
- UUID contractId;
-
- /**
- * Unique identifier of the product being accessed or modified.
- * This comes from common-platform-product-mgmt.
- * Optional for operations that don't require a product context.
- */
- UUID productId;
-
- /**
- * Roles that the party has in the context of this contract/product.
- * Used for authorization decisions.
- */
+
+ /** Authenticated subject identifier (OIDC {@code sub}); from the validated security principal. */
+ String subject;
+
+ /** Generic tenant discriminator; {@code null} when single-tenant. */
+ UUID tenantId;
+
+ /** Granted authorities/roles, used for authorization decisions. */
Set roles;
-
- /**
- * Permissions that the party has in this context.
- * Derived from roles and used for fine-grained authorization.
- */
+
+ /** Granted fine-grained permissions/scopes. */
Set permissions;
-
- /**
- * The tenant/organization this context belongs to.
- * Links to AppConfig's tenantId.
- */
- UUID tenantId;
-
- /**
- * Additional context-specific attributes.
- * Can be used to store domain-specific context information.
- */
- java.util.Map attributes;
-
- /**
- * Checks if the context has a specific role
- *
- * @param role the role to check
- * @return true if the role is present
- */
+
+ /** Open, product-defined attributes (also the ABAC input bag). */
+ Map attributes;
+
public boolean hasRole(String role) {
return roles != null && roles.contains(role);
}
-
- /**
- * Checks if the context has any of the specified roles
- *
- * @param roles the roles to check
- * @return true if any of the roles are present
- */
- public boolean hasAnyRole(String... roles) {
- if (this.roles == null || roles == null) {
+
+ public boolean hasAnyRole(String... candidates) {
+ if (roles == null || candidates == null) {
return false;
}
- for (String role : roles) {
- if (this.roles.contains(role)) {
+ for (String role : candidates) {
+ if (roles.contains(role)) {
return true;
}
}
return false;
}
-
- /**
- * Checks if the context has all of the specified roles
- *
- * @param roles the roles to check
- * @return true if all roles are present
- */
- public boolean hasAllRoles(String... roles) {
- if (this.roles == null || roles == null) {
+
+ public boolean hasAllRoles(String... candidates) {
+ if (roles == null || candidates == null) {
return false;
}
- for (String role : roles) {
- if (!this.roles.contains(role)) {
+ for (String role : candidates) {
+ if (!roles.contains(role)) {
return false;
}
}
return true;
}
-
- /**
- * Checks if the context has a specific permission
- *
- * @param permission the permission to check
- * @return true if the permission is present
- */
+
public boolean hasPermission(String permission) {
return permissions != null && permissions.contains(permission);
}
-
- /**
- * Checks if this context has a contract association
- *
- * @return true if contractId is present
- */
- public boolean hasContract() {
- return contractId != null;
- }
-
- /**
- * Checks if this context has a product association
- *
- * @return true if productId is present
- */
- public boolean hasProduct() {
- return productId != null;
- }
-
- /**
- * Gets an attribute from the context
- *
- * @param key the attribute key
- * @param the expected type
- * @return the attribute value or null if not present
- */
+
@SuppressWarnings("unchecked")
public T getAttribute(String key) {
return attributes != null ? (T) attributes.get(key) : null;
diff --git a/src/main/java/org/fireflyframework/common/application/context/AppSecurityContext.java b/src/main/java/org/fireflyframework/common/application/context/AppSecurityContext.java
index 3b302ff..e82d0b3 100644
--- a/src/main/java/org/fireflyframework/common/application/context/AppSecurityContext.java
+++ b/src/main/java/org/fireflyframework/common/application/context/AppSecurityContext.java
@@ -26,18 +26,18 @@
/**
* Immutable security context for application requests.
* Contains security-related information including endpoint-role mappings and authorization results.
- *
- * This class is used in conjunction with the Firefly SecurityCenter to determine
- * whether a party has sufficient rights to perform an operation based on their role
- * in a contract/product context.
- *
+ *
+ * This class describes the security requirements of an endpoint (the roles and permissions it
+ * demands) together with the outcome of evaluating those requirements against the authenticated
+ * subject's granted authorities.
+ *
* Security context can be configured in two ways:
*
* - Declarative: Using @Secure annotation on endpoints/controllers
* - Programmatic: Explicit endpoint-role mapping registration
*
*
- *
+ *
* @author Firefly Development Team
* @since 1.0.0
*/
@@ -45,105 +45,105 @@
@Builder(toBuilder = true)
@With
public class AppSecurityContext {
-
+
/**
* The endpoint being accessed (e.g., "/api/v1/accounts/{id}/transfer")
*/
String endpoint;
-
+
/**
* The HTTP method being used (GET, POST, PUT, DELETE, etc.)
*/
String httpMethod;
-
+
/**
* Roles required to access this endpoint
*/
Set requiredRoles;
-
+
/**
* Permissions required to access this endpoint
*/
Set requiredPermissions;
-
+
/**
* Whether authorization was successful
*/
boolean authorized;
-
+
/**
* Reason for authorization failure (if applicable)
*/
String authorizationFailureReason;
-
+
/**
- * Source of the security configuration (ANNOTATION, EXPLICIT_MAP, SECURITY_CENTER)
+ * Source of the security configuration (ANNOTATION, EXPLICIT_MAP, POLICY, DEFAULT)
*/
SecurityConfigSource configSource;
-
+
/**
* Additional security attributes
*/
Map securityAttributes;
-
+
/**
* Whether this endpoint requires authentication
*/
@Builder.Default
boolean requiresAuthentication = true;
-
+
/**
* Whether this endpoint allows anonymous access
*/
@Builder.Default
boolean allowAnonymous = false;
-
+
/**
- * Custom security evaluation result from SecurityCenter
+ * Custom security evaluation result from the policy engine
*/
SecurityEvaluationResult evaluationResult;
-
+
/**
* Checks if the security context requires any roles
- *
+ *
* @return true if roles are required
*/
public boolean hasRequiredRoles() {
return requiredRoles != null && !requiredRoles.isEmpty();
}
-
+
/**
* Checks if the security context requires any permissions
- *
+ *
* @return true if permissions are required
*/
public boolean hasRequiredPermissions() {
return requiredPermissions != null && !requiredPermissions.isEmpty();
}
-
+
/**
* Checks if a specific role is required
- *
+ *
* @param role the role to check
* @return true if the role is required
*/
public boolean requiresRole(String role) {
return requiredRoles != null && requiredRoles.contains(role);
}
-
+
/**
* Checks if a specific permission is required
- *
+ *
* @param permission the permission to check
* @return true if the permission is required
*/
public boolean requiresPermission(String permission) {
return requiredPermissions != null && requiredPermissions.contains(permission);
}
-
+
/**
* Gets a security attribute
- *
+ *
* @param key the attribute key
* @param the expected type
* @return the attribute value or null if not found
@@ -152,7 +152,7 @@ public boolean requiresPermission(String permission) {
public T getSecurityAttribute(String key) {
return securityAttributes != null ? (T) securityAttributes.get(key) : null;
}
-
+
/**
* Source of security configuration
*/
@@ -161,59 +161,59 @@ public enum SecurityConfigSource {
* Security configuration from @Secure annotation
*/
ANNOTATION,
-
+
/**
* Security configuration from explicit endpoint-role mapping
*/
EXPLICIT_MAP,
-
+
/**
- * Security configuration from Firefly SecurityCenter
+ * Security configuration from an external policy engine
*/
- SECURITY_CENTER,
-
+ POLICY,
+
/**
* Security configuration from default/fallback rules
*/
DEFAULT
}
-
+
/**
- * Result of security evaluation from SecurityCenter
+ * Result of a policy-engine security evaluation
*/
@Value
@Builder(toBuilder = true)
@With
public static class SecurityEvaluationResult {
-
+
/**
* Whether access is granted
*/
boolean granted;
-
+
/**
* Reason for the decision
*/
String reason;
-
+
/**
* Rule or policy that was evaluated
*/
String evaluatedPolicy;
-
+
/**
* Additional evaluation details
*/
Map evaluationDetails;
-
+
/**
* Timestamp of evaluation
*/
java.time.Instant evaluatedAt;
-
+
/**
* Gets an evaluation detail
- *
+ *
* @param key the detail key
* @param the expected type
* @return the detail value or null if not found
diff --git a/src/main/java/org/fireflyframework/common/application/context/ApplicationExecutionContext.java b/src/main/java/org/fireflyframework/common/application/context/ApplicationExecutionContext.java
index 5d69f5d..6a80937 100644
--- a/src/main/java/org/fireflyframework/common/application/context/ApplicationExecutionContext.java
+++ b/src/main/java/org/fireflyframework/common/application/context/ApplicationExecutionContext.java
@@ -21,136 +21,63 @@
import lombok.With;
import jakarta.validation.constraints.NotNull;
+import java.util.UUID;
/**
- * Complete execution context for an application request.
- * Aggregates all contextual information needed for request processing.
- *
- * This is the main context object that flows through the application layer,
- * containing all necessary information for:
- *
- * - Business context and authorization (AppContext)
- * - Tenant configuration and providers (AppConfig)
- * - Security and access control (AppSecurityContext)
- *
- *
- * Note: Application metadata ({@code @FireflyApplication}) is now application-level (singleton),
- * not per-request, and accessed via {@code AppMetadataProvider}.
- *
- * Usage example:
- *
- * ApplicationExecutionContext context = ApplicationExecutionContext.builder()
- * .context(appContext)
- * .config(appConfig)
- * .securityContext(securityContext)
- * .build();
- *
- *
- * @author Firefly Development Team
- * @since 1.0.0
+ * Complete execution context for an application request: the product-agnostic {@link AppContext},
+ * the tenant {@link AppConfig}, and the {@link AppSecurityContext}.
*/
@Value
@Builder(toBuilder = true)
@With
public class ApplicationExecutionContext {
-
- /**
- * Business context (partyId, contractId, productId, roles, permissions)
- */
+
+ /** Product-agnostic request context (subject, tenant, roles, permissions, attributes). */
@NotNull
AppContext context;
-
- /**
- * Application configuration (tenantId, providers, feature flags)
- */
+
+ /** Application/tenant configuration (tenantId, providers, feature flags). */
@NotNull
AppConfig config;
-
- /**
- * Security context (endpoint security, authorization results)
- */
+
+ /** Security context (endpoint security, authorization results). */
AppSecurityContext securityContext;
-
+
/**
- * Creates a minimal execution context with only required fields
- *
- * @param partyId the party ID
- * @param tenantId the tenant ID
+ * Creates a minimal execution context with only the required fields.
+ *
+ * @param subject the authenticated subject
+ * @param tenantId the tenant id
* @return a new ApplicationExecutionContext
*/
- public static ApplicationExecutionContext createMinimal(java.util.UUID partyId, java.util.UUID tenantId) {
+ public static ApplicationExecutionContext createMinimal(String subject, UUID tenantId) {
return ApplicationExecutionContext.builder()
- .context(AppContext.builder()
- .partyId(partyId)
- .tenantId(tenantId)
- .build())
- .config(AppConfig.builder()
- .tenantId(tenantId)
- .build())
+ .context(AppContext.builder().subject(subject).tenantId(tenantId).build())
+ .config(AppConfig.builder().tenantId(tenantId).build())
.build();
}
-
- /**
- * Gets the tenant ID from the config
- *
- * @return the tenant ID
- */
- public java.util.UUID getTenantId() {
+
+ /** @return the tenant id from the config. */
+ public UUID getTenantId() {
return config.getTenantId();
}
-
- /**
- * Gets the party ID from the context
- *
- * @return the party ID
- */
- public java.util.UUID getPartyId() {
- return context.getPartyId();
- }
-
- /**
- * Gets the contract ID from the context
- *
- * @return the contract ID (may be null)
- */
- public java.util.UUID getContractId() {
- return context.getContractId();
- }
-
- /**
- * Gets the product ID from the context
- *
- * @return the product ID (may be null)
- */
- public java.util.UUID getProductId() {
- return context.getProductId();
+
+ /** @return the authenticated subject from the context. */
+ public String getSubject() {
+ return context.getSubject();
}
-
- /**
- * Checks if the context is authorized
- *
- * @return true if security context exists and is authorized
- */
+
+ /** @return true if a security context exists and authorization succeeded. */
public boolean isAuthorized() {
return securityContext != null && securityContext.isAuthorized();
}
-
- /**
- * Checks if the context has a specific role
- *
- * @param role the role to check
- * @return true if the role is present in the context
- */
+
+ /** @return true if the context holds the given role. */
public boolean hasRole(String role) {
return context.hasRole(role);
}
-
- /**
- * Checks if a feature is enabled for this tenant
- *
- * @param feature the feature flag name
- * @return true if the feature is enabled
- */
+
+ /** @return true if the feature is enabled for this tenant. */
public boolean isFeatureEnabled(String feature) {
return config.isFeatureEnabled(feature);
}
diff --git a/src/main/java/org/fireflyframework/common/application/controller/AbstractApplicationController.java b/src/main/java/org/fireflyframework/common/application/controller/AbstractApplicationController.java
index f6724cd..1921b20 100644
--- a/src/main/java/org/fireflyframework/common/application/controller/AbstractApplicationController.java
+++ b/src/main/java/org/fireflyframework/common/application/controller/AbstractApplicationController.java
@@ -17,6 +17,7 @@
package org.fireflyframework.common.application.controller;
import org.fireflyframework.common.application.context.ApplicationExecutionContext;
+import org.fireflyframework.common.application.context.AppContext;
import org.fireflyframework.common.application.resolver.ContextResolver;
import org.fireflyframework.common.application.resolver.ConfigResolver;
import lombok.extern.slf4j.Slf4j;
@@ -26,100 +27,96 @@
/**
* Abstract Base Controller for Application-Layer Endpoints
- *
- * This base class is for controllers that operate on application-level resources
- * without requiring a contract or product context. Perfect for onboarding, product catalogs,
- * or any operation that only needs the authenticated party identity.
- *
+ *
+ * This base class is for controllers that operate on application-level resources.
+ * It resolves the authenticated identity and tenant configuration into a single
+ * {@link ApplicationExecutionContext}, perfect for onboarding, catalogs, or any operation that only
+ * needs the authenticated subject and tenant.
+ *
* When to Use
* Extend this class when building REST endpoints for:
*
- * - Onboarding: Customer registration, KYC verification
- * - Product Catalog: Listing available products for a party
- * - Party Profile: Managing party information, preferences
- * - Contract Creation: Requesting new contracts or products
+ * - Onboarding: Registration and verification flows
+ * - Catalogs: Listing resources available to the subject
+ * - Profile: Managing subject information and preferences
*
- *
+ *
* Architecture
* This controller automatically resolves:
*
- * - Party ID: Extracted from Istio-injected
X-Party-Id header
- * - Tenant ID: Extracted from Istio-injected
X-Tenant-Id header
- * - Roles/Permissions: Enriched from platform SDKs
- * - Tenant Config: Loaded from configuration service
+ * - Subject: The authenticated subject from the validated security context
+ * - Tenant ID: The tenant the subject belongs to
+ * - Roles/Permissions: Authorities and scopes resolved for the subject
+ * - Tenant Config: Loaded from the configuration service
*
- *
+ *
* Quick Example
*
* {@code
* @RestController
* @RequestMapping("/api/v1/onboarding")
* public class OnboardingController extends AbstractApplicationController {
- *
+ *
* @Autowired
* private OnboardingApplicationService onboardingService;
- *
+ *
* @PostMapping("/start")
- * @Secure(requireParty = true)
+ * @Secure
* public Mono startOnboarding(
* @RequestBody OnboardingRequest request,
* ServerWebExchange exchange) {
- *
- * // Automatically resolved context with party + tenant
+ *
+ * // Automatically resolved context with subject + tenant
* return resolveExecutionContext(exchange)
* .flatMap(context -> onboardingService.startOnboarding(context, request));
* }
* }
* }
*
- *
+ *
* What You Get
*
* - Automatic Context Resolution: {@link #resolveExecutionContext(ServerWebExchange)}
- * - Party + Tenant Only: No contract or product IDs required
+ * - Subject + Tenant: The authenticated identity and its tenant
* - Full Config Access: Tenant configuration, feature flags, providers
* - Security Ready: Works seamlessly with {@code @Secure} annotations
*
- *
+ *
* @author Firefly Development Team
* @since 1.0.0
- * @see AbstractResourceController For resource endpoints (contract + product required)
+ * @see AbstractResourceController For a thin generic base controller
*/
@Slf4j
public abstract class AbstractApplicationController {
-
+
@Autowired
private ContextResolver contextResolver;
-
+
@Autowired
private ConfigResolver configResolver;
-
+
/**
- * Resolves the full application execution context for application-layer endpoints.
- *
+ * Resolves the full {@link ApplicationExecutionContext} for application-layer endpoints.
+ *
* This method:
*
- * - Extracts party ID and tenant ID from Istio headers
- * - Enriches with roles and permissions from platform SDKs
- * - Loads tenant configuration
- * - Returns complete {@link ApplicationExecutionContext}
+ * - Resolves the {@link AppContext} (subject, tenant, roles, permissions) from the
+ * validated security context
+ * - Loads the tenant configuration
+ * - Returns the complete {@link ApplicationExecutionContext}
*
- *
- * Note: Contract and product IDs will be null
- * since this is an application-layer endpoint.
- *
+ *
* @param exchange the server web exchange
- * @return Mono of ApplicationExecutionContext with party and tenant context
+ * @return Mono of ApplicationExecutionContext with subject and tenant context
*/
protected Mono resolveExecutionContext(ServerWebExchange exchange) {
- log.debug("Resolving application-layer execution context (no contract/product)");
-
- // Pass null for contract and product since this is application-layer only
- return contextResolver.resolveContext(exchange, null, null)
+ log.debug("Resolving application-layer execution context");
+
+ return contextResolver.resolveContext(exchange)
.flatMap(appContext -> {
- log.debug("Resolved application context: party={}, tenant={}",
- appContext.getPartyId(), appContext.getTenantId());
-
+ log.debug("Resolved application context: subject={}, tenant={}",
+ appContext.getSubject(), appContext.getTenantId());
+
return configResolver.resolveConfig(appContext.getTenantId())
.map(appConfig -> ApplicationExecutionContext.builder()
.context(appContext)
@@ -129,16 +126,16 @@ protected Mono resolveExecutionContext(ServerWebExc
.doOnSuccess(ctx -> log.debug("Successfully resolved application-layer execution context"))
.doOnError(error -> log.error("Failed to resolve application-layer execution context", error));
}
-
+
/**
- * Logs the current operation with party context.
- *
+ * Logs the current operation.
+ *
* Convenience method for consistent, structured logging.
- *
+ *
* @param exchange the server web exchange
* @param operation a short description of the operation (e.g., "startOnboarding", "submitKYC")
*/
protected void logOperation(ServerWebExchange exchange, String operation) {
- log.info("[Party Operation] {}", operation);
+ log.info("[Operation] {}", operation);
}
}
diff --git a/src/main/java/org/fireflyframework/common/application/controller/AbstractResourceController.java b/src/main/java/org/fireflyframework/common/application/controller/AbstractResourceController.java
index 3b38caa..06afcca 100644
--- a/src/main/java/org/fireflyframework/common/application/controller/AbstractResourceController.java
+++ b/src/main/java/org/fireflyframework/common/application/controller/AbstractResourceController.java
@@ -17,6 +17,7 @@
package org.fireflyframework.common.application.controller;
import org.fireflyframework.common.application.context.ApplicationExecutionContext;
+import org.fireflyframework.common.application.context.AppContext;
import org.fireflyframework.common.application.resolver.ContextResolver;
import org.fireflyframework.common.application.resolver.ConfigResolver;
import lombok.extern.slf4j.Slf4j;
@@ -24,196 +25,139 @@
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
-import java.util.UUID;
-
/**
* Abstract Base Controller for Resource Endpoints
- *
- * This base class is for controllers that operate on contract and product resources.
- * It automatically resolves the full application context including party, tenant, contract, and product.
- * Both contractId and productId are REQUIRED - this controller enforces the complete resource hierarchy.
- *
+ *
+ * This base class is a thin, product-agnostic foundation for REST controllers. It resolves the
+ * request {@link AppContext} (subject, tenant, roles, permissions) from the validated security
+ * context via the {@link ContextResolver}, and loads the tenant {@link org.fireflyframework.common.application.context.AppConfig}
+ * via the {@link ConfigResolver}, exposing both as a single {@link ApplicationExecutionContext}.
+ *
* When to Use
- * Extend this class when building REST endpoints that operate on resources within a contract+product scope:
- *
- * - Accounts: {@code /contracts/{contractId}/products/{productId}/accounts}
- * - Transactions: {@code /contracts/{contractId}/products/{productId}/transactions}
- * - Balances: {@code /contracts/{contractId}/products/{productId}/balances}
- * - Cards: {@code /contracts/{contractId}/products/{productId}/cards}
- * - Limits: {@code /contracts/{contractId}/products/{productId}/limits}
- * - Beneficiaries: {@code /contracts/{contractId}/products/{productId}/beneficiaries}
- *
- *
+ * Extend this class when building REST endpoints that need the authenticated identity and tenant
+ * configuration. There is no built-in resource hierarchy or scoping — model any
+ * domain-specific path segments with your own {@code @PathVariable} parameters.
+ *
* Architecture
* This controller automatically resolves:
*
- * - Party ID: From Istio header
X-Party-Id
- * - Tenant ID: Dynamically fetched from config-mgmt using party ID
- * - Contract ID: From {@code @PathVariable UUID contractId} (REQUIRED)
- * - Product ID: From {@code @PathVariable UUID productId} (REQUIRED)
- * - Roles/Permissions: Enriched from FireflySessionManager based on party+contract+product
- * - Tenant Config: Loaded from configuration service
+ * - Subject: The authenticated subject from the validated security context
+ * - Tenant ID: The tenant the subject belongs to
+ * - Roles/Permissions: Authorities and scopes resolved for the subject
+ * - Tenant Config: Loaded from the configuration service
*
- *
+ *
* Quick Example
*
* {@code
* @RestController
- * @RequestMapping("/api/v1/contracts/{contractId}/products/{productId}/transactions")
+ * @RequestMapping("/api/v1/transactions")
* public class TransactionController extends AbstractResourceController {
- *
+ *
* @Autowired
* private TransactionApplicationService transactionService;
- *
+ *
* @GetMapping
- * @Secure(requireParty = true, requireContract = true, requireProduct = true, requireRole = "transaction:viewer")
- * public Mono> listTransactions(
- * @PathVariable UUID contractId,
- * @PathVariable UUID productId,
- * ServerWebExchange exchange) {
- *
- * // Automatically resolved context with party + tenant + contract + product
- * return resolveExecutionContext(exchange, contractId, productId)
+ * @Secure(requireRole = "transaction:viewer")
+ * public Mono> listTransactions(ServerWebExchange exchange) {
+ *
+ * // Automatically resolved context with subject + tenant + roles + permissions
+ * return resolveExecutionContext(exchange)
* .flatMap(context -> transactionService.listTransactions(context));
* }
* }
* }
*
- *
+ *
* What You Get
*
- * - Automatic Context Resolution: {@link #resolveExecutionContext(ServerWebExchange, UUID, UUID)}
- * - Complete Resource Hierarchy: Party + Tenant + Contract + Product (all required)
- * - Validation: {@link #requireContext(UUID, UUID)} ensures both IDs are not null
+ * - Automatic Context Resolution: {@link #resolveExecutionContext(ServerWebExchange)}
+ * - Raw Context Access: {@link #resolveContext(ServerWebExchange)}
* - Full Config Access: Tenant configuration, feature flags, providers
* - Security Ready: Works seamlessly with {@code @Secure} annotations
*
- *
+ *
* @author Firefly Development Team
* @since 1.0.0
- * @see AbstractApplicationController For application-layer endpoints (no contract/product)
+ * @see AbstractApplicationController For application-layer endpoints
*/
@Slf4j
public abstract class AbstractResourceController {
-
+
@Autowired
private ContextResolver contextResolver;
-
+
@Autowired
private ConfigResolver configResolver;
-
+
+ /**
+ * Resolves the raw {@link AppContext} for the request.
+ *
+ * Delegates to the configured {@link ContextResolver}, which derives the subject, tenant,
+ * roles and permissions from the validated security context.
+ *
+ * @param exchange the server web exchange
+ * @return Mono of AppContext (subject, tenant, roles, permissions, attributes)
+ */
+ protected Mono resolveContext(ServerWebExchange exchange) {
+ return contextResolver.resolveContext(exchange);
+ }
+
/**
- * Resolves the full application execution context for resource endpoints.
- *
+ * Resolves the full {@link ApplicationExecutionContext} for the request.
+ *
* This method:
*
- * - Validates contractId and productId are not null (BOTH REQUIRED)
- * - Extracts party ID from Istio-injected
X-Party-Id header
- * - Fetches tenant ID from config-mgmt using party ID
- * - Uses the provided contractId and productId from {@code @PathVariable}
- * - Enriches with roles and permissions from FireflySessionManager (party+contract+product scope)
- * - Loads tenant configuration
- * - Returns complete {@link ApplicationExecutionContext}
+ * - Resolves the {@link AppContext} (subject, tenant, roles, permissions) from the
+ * validated security context
+ * - Loads the tenant configuration
+ * - Returns the complete {@link ApplicationExecutionContext}
*
- *
+ *
* @param exchange the server web exchange
- * @param contractId the contract ID from {@code @PathVariable} (REQUIRED)
- * @param productId the product ID from {@code @PathVariable} (REQUIRED)
- * @return Mono of ApplicationExecutionContext with complete resource hierarchy
- * @throws IllegalArgumentException if contractId or productId is null
+ * @return Mono of ApplicationExecutionContext with context and config
*/
- protected Mono resolveExecutionContext(
- ServerWebExchange exchange, UUID contractId, UUID productId) {
-
- requireContext(contractId, productId);
- log.debug("Resolving resource execution context for contract: {}, product: {}",
- contractId, productId);
-
- // Pass both contractId and productId (both required)
- return contextResolver.resolveContext(exchange, contractId, productId)
+ protected Mono resolveExecutionContext(ServerWebExchange exchange) {
+ log.debug("Resolving execution context");
+
+ return contextResolver.resolveContext(exchange)
.flatMap(appContext -> {
- log.debug("Resolved resource context: party={}, tenant={}, contract={}, product={}",
- appContext.getPartyId(), appContext.getTenantId(),
- appContext.getContractId(), appContext.getProductId());
-
+ log.debug("Resolved context: subject={}, tenant={}",
+ appContext.getSubject(), appContext.getTenantId());
+
return configResolver.resolveConfig(appContext.getTenantId())
.map(appConfig -> ApplicationExecutionContext.builder()
.context(appContext)
.config(appConfig)
.build());
})
- .doOnSuccess(ctx -> log.debug("Successfully resolved resource execution context"))
- .doOnError(error -> log.error("Failed to resolve resource execution context", error));
- }
-
- /**
- * Validates that both contractId and productId are not null.
- *
- * IMPORTANT: This controller REQUIRES both contractId and productId.
- * Call this method (or let {@link #resolveExecutionContext} call it automatically) to ensure
- * both path variables are present.
- *
- * Example:
- *
- * {@code
- * @GetMapping("/{transactionId}")
- * public Mono getTransaction(
- * @PathVariable UUID contractId,
- * @PathVariable UUID productId,
- * @PathVariable UUID transactionId) {
- * requireContext(contractId, productId); // Validates both IDs are present
- * // ... rest of your logic
- * }
- * }
- *
- *
- * @param contractId the contract ID from the path variable (REQUIRED)
- * @param productId the product ID from the path variable (REQUIRED)
- * @throws IllegalArgumentException if contractId or productId is null
- */
- protected final void requireContext(UUID contractId, UUID productId) {
- if (contractId == null) {
- log.error("Missing required path variable: contractId");
- throw new IllegalArgumentException(
- "contractId is required but was null. Check your @PathVariable mapping."
- );
- }
- if (productId == null) {
- log.error("Missing required path variable: productId");
- throw new IllegalArgumentException(
- "productId is required but was null. Check your @PathVariable mapping."
- );
- }
- log.trace("Resource context validated - Contract: {}, Product: {}", contractId, productId);
+ .doOnSuccess(ctx -> log.debug("Successfully resolved execution context"))
+ .doOnError(error -> log.error("Failed to resolve execution context", error));
}
-
-
+
/**
- * Logs the current operation with full resource context.
- *
- * This is a convenience method for adding consistent, structured logging
- * to your endpoints. It logs at INFO level with both contract and product IDs.
- *
+ * Logs the current operation.
+ *
+ * This is a convenience method for adding consistent, structured logging to your endpoints.
+ * It logs at INFO level.
+ *
* Example:
*
* {@code
* @PostMapping
* public Mono createTransaction(
- * @PathVariable UUID contractId,
- * @PathVariable UUID productId,
- * @RequestBody CreateTransactionRequest request) {
- * logOperation(contractId, productId, "createTransaction");
- * return resolveExecutionContext(exchange, contractId, productId)
+ * @RequestBody CreateTransactionRequest request,
+ * ServerWebExchange exchange) {
+ * logOperation("createTransaction");
+ * return resolveExecutionContext(exchange)
* .flatMap(context -> transactionService.createTransaction(context, request));
* }
* }
*
- *
- * @param contractId the contract ID
- * @param productId the product ID
+ *
* @param operation a short description of the operation (e.g., "createTransaction", "listAccounts")
*/
- protected final void logOperation(UUID contractId, UUID productId, String operation) {
- log.info("[Resource] Contract: {}, Product: {}, Operation: {}", contractId, productId, operation);
+ protected final void logOperation(String operation) {
+ log.info("[Operation] {}", operation);
}
}
diff --git a/src/main/java/org/fireflyframework/common/application/resolver/AbstractContextResolver.java b/src/main/java/org/fireflyframework/common/application/resolver/AbstractContextResolver.java
index 0c4fa25..fa9aee8 100644
--- a/src/main/java/org/fireflyframework/common/application/resolver/AbstractContextResolver.java
+++ b/src/main/java/org/fireflyframework/common/application/resolver/AbstractContextResolver.java
@@ -17,201 +17,43 @@
package org.fireflyframework.common.application.resolver;
import org.fireflyframework.common.application.context.AppContext;
-import org.fireflyframework.common.application.context.AppMetadata;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
+import java.util.Optional;
import java.util.Set;
-import java.util.UUID;
/**
- * Abstract base implementation of ContextResolver.
- * Provides common functionality and template methods for context resolution.
- *
- * Subclasses should implement the abstract methods to provide specific
- * resolution strategies for their use case.
- *
- * This class integrates with platform SDKs to fetch context data:
- *
- * - common-platform-customer-mgmt-sdk: For party/customer information
- * - common-platform-contract-mgmt-sdk: For contract information
- * - common-platform-product-mgmt: For product information
- *
- *
- * @author Firefly Development Team
- * @since 1.0.0
+ * Template base for {@link ContextResolver}s: assembles an {@link AppContext} from the resolved
+ * subject, tenant, roles and permissions. Subclasses provide {@link #resolveSubject} and
+ * {@link #resolveTenantId}; roles/permissions default to empty and can be overridden.
*/
@Slf4j
public abstract class AbstractContextResolver implements ContextResolver {
-
+
@Override
public Mono resolveContext(ServerWebExchange exchange) {
- log.debug("Resolving application context for request (deprecated - use version with explicit IDs)");
-
- return Mono.zip(
- resolvePartyId(exchange),
- resolveTenantId(exchange),
- resolveContractId(exchange).defaultIfEmpty(UUID.randomUUID()), // placeholder UUID if empty
- resolveProductId(exchange).defaultIfEmpty(UUID.randomUUID()) // placeholder UUID if empty
- )
- .flatMap(tuple -> {
- UUID partyId = tuple.getT1();
- UUID tenantId = tuple.getT2();
- UUID contractId = tuple.getT3();
- UUID productId = tuple.getT4();
-
- return enrichContext(
- AppContext.builder()
- .partyId(partyId)
- .tenantId(tenantId)
- .contractId(contractId)
- .productId(productId)
- .build(),
- exchange
- );
- })
- .doOnSuccess(context -> log.debug("Successfully resolved context for party: {}", context.getPartyId()))
- .doOnError(error -> log.error("Failed to resolve context", error));
- }
-
- @Override
- public Mono resolveContext(ServerWebExchange exchange, UUID contractId, UUID productId) {
- log.debug("Resolving application context with explicit contract: {} and product: {}", contractId, productId);
-
- return Mono.zip(
- resolvePartyId(exchange),
- resolveTenantId(exchange)
- )
- .flatMap(tuple -> {
- UUID partyId = tuple.getT1();
- UUID tenantId = tuple.getT2();
-
- return enrichContext(
- AppContext.builder()
- .partyId(partyId)
- .tenantId(tenantId)
- .contractId(contractId) // Explicit from controller
- .productId(productId) // Explicit from controller
- .build(),
- exchange
- );
- })
- .doOnSuccess(context -> log.debug("Successfully resolved context for party: {}, contract: {}, product: {}",
- context.getPartyId(), context.getContractId(), context.getProductId()))
- .doOnError(error -> log.error("Failed to resolve context", error));
- }
-
- /**
- * Enriches the basic context with roles, permissions, and additional data.
- * This method should fetch data from platform services.
- *
- * @param basicContext the basic context with IDs
- * @param exchange the server web exchange
- * @return Mono of enriched AppContext
- */
- protected Mono enrichContext(AppContext basicContext,
- ServerWebExchange exchange) {
- return Mono.zip(
- resolveRoles(basicContext, exchange),
- resolvePermissions(basicContext, exchange)
- )
- .map(tuple -> basicContext.toBuilder()
- .roles(tuple.getT1())
- .permissions(tuple.getT2())
- .build())
- .defaultIfEmpty(basicContext);
+ return resolveSubject(exchange).flatMap(subject -> Mono.zip(
+ resolveTenantId(exchange).map(Optional::of).defaultIfEmpty(Optional.empty()),
+ resolveRoles(subject, exchange),
+ resolvePermissions(subject, exchange))
+ .map(tuple -> AppContext.builder()
+ .subject(subject)
+ .tenantId(tuple.getT1().orElse(null))
+ .roles(tuple.getT2())
+ .permissions(tuple.getT3())
+ .build()))
+ .doOnError(error -> log.error("Failed to resolve application context", error));
}
-
- /**
- * Resolves roles for the party in the context of the contract/product.
- *
- * TODO: Implementation should use common-platform-customer-mgmt-sdk and
- * common-platform-contract-mgmt-sdk to fetch the party's roles in the contract.
- *
- * @param context the application context
- * @param exchange the server web exchange
- * @return Mono of role set
- */
- protected Mono> resolveRoles(AppContext context, ServerWebExchange exchange) {
- // TODO: Implement role resolution using platform SDKs
- // Example:
- // return customerManagementClient.getPartyRoles(context.getPartyId(), context.getContractId())
- // .map(response -> response.getRoles());
-
- log.debug("Resolving roles for party: {} in contract: {}",
- context.getPartyId(), context.getContractId());
+
+ /** Resolve roles for the subject (default empty; override to enrich). */
+ protected Mono> resolveRoles(String subject, ServerWebExchange exchange) {
return Mono.just(Set.of());
}
-
- /**
- * Resolves permissions for the party in the context of the contract/product.
- *
- * TODO: Implementation should use common-platform-contract-mgmt-sdk to fetch
- * the party's permissions based on their roles in the contract.
- *
- * @param context the application context
- * @param exchange the server web exchange
- * @return Mono of permission set
- */
- protected Mono> resolvePermissions(AppContext context, ServerWebExchange exchange) {
- // TODO: Implement permission resolution using platform SDKs
- // Example:
- // return contractManagementClient.getPartyPermissions(
- // context.getPartyId(),
- // context.getContractId(),
- // context.getProductId()
- // ).map(response -> response.getPermissions());
-
- log.debug("Resolving permissions for party: {} in contract: {}, product: {}",
- context.getPartyId(), context.getContractId(), context.getProductId());
+
+ /** Resolve permissions for the subject (default empty; override to enrich). */
+ protected Mono> resolvePermissions(String subject, ServerWebExchange exchange) {
return Mono.just(Set.of());
}
-
- /**
- * Extracts UUID from request attribute or header.
- *
- * @param exchange the server web exchange
- * @param attributeName the attribute name
- * @param headerName the header name
- * @return Mono of UUID
- */
- protected Mono extractUUID(ServerWebExchange exchange, String attributeName, String headerName) {
- // Try to get from attribute first
- UUID fromAttribute = exchange.getAttribute(attributeName);
- if (fromAttribute != null) {
- return Mono.just(fromAttribute);
- }
-
- // Try to get from header
- String headerValue = exchange.getRequest().getHeaders().getFirst(headerName);
- if (headerValue != null && !headerValue.isEmpty()) {
- try {
- return Mono.just(UUID.fromString(headerValue));
- } catch (IllegalArgumentException e) {
- log.warn("Invalid UUID format in header {}: {}", headerName, headerValue);
- }
- }
-
- return Mono.empty();
- }
-
- /**
- * Extracts UUID from path variable.
- *
- * @param exchange the server web exchange
- * @param variableName the path variable name
- * @return Mono of UUID
- */
- protected Mono extractUUIDFromPath(ServerWebExchange exchange, String variableName) {
- try {
- String value = exchange.getRequest().getPath().value();
- // This is a simplified implementation
- // In practice, you'd use a proper path matcher or get it from request attributes
- return Mono.empty();
- } catch (Exception e) {
- log.warn("Failed to extract UUID from path variable: {}", variableName, e);
- return Mono.empty();
- }
- }
}
diff --git a/src/main/java/org/fireflyframework/common/application/resolver/ContextResolver.java b/src/main/java/org/fireflyframework/common/application/resolver/ContextResolver.java
index 8c052d7..e4304be 100644
--- a/src/main/java/org/fireflyframework/common/application/resolver/ContextResolver.java
+++ b/src/main/java/org/fireflyframework/common/application/resolver/ContextResolver.java
@@ -23,93 +23,27 @@
import java.util.UUID;
/**
- * Interface for resolving application context from incoming requests.
- * Implementations are responsible for extracting and enriching context information
- * such as partyId, contractId, productId, roles, and permissions.
- *
- * This is the main entry point for context resolution in the application layer.
- *
- * @author Firefly Development Team
- * @since 1.0.0
+ * Resolves the product-agnostic {@link AppContext} for a request from the validated security
+ * context. Implementations derive the subject, tenant, roles and permissions — never from a trusted
+ * transport header.
*/
public interface ContextResolver {
-
- /**
- * Resolves the complete application context from the request.
- * This method extracts all IDs automatically (party, tenant, contract, product).
- *
- * @param exchange the server web exchange
- * @return Mono of resolved AppContext
- */
+
+ /** Resolve the full context (subject + tenant + roles + permissions). */
Mono resolveContext(ServerWebExchange exchange);
-
- /**
- * Resolves the application context with explicit contractId and productId.
- * This is the method controllers should use to pass IDs extracted from {@code @PathVariable}.
- *
- * Party and tenant IDs are still extracted from Istio headers (X-Party-Id, X-Tenant-Id),
- * but contract and product IDs are provided explicitly by the controller.
- *
- * @param exchange the server web exchange
- * @param contractId the contract ID from {@code @PathVariable} (nullable)
- * @param productId the product ID from {@code @PathVariable} (nullable)
- * @return Mono of resolved AppContext
- */
- Mono resolveContext(ServerWebExchange exchange, UUID contractId, UUID productId);
-
- /**
- * Resolves the party ID from the request.
- * This should extract the authenticated user/customer identifier.
- *
- * @param exchange the server web exchange
- * @return Mono of party UUID
- */
- Mono resolvePartyId(ServerWebExchange exchange);
-
- /**
- * Resolves the contract ID from the request.
- * This may come from path parameters, query parameters, or headers.
- *
- * @param exchange the server web exchange
- * @return Mono of contract UUID (may be empty)
- */
- Mono resolveContractId(ServerWebExchange exchange);
-
- /**
- * Resolves the product ID from the request.
- * This may come from path parameters, query parameters, or headers.
- *
- * @param exchange the server web exchange
- * @return Mono of product UUID (may be empty)
- */
- Mono resolveProductId(ServerWebExchange exchange);
-
- /**
- * Resolves the tenant ID from the request.
- * This typically comes from authentication tokens or subdomain.
- *
- * @param exchange the server web exchange
- * @return Mono of tenant UUID
- */
+
+ /** Resolve the authenticated subject identifier. */
+ Mono resolveSubject(ServerWebExchange exchange);
+
+ /** Resolve the generic tenant id (may be empty when single-tenant). */
Mono resolveTenantId(ServerWebExchange exchange);
-
- /**
- * Checks if this resolver supports the given request.
- * Allows for multiple resolver implementations with different strategies.
- *
- * @param exchange the server web exchange
- * @return true if this resolver can handle the request
- */
+
+ /** Whether this resolver supports the given request. */
default boolean supports(ServerWebExchange exchange) {
return true;
}
-
- /**
- * Priority of this resolver (higher values take precedence).
- * Used when multiple resolvers support the same request.
- *
- * @return priority value
- */
+
+ /** Priority of this resolver (higher wins) when multiple support a request. */
default int getPriority() {
return 0;
}
diff --git a/src/main/java/org/fireflyframework/common/application/resolver/DefaultContextResolver.java b/src/main/java/org/fireflyframework/common/application/resolver/DefaultContextResolver.java
index c513d6b..4fa8eb2 100644
--- a/src/main/java/org/fireflyframework/common/application/resolver/DefaultContextResolver.java
+++ b/src/main/java/org/fireflyframework/common/application/resolver/DefaultContextResolver.java
@@ -16,13 +16,10 @@
package org.fireflyframework.common.application.resolver;
-import org.fireflyframework.common.application.context.AppContext;
-import org.fireflyframework.common.application.util.SessionContextMapper;
-import org.fireflyframework.common.application.spi.SessionContext;
-import org.fireflyframework.common.application.spi.SessionManager;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Autowired;
+import org.fireflyframework.security.api.domain.SecurityPrincipal;
+import org.fireflyframework.security.spi.SecurityContextPort;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@@ -30,218 +27,53 @@
import java.util.UUID;
/**
- * Default implementation of ContextResolver.
- *
- * This is provided by the library - microservices don't need to implement anything.
- *
- * This resolver automatically:
- *
- * - Extracts partyId from Istio-injected HTTP header ({@code X-Party-Id})
- * - Resolves tenantId by calling {@code common-platform-config-mgmt} with the partyId
- * - Enriches context with roles and permissions from platform SDKs
- * - Caches results for performance
- *
- *
- * Important: ContractId and ProductId are NOT extracted here.
- * They must be extracted from {@code @PathVariable} in your controllers and passed explicitly.
- *
- * Architecture
- *
- * - Istio Gateway: Validates JWT, injects X-Party-Id header (from JWT subject)
- * - This Resolver: Uses partyId to fetch tenantId from config-mgmt microservice
- * - Controllers: Extract contractId/productId from {@code @PathVariable} in REST path
- * - SDK Enrichment: Fetch roles/permissions from platform SDKs
- *
- *
- * Expected HTTP Headers (Injected by Istio)
- *
- * X-Party-Id - Party UUID (required) - Extracted from authenticated JWT subject
- *
- *
- * Tenant Resolution
- * The tenant ID is NOT in the JWT or headers. Instead, it is resolved dynamically:
- *
- * {@code
- * // Call common-platform-config-mgmt microservice
- * GET /api/v1/parties/{partyId}/tenant
- * Response: { "tenantId": "uuid", "tenantName": "...", ... }
- * }
- *
- *
- * Role & Permission Resolution (SessionManager)
- * Roles and permissions are NOT fetched from individual platform services.
- * Instead, they come from the SessionManager in Security Center:
- *
- * - Session Management: Tracks which contracts a party has access to
- * - Role Mapping: Provides party roles in each contract/product
- * - Role Scopes: Supports party-level, contract-level, and product-level roles
- * - Permission Derivation: Converts roles to permissions using role mappings
- *
- *
- * {@code
- * // Call SessionManager from Security Center
- * PartySession session = sessionManager.getPartySession(partyId, tenantId);
- *
- * // Get contract-specific roles
- * Set roles = session.getContractRoles(contractId, productId);
- * // e.g., ["owner", "account:viewer", "transaction:creator"]
- *
- * // Derive permissions from roles
- * Set permissions = session.getPermissionsForRoles(roles);
- * // e.g., ["account:read", "account:update", "transaction:create"]
- * }
- *
- *
- * Controller Responsibility
- * Controllers must extract contractId and productId from path variables:
- *
- * {@code
- * @GetMapping("/contracts/{contractId}/accounts")
- * public Mono> getAccounts(@PathVariable UUID contractId, ServerWebExchange exchange) {
- * // Controller extracts contractId from path, passes to service
- * }
- * }
- *
- *
- * @author Firefly Development Team
- * @since 1.0.0
+ * Default {@link ContextResolver} provided by the library: it projects the request context from the
+ * validated security principal exposed by the {@code fireflyframework-security}
+ * platform ({@link SecurityContextPort}). The subject is the principal's subject, roles are its
+ * authorities, and permissions are its scopes — no trusted {@code X-Party-Id}-style header is read.
*/
@Slf4j
@RequiredArgsConstructor
public class DefaultContextResolver extends AbstractContextResolver {
-
- @Autowired(required = false)
- private final SessionManager sessionManager;
-
- // TODO: Inject platform SDK clients when available
- // private final ConfigManagementClient configMgmtClient; // For tenant resolution
-
+
+ private final SecurityContextPort securityContextPort;
+
@Override
- public Mono resolvePartyId(ServerWebExchange exchange) {
- log.debug("Resolving party ID from Istio-injected header");
-
- // Party ID is injected by Istio as X-Party-Id header
- return extractUUID(exchange, "partyId", "X-Party-Id")
- .doOnNext(id -> log.debug("Resolved party ID from Istio header: {}", id))
+ public Mono resolveSubject(ServerWebExchange exchange) {
+ return securityContextPort.currentPrincipal()
+ .map(SecurityPrincipal::subject)
.switchIfEmpty(Mono.error(new IllegalStateException(
- "X-Party-Id header not found. Ensure request passes through Istio gateway.")));
+ "No authenticated security principal available to resolve the request subject")));
}
-
+
@Override
public Mono resolveTenantId(ServerWebExchange exchange) {
- log.debug("Resolving tenant ID from config-mgmt using party ID");
-
- // Tenant ID is NOT in headers - must be resolved from config-mgmt microservice
- // First, get the party ID from the header
- return resolvePartyId(exchange)
- .flatMap(partyId -> {
- log.debug("Fetching tenant ID for party: {} from config-mgmt", partyId);
-
- // TODO: Implement using common-platform-config-mgmt-sdk
- // When SDK is available, call:
- /*
- return configMgmtClient.getPartyTenant(partyId)
- .map(response -> response.getTenantId())
- .doOnNext(tenantId -> log.debug("Resolved tenant ID: {} for party: {}", tenantId, partyId));
- */
-
- // Temporary: Try to get from header first (for backwards compatibility during migration)
- // Then fallback to error if not available
- return extractUUID(exchange, "tenantId", "X-Tenant-Id")
- .doOnNext(id -> log.warn("Using X-Tenant-Id header (deprecated) - should fetch from config-mgmt: {}", id))
- .switchIfEmpty(Mono.error(new IllegalStateException(
- "Tenant resolution not implemented. Need to integrate common-platform-config-mgmt-sdk. "
- + "SDK should call: GET /api/v1/parties/" + partyId + "/tenant")));
- })
- .doOnError(error -> log.error("Failed to resolve tenant ID", error));
- }
-
- @Override
- public Mono resolveContractId(ServerWebExchange exchange) {
- // Contract ID is not extracted here - it must be passed explicitly by controllers
- // Controllers extract contractId from @PathVariable and pass it to services
- log.debug("Contract ID resolution delegated to controller layer");
- return Mono.empty();
- }
-
- @Override
- public Mono resolveProductId(ServerWebExchange exchange) {
- // Product ID is not extracted here - it must be passed explicitly by controllers
- // Controllers extract productId from @PathVariable and pass it to services
- log.debug("Product ID resolution delegated to controller layer");
- return Mono.empty();
+ return securityContextPort.currentPrincipal()
+ .flatMap(principal -> {
+ String tenant = principal.tenantId();
+ if (tenant == null || tenant.isBlank()) {
+ return Mono.empty();
+ }
+ try {
+ return Mono.just(UUID.fromString(tenant));
+ } catch (IllegalArgumentException ex) {
+ log.debug("Principal tenantId '{}' is not a UUID; treating as single-tenant", tenant);
+ return Mono.empty();
+ }
+ });
}
-
- @Override
- protected Mono> resolveRoles(AppContext context, ServerWebExchange exchange) {
- log.debug("Resolving roles for party: {} in contract: {}, product: {}",
- context.getPartyId(), context.getContractId(), context.getProductId());
-
- // Check if SessionManager is available
- if (sessionManager == null) {
- log.warn("SessionManager not available - returning empty roles. " +
- "Ensure common-platform-security-center is deployed and accessible.");
- return Mono.just(Set.of());
- }
-
- // Use SessionManager to get session with enriched contract/role data
- return sessionManager.createOrGetSession(exchange)
- .map(session -> {
- // Extract roles using SessionContextMapper based on context scope
- Set roles = SessionContextMapper.extractRoles(
- session,
- context.getContractId(),
- context.getProductId()
- );
-
- log.debug("Resolved {} roles for party {}: {}", roles.size(), context.getPartyId(), roles);
- return roles;
- })
- .doOnError(error -> log.error("Failed to resolve roles from SessionManager: {}",
- error.getMessage(), error))
- .onErrorReturn(Set.of()); // Graceful degradation on error
- }
-
- @Override
- protected Mono> resolvePermissions(AppContext context, ServerWebExchange exchange) {
- log.debug("Resolving permissions for party: {} in contract: {}, product: {}",
- context.getPartyId(), context.getContractId(), context.getProductId());
-
- // Check if SessionManager is available
- if (sessionManager == null) {
- log.warn("SessionManager not available - returning empty permissions. " +
- "Ensure common-platform-security-center is deployed and accessible.");
- return Mono.just(Set.of());
- }
-
- // Use SessionManager to get session with enriched contract/role/permission data
- return sessionManager.createOrGetSession(exchange)
- .map(session -> {
- // Extract permissions from role scopes using SessionContextMapper
- Set permissions = SessionContextMapper.extractPermissions(
- session,
- context.getContractId(),
- context.getProductId()
- );
-
- log.debug("Resolved {} permissions for party {}: {}",
- permissions.size(), context.getPartyId(), permissions);
- return permissions;
- })
- .doOnError(error -> log.error("Failed to resolve permissions from SessionManager: {}",
- error.getMessage(), error))
- .onErrorReturn(Set.of()); // Graceful degradation on error
- }
-
+
@Override
- public boolean supports(ServerWebExchange exchange) {
- // This default resolver supports all requests
- return true;
+ protected Mono> resolveRoles(String subject, ServerWebExchange exchange) {
+ return securityContextPort.currentPrincipal()
+ .map(SecurityPrincipal::authorities)
+ .defaultIfEmpty(Set.of());
}
-
+
@Override
- public int getPriority() {
- // Default priority
- return 0;
+ protected Mono> resolvePermissions(String subject, ServerWebExchange exchange) {
+ return securityContextPort.currentPrincipal()
+ .map(SecurityPrincipal::scopes)
+ .defaultIfEmpty(Set.of());
}
}
diff --git a/src/main/java/org/fireflyframework/common/application/security/AbstractSecurityAuthorizationService.java b/src/main/java/org/fireflyframework/common/application/security/AbstractSecurityAuthorizationService.java
index 22751be..65e79e9 100644
--- a/src/main/java/org/fireflyframework/common/application/security/AbstractSecurityAuthorizationService.java
+++ b/src/main/java/org/fireflyframework/common/application/security/AbstractSecurityAuthorizationService.java
@@ -26,15 +26,17 @@
import org.springframework.expression.spel.support.StandardEvaluationContext;
import reactor.core.publisher.Mono;
-import java.time.Instant;
-
/**
- * Abstract implementation of SecurityAuthorizationService.
- * Provides integration with Firefly SecurityCenter for authorization decisions.
- *
- * This class handles the core authorization logic and delegates to SecurityCenter
- * when needed. Subclasses can override specific methods for custom authorization logic.
- *
+ * Abstract implementation of {@link SecurityAuthorizationService}.
+ *
+ * Authorization decisions are derived solely from the roles and permissions already
+ * resolved into the {@link AppContext}. This keeps the authorization layer fully
+ * product-agnostic: the validated identity (subject, tenant, roles, permissions) is
+ * resolved up-front by the context resolution layer, and this service simply evaluates
+ * the declared requirements against it.
+ *
+ * Subclasses can override specific hook methods for custom authorization logic.
+ *
* @author Firefly Development Team
* @since 1.0.0
*/
@@ -45,15 +47,15 @@ public abstract class AbstractSecurityAuthorizationService implements SecurityAu
@Override
public Mono authorize(AppContext context, AppSecurityContext securityContext) {
- log.debug("Authorizing request for endpoint: {} {} by party: {}",
- securityContext.getHttpMethod(), securityContext.getEndpoint(), context.getPartyId());
-
+ log.debug("Authorizing request for endpoint: {} {} by subject: {}",
+ securityContext.getHttpMethod(), securityContext.getEndpoint(), context.getSubject());
+
// If anonymous access is allowed, grant immediately
if (securityContext.isAllowAnonymous()) {
return Mono.just(securityContext.withAuthorized(true)
.withConfigSource(AppSecurityContext.SecurityConfigSource.DEFAULT));
}
-
+
// Check if roles are required
if (securityContext.hasRequiredRoles()) {
return checkRoles(context, securityContext)
@@ -68,32 +70,32 @@ public Mono authorize(AppContext context, AppSecurityContext
return Mono.just(createAuthorizedContext(securityContext));
});
}
-
+
// Check permissions if specified
if (securityContext.hasRequiredPermissions()) {
return checkPermissions(context, securityContext);
}
-
- // If SecurityCenter should be used, delegate to it
- if (securityContext.getConfigSource() == AppSecurityContext.SecurityConfigSource.SECURITY_CENTER) {
- return authorizeWithSecurityCenter(context, securityContext);
+
+ // If a policy source is configured, delegate to it
+ if (securityContext.getConfigSource() == AppSecurityContext.SecurityConfigSource.POLICY) {
+ return authorizeWithPolicy(context, securityContext);
}
-
+
// Default: allow access if no specific requirements
log.debug("No specific security requirements, allowing access");
return Mono.just(createAuthorizedContext(securityContext));
}
-
+
@Override
public Mono hasRole(AppContext context, String role) {
return Mono.just(context.hasRole(role));
}
-
+
@Override
public Mono hasPermission(AppContext context, String permission) {
return Mono.just(context.hasPermission(permission));
}
-
+
@Override
public Mono evaluateExpression(AppContext context, String expression) {
if (expression == null || expression.isBlank()) {
@@ -112,10 +114,10 @@ public Mono evaluateExpression(AppContext context, String expression) {
return Mono.just(false);
}
}
-
+
/**
* Checks if the context has the required roles.
- *
+ *
* @param context the application context
* @param securityContext the security context
* @return Mono of boolean indicating if roles are satisfied
@@ -124,16 +126,16 @@ protected Mono checkRoles(AppContext context, AppSecurityContext securi
if (securityContext.getRequiredRoles() == null || securityContext.getRequiredRoles().isEmpty()) {
return Mono.just(true);
}
-
+
boolean hasRequiredRoles = securityContext.getRequiredRoles().stream()
.anyMatch(context::hasRole);
-
+
return Mono.just(hasRequiredRoles);
}
-
+
/**
* Checks if the context has the required permissions.
- *
+ *
* @param context the application context
* @param securityContext the security context
* @return Mono of AppSecurityContext with authorization result
@@ -142,66 +144,53 @@ protected Mono checkPermissions(AppContext context, AppSecur
if (securityContext.getRequiredPermissions() == null || securityContext.getRequiredPermissions().isEmpty()) {
return Mono.just(createAuthorizedContext(securityContext));
}
-
+
boolean hasRequiredPermissions = securityContext.getRequiredPermissions().stream()
.anyMatch(context::hasPermission);
-
+
if (hasRequiredPermissions) {
return Mono.just(createAuthorizedContext(securityContext));
} else {
return Mono.just(createUnauthorizedContext(securityContext, "Required permissions not granted"));
}
}
-
+
/**
- * Authorizes a request using the Firefly SecurityCenter.
- *
- * TODO: Implementation should integrate with SecurityCenter to evaluate
- * authorization policies based on the party, contract, product, and endpoint.
- *
+ * Authorizes a request using an external policy source.
+ *
+ * The default implementation evaluates the declared role and permission requirements
+ * against the roles and permissions already present in the {@link AppContext}. Subclasses
+ * may override this method to integrate with a dedicated policy decision point (PDP) for
+ * attribute-based or policy-based access control.
+ *
* @param context the application context
* @param securityContext the security context
* @return Mono of AppSecurityContext with authorization result
*/
- protected Mono authorizeWithSecurityCenter(AppContext context,
- AppSecurityContext securityContext) {
- // TODO: Implement SecurityCenter integration
- // Example:
- // return securityCenterClient.authorize(
- // AuthorizationRequest.builder()
- // .partyId(context.getPartyId())
- // .contractId(context.getContractId())
- // .productId(context.getProductId())
- // .endpoint(securityContext.getEndpoint())
- // .httpMethod(securityContext.getHttpMethod())
- // .roles(context.getRoles())
- // .permissions(context.getPermissions())
- // .build()
- // ).map(response -> {
- // AppSecurityContext.SecurityEvaluationResult evaluationResult =
- // AppSecurityContext.SecurityEvaluationResult.builder()
- // .granted(response.isGranted())
- // .reason(response.getReason())
- // .evaluatedPolicy(response.getPolicyName())
- // .evaluationDetails(response.getDetails())
- // .evaluatedAt(Instant.now())
- // .build();
- //
- // return securityContext.toBuilder()
- // .authorized(response.isGranted())
- // .authorizationFailureReason(response.isGranted() ? null : response.getReason())
- // .configSource(AppSecurityContext.SecurityConfigSource.SECURITY_CENTER)
- // .evaluationResult(evaluationResult)
- // .build();
- // });
-
- log.warn("SecurityCenter integration not implemented, denying access by default");
- return Mono.just(createUnauthorizedContext(securityContext, "SecurityCenter integration not implemented"));
+ protected Mono authorizeWithPolicy(AppContext context,
+ AppSecurityContext securityContext) {
+ // Default policy evaluation falls back to role/permission checks against the resolved context.
+ if (securityContext.hasRequiredRoles()) {
+ return checkRoles(context, securityContext)
+ .flatMap(rolesOk -> {
+ if (!rolesOk) {
+ return Mono.just(createUnauthorizedContext(securityContext, "Required roles not present"));
+ }
+ if (securityContext.hasRequiredPermissions()) {
+ return checkPermissions(context, securityContext);
+ }
+ return Mono.just(createAuthorizedContext(securityContext));
+ });
+ }
+ if (securityContext.hasRequiredPermissions()) {
+ return checkPermissions(context, securityContext);
+ }
+ return Mono.just(createAuthorizedContext(securityContext));
}
-
+
/**
* Creates an authorized security context.
- *
+ *
* @param original the original security context
* @return authorized security context
*/
@@ -209,10 +198,10 @@ protected AppSecurityContext createAuthorizedContext(AppSecurityContext original
return original.withAuthorized(true)
.withAuthorizationFailureReason(null);
}
-
+
/**
* Creates an unauthorized security context with a reason.
- *
+ *
* @param original the original security context
* @param reason the reason for denial
* @return unauthorized security context
diff --git a/src/main/java/org/fireflyframework/common/application/security/AbstractSecurityConfiguration.java b/src/main/java/org/fireflyframework/common/application/security/AbstractSecurityConfiguration.java
index 3527d38..5f0a728 100644
--- a/src/main/java/org/fireflyframework/common/application/security/AbstractSecurityConfiguration.java
+++ b/src/main/java/org/fireflyframework/common/application/security/AbstractSecurityConfiguration.java
@@ -25,12 +25,12 @@
/**
* Abstract Base Class for Declarative Endpoint Security Configuration
- *
+ *
* This abstract class simplifies the process of configuring endpoint security using
* the {@link EndpointSecurityRegistry}. Instead of manually calling registry methods,
* you can extend this class and override {@link #configureEndpointSecurity()} to
* define security rules in a clean, declarative way.
- *
+ *
* Why Use This?
*
* - Cleaner code: Declarative API vs manual registry calls
@@ -38,36 +38,36 @@
* - Readability: Security rules are clear and organized
* - Flexibility: Override annotations dynamically based on environment
*
- *
+ *
* Quick Example
*
* {@code
* @Configuration
- * public class AccountSecurityConfig extends AbstractSecurityConfiguration {
- *
+ * public class ResourceSecurityConfig extends AbstractSecurityConfiguration {
+ *
* @Override
* protected void configureEndpointSecurity() {
* // Simple role-based access
- * protect("/api/v1/contracts/{contractId}/accounts")
+ * protect("/api/v1/resources")
* .onMethod("GET")
- * .requireRoles("ACCOUNT_VIEWER")
+ * .requireRoles("RESOURCE_VIEWER")
* .register();
- *
+ *
* // Role + Permission
- * protect("/api/v1/contracts/{contractId}/accounts")
+ * protect("/api/v1/resources")
* .onMethod("POST")
- * .requireRoles("ACCOUNT_CREATOR")
- * .requirePermissions("CREATE_ACCOUNT")
+ * .requireRoles("RESOURCE_CREATOR")
+ * .requirePermissions("CREATE_RESOURCE")
* .register();
- *
+ *
* // Multiple roles (user needs ALL of them)
- * protect("/api/v1/contracts/{contractId}/accounts/{accountId}")
+ * protect("/api/v1/resources/{resourceId}")
* .onMethod("DELETE")
- * .requireAllRoles("ACCOUNT_ADMIN", "DELETE_AUTHORIZED")
+ * .requireAllRoles("RESOURCE_ADMIN", "DELETE_AUTHORIZED")
* .register();
- *
+ *
* // Public endpoint
- * protect("/api/v1/public/rates")
+ * protect("/api/v1/public/info")
* .onMethod("GET")
* .allowAnonymous()
* .register();
@@ -75,38 +75,38 @@
* }
* }
*
- *
+ *
* Complete Example with Feature Flags
*
* {@code
* @Configuration
- * public class TransactionSecurityConfig extends AbstractSecurityConfiguration {
- *
+ * public class OperationSecurityConfig extends AbstractSecurityConfiguration {
+ *
* @Value("${security.strict-mode:false}")
* private boolean strictMode;
- *
+ *
* @Override
* protected void configureEndpointSecurity() {
* if (strictMode) {
* // Production: Strict security
- * protect("/api/v1/contracts/{contractId}/products/{productId}/transactions")
+ * protect("/api/v1/operations")
* .onMethod("POST")
- * .requireRoles("ACCOUNT_HOLDER")
- * .requireAllPermissions("TRANSFER_FUNDS", "HIGH_VALUE_TRANSFER")
+ * .requireRoles("OPERATOR")
+ * .requireAllPermissions("EXECUTE_OPERATION", "HIGH_VALUE_OPERATION")
* .register();
* } else {
* // Development: Relaxed security
- * protect("/api/v1/contracts/{contractId}/products/{productId}/transactions")
+ * protect("/api/v1/operations")
* .onMethod("POST")
- * .requireRoles("ACCOUNT_HOLDER")
- * .requirePermissions("TRANSFER_FUNDS")
+ * .requireRoles("OPERATOR")
+ * .requirePermissions("EXECUTE_OPERATION")
* .register();
* }
* }
* }
* }
*
- *
+ *
* How It Works
*
* - Extend this class in your {@code @Configuration} class
@@ -115,11 +115,11 @@
* - Chain methods to configure roles, permissions, authentication requirements
* - Call {@link EndpointProtectionBuilder#register()} to register the rule
*
- *
+ *
* Priority
* Remember: Configuration defined here (via {@link EndpointSecurityRegistry})
* ALWAYS overrides {@code @Secure} annotations on controller methods.
- *
+ *
* @author Firefly Development Team
* @since 1.0.0
* @see EndpointSecurityRegistry
@@ -127,35 +127,35 @@
*/
@Slf4j
public abstract class AbstractSecurityConfiguration {
-
+
@Autowired
private EndpointSecurityRegistry securityRegistry;
-
+
/**
* Override this method to define your endpoint security rules.
- *
+ *
* This method is automatically called after the Spring context is initialized
* ({@code @PostConstruct}). Use the {@link #protect(String)} method to start
* defining security rules for your endpoints.
- *
+ *
* Example:
*
* {@code
* @Override
* protected void configureEndpointSecurity() {
- * protect("/api/v1/accounts")
+ * protect("/api/v1/resources")
* .onMethod("POST")
- * .requireRoles("ACCOUNT_CREATOR")
+ * .requireRoles("RESOURCE_CREATOR")
* .register();
* }
* }
*
*/
protected abstract void configureEndpointSecurity();
-
+
/**
* Initializes security configuration.
- *
+ *
* This method is called automatically by Spring after the bean is constructed.
* It calls {@link #configureEndpointSecurity()} to allow subclasses to define
* their security rules.
@@ -164,40 +164,40 @@ public abstract class AbstractSecurityConfiguration {
private void initialize() {
log.info("Initializing endpoint security configuration: {}", getClass().getSimpleName());
configureEndpointSecurity();
- log.info("Endpoint security configuration completed: {} endpoints registered",
+ log.info("Endpoint security configuration completed: {} endpoints registered",
securityRegistry.getAllEndpoints().size());
}
-
+
/**
* Starts building a security rule for the given endpoint path.
- *
+ *
* This is the entry point for defining endpoint security. Chain additional
* methods to configure the security requirements.
- *
+ *
* Example:
*
* {@code
- * protect("/api/v1/contracts/{contractId}/accounts")
+ * protect("/api/v1/resources")
* .onMethod("POST")
- * .requireRoles("ACCOUNT_CREATOR")
+ * .requireRoles("RESOURCE_CREATOR")
* .register();
* }
*
- *
- * @param endpointPath the endpoint path (with path variables like {@code {contractId}})
+ *
+ * @param endpointPath the endpoint path (with path variables like {@code {resourceId}})
* @return a builder to continue configuring the security rule
*/
protected final EndpointProtectionBuilder protect(String endpointPath) {
return new EndpointProtectionBuilder(endpointPath, securityRegistry);
}
-
+
/**
* Fluent Builder for Endpoint Security Configuration
- *
+ *
* This builder provides a clean, fluent API for configuring endpoint security.
* Chain methods to define the security requirements, then call {@link #register()}
* to register the configuration.
- *
+ *
* Available Methods
*
* - {@link #onMethod(String)} - Set the HTTP method (GET, POST, PUT, DELETE, etc.)
@@ -213,7 +213,7 @@ protected final EndpointProtectionBuilder protect(String endpointPath) {
protected static final class EndpointProtectionBuilder {
private final String endpointPath;
private final EndpointSecurityRegistry registry;
-
+
private String httpMethod = "GET";
private Set roles = Set.of();
private Set permissions = Set.of();
@@ -221,15 +221,15 @@ protected static final class EndpointProtectionBuilder {
private boolean requireAllPermissions = false;
private boolean allowAnonymous = false;
private boolean requiresAuthentication = true;
-
+
private EndpointProtectionBuilder(String endpointPath, EndpointSecurityRegistry registry) {
this.endpointPath = endpointPath;
this.registry = registry;
}
-
+
/**
* Sets the HTTP method for this security rule.
- *
+ *
* @param method the HTTP method (e.g., "GET", "POST", "PUT", "DELETE")
* @return this builder for method chaining
*/
@@ -237,12 +237,12 @@ public EndpointProtectionBuilder onMethod(String method) {
this.httpMethod = method.toUpperCase();
return this;
}
-
+
/**
* Requires the user to have ANY of the specified roles.
- *
+ *
* The user needs at least one of these roles to access the endpoint.
- *
+ *
* @param roles the required roles
* @return this builder for method chaining
*/
@@ -251,12 +251,12 @@ public EndpointProtectionBuilder requireRoles(String... roles) {
this.requireAllRoles = false;
return this;
}
-
+
/**
* Requires the user to have ALL of the specified roles.
- *
+ *
* The user must have every single one of these roles to access the endpoint.
- *
+ *
* @param roles the required roles
* @return this builder for method chaining
*/
@@ -265,12 +265,12 @@ public EndpointProtectionBuilder requireAllRoles(String... roles) {
this.requireAllRoles = true;
return this;
}
-
+
/**
* Requires the user to have ANY of the specified permissions.
- *
+ *
* The user needs at least one of these permissions to access the endpoint.
- *
+ *
* @param permissions the required permissions
* @return this builder for method chaining
*/
@@ -279,12 +279,12 @@ public EndpointProtectionBuilder requirePermissions(String... permissions) {
this.requireAllPermissions = false;
return this;
}
-
+
/**
* Requires the user to have ALL of the specified permissions.
- *
+ *
* The user must have every single one of these permissions to access the endpoint.
- *
+ *
* @param permissions the required permissions
* @return this builder for method chaining
*/
@@ -293,12 +293,12 @@ public EndpointProtectionBuilder requireAllPermissions(String... permissions) {
this.requireAllPermissions = true;
return this;
}
-
+
/**
* Explicitly requires authentication for this endpoint.
- *
+ *
* This is the default behavior, so you don't usually need to call this method.
- *
+ *
* @return this builder for method chaining
*/
public EndpointProtectionBuilder requireAuthentication() {
@@ -306,12 +306,12 @@ public EndpointProtectionBuilder requireAuthentication() {
this.allowAnonymous = false;
return this;
}
-
+
/**
* Allows unauthenticated (anonymous) access to this endpoint.
- *
+ *
* Use this for public endpoints that don't require authentication.
- *
+ *
* @return this builder for method chaining
*/
public EndpointProtectionBuilder allowAnonymous() {
@@ -319,10 +319,10 @@ public EndpointProtectionBuilder allowAnonymous() {
this.requiresAuthentication = false;
return this;
}
-
+
/**
* Registers this security configuration with the {@link EndpointSecurityRegistry}.
- *
+ *
* Call this method after you've finished configuring the security rule.
* The configuration will be registered and will override any {@code @Secure}
* annotation on the controller method.
@@ -336,10 +336,10 @@ public void register() {
.allowAnonymous(allowAnonymous)
.requiresAuthentication(requiresAuthentication)
.build();
-
+
registry.registerEndpoint(endpointPath, httpMethod, security);
-
- log.debug("Registered security for {} {} - Roles: {}, Permissions: {}, Auth required: {}",
+
+ log.debug("Registered security for {} {} - Roles: {}, Permissions: {}, Auth required: {}",
httpMethod, endpointPath, roles, permissions, requiresAuthentication);
}
}
diff --git a/src/main/java/org/fireflyframework/common/application/security/DefaultSecurityAuthorizationService.java b/src/main/java/org/fireflyframework/common/application/security/DefaultSecurityAuthorizationService.java
index 06adab3..b292bbd 100644
--- a/src/main/java/org/fireflyframework/common/application/security/DefaultSecurityAuthorizationService.java
+++ b/src/main/java/org/fireflyframework/common/application/security/DefaultSecurityAuthorizationService.java
@@ -16,147 +16,50 @@
package org.fireflyframework.common.application.security;
-import org.fireflyframework.common.application.context.AppContext;
-import org.fireflyframework.common.application.context.AppSecurityContext;
-import org.fireflyframework.common.application.util.SessionContextMapper;
-import org.fireflyframework.common.application.spi.SessionContext;
-import org.fireflyframework.common.application.spi.SessionManager;
-import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Autowired;
-import reactor.core.publisher.Mono;
/**
- * Default implementation of SecurityAuthorizationService.
- *
+ * Default implementation of {@link SecurityAuthorizationService}.
+ *
* This is provided by the library - microservices don't need to implement anything.
- *
+ *
* This service automatically:
*
* - Performs role-based authorization
* - Performs permission-based authorization
* - Supports requireAll vs requireAny semantics
- * - Optionally integrates with SecurityCenter for complex policies
*
- *
+ *
* What Microservices Need to Do
* Nothing. Authorization works automatically based on:
*
* @Secure annotations on controllers/methods
* - Programmatic security rules in
EndpointSecurityRegistry
*
- *
+ *
* Authorization Logic
- * By default, this service checks if:
+ * Authorization is evaluated entirely against the roles and permissions already resolved
+ * into the {@code AppContext}:
*
- * - User's roles (from context) match required roles
- * - User's permissions (from context) match required permissions
- * - If SecurityCenter is enabled, delegates complex policy evaluation
+ * - The subject's roles (from context) are matched against the required roles
+ * - The subject's permissions (from context) are matched against the required permissions
*
- *
- * SecurityCenter Integration
- * When SecurityCenter SDK is available, complex authorization policies
- * will be evaluated by SecurityCenter for:
- *
- * - Attribute-Based Access Control (ABAC)
- * - Policy-based decisions
- * - Audit trail
- *
- *
+ *
+ * All decisioning is product-agnostic and relies exclusively on the validated identity
+ * resolved by the context resolution layer; there is no dependency on any external session
+ * store or domain-specific resource scoping.
+ *
* @author Firefly Development Team
* @since 1.0.0
*/
@Slf4j
-@RequiredArgsConstructor
public class DefaultSecurityAuthorizationService extends AbstractSecurityAuthorizationService {
-
- @Autowired(required = false)
- private final SessionManager sessionManager;
-
- // The parent AbstractSecurityAuthorizationService already provides:
- // - Role checking (hasRole, hasAnyRole, hasAllRoles)
- // - Permission checking (hasPermission, hasAnyPermission, hasAllPermissions)
+
+ // The parent AbstractSecurityAuthorizationService already provides everything needed:
+ // - Role checking (checkRoles)
+ // - Permission checking (checkPermissions)
// - Authorization with requireAll/requireAny semantics
-
- /**
- * Enhanced authorization using SessionManager for product access validation.
- *
- * This method integrates with the Security Center's SessionManager to:
- *
- * - Validate party has access to specific products/contracts
- * - Check granular permissions (action + resource)
- * - Provide graceful degradation if SecurityCenter is unavailable
- *
- */
- @Override
- protected Mono authorizeWithSecurityCenter(
- AppContext context,
- AppSecurityContext securityContext) {
-
- if (sessionManager == null) {
- log.warn("SessionManager not available - falling back to basic role/permission checks. " +
- "Deploy common-platform-security-center for enhanced authorization.");
- return super.authorizeWithSecurityCenter(context, securityContext);
- }
-
- log.debug("Using SessionManager for authorization: party={}, contract={}, product={}",
- context.getPartyId(), context.getContractId(), context.getProductId());
-
- // If product access needs to be validated
- if (context.getProductId() != null) {
- return sessionManager.hasAccessToProduct(context.getPartyId(), context.getProductId())
- .flatMap(hasAccess -> {
- if (!hasAccess) {
- log.warn("Party {} does not have access to product {}",
- context.getPartyId(), context.getProductId());
- return Mono.just(createUnauthorizedContext(securityContext,
- "No access to requested product"));
- }
-
- // Product access OK - now check role/permission requirements
- return performRolePermissionChecks(context, securityContext);
- })
- .doOnError(error -> log.error("Error checking product access via SessionManager: {}",
- error.getMessage(), error))
- .onErrorResume(error -> {
- // Graceful degradation on error
- log.warn("Falling back to basic checks due to error: {}", error.getMessage());
- return performRolePermissionChecks(context, securityContext);
- });
- }
-
- // No product context - just perform role/permission checks
- return performRolePermissionChecks(context, securityContext);
- }
-
- /**
- * Performs standard role and permission checks using the AppContext.
- */
- private Mono performRolePermissionChecks(
- AppContext context, AppSecurityContext securityContext) {
-
- // Check required roles
- if (securityContext.hasRequiredRoles()) {
- return checkRoles(context, securityContext)
- .flatMap(rolesOk -> {
- if (!rolesOk) {
- return Mono.just(createUnauthorizedContext(securityContext,
- "Required roles not present"));
- }
- // Roles OK - check permissions if needed
- if (securityContext.hasRequiredPermissions()) {
- return checkPermissions(context, securityContext);
- }
- return Mono.just(createAuthorizedContext(securityContext));
- });
- }
-
- // Check permissions
- if (securityContext.hasRequiredPermissions()) {
- return checkPermissions(context, securityContext);
- }
-
- // No specific requirements - grant access
- return Mono.just(createAuthorizedContext(securityContext));
- }
+ //
+ // Authorization is performed solely from the roles and permissions already resolved
+ // into the AppContext, so no additional behaviour is required here.
}
diff --git a/src/main/java/org/fireflyframework/common/application/security/SecurityAuthorizationService.java b/src/main/java/org/fireflyframework/common/application/security/SecurityAuthorizationService.java
index c63f71f..420ebd3 100644
--- a/src/main/java/org/fireflyframework/common/application/security/SecurityAuthorizationService.java
+++ b/src/main/java/org/fireflyframework/common/application/security/SecurityAuthorizationService.java
@@ -25,52 +25,54 @@
/**
* Service for authorization decisions.
- * Integrates with Firefly SecurityCenter to determine access rights.
- *
+ *
+ * Authorization is evaluated against the roles and permissions already resolved into the
+ * {@link AppContext} by the context resolution layer. The service is fully product-agnostic.
+ *
* @author Firefly Development Team
* @since 1.0.0
*/
public interface SecurityAuthorizationService {
-
+
/**
* Authorizes an operation based on the application context and security requirements.
- *
+ *
* @param context the application context
* @param securityContext the security context with requirements
* @return Mono of updated AppSecurityContext with authorization result
*/
Mono authorize(AppContext context, AppSecurityContext securityContext);
-
+
/**
- * Checks if a party has a specific role in a contract/product context.
- *
+ * Checks if the current subject has a specific role.
+ *
* @param context the application context
* @param role the role to check
* @return Mono of boolean indicating if role is present
*/
Mono hasRole(AppContext context, String role);
-
+
/**
- * Checks if a party has a specific permission in a contract/product context.
- *
+ * Checks if the current subject has a specific permission.
+ *
* @param context the application context
* @param permission the permission to check
* @return Mono of boolean indicating if permission is granted
*/
Mono hasPermission(AppContext context, String permission);
-
+
/**
* Evaluates a custom security expression.
- *
+ *
* @param context the application context
* @param expression the SpEL expression to evaluate
* @return Mono of boolean result
*/
Mono evaluateExpression(AppContext context, String expression);
-
+
/**
- * Checks if a party has all specified permissions.
- *
+ * Checks if the current subject has all specified permissions.
+ *
* @param context the application context
* @param permissions the permissions to check
* @return Mono of boolean indicating if all permissions are granted
@@ -83,10 +85,10 @@ default Mono hasAllPermissions(AppContext context, Set permissi
.flatMap(p -> hasPermission(context, p))
.all(Boolean::booleanValue);
}
-
+
/**
- * Checks if a party has any of the specified roles.
- *
+ * Checks if the current subject has any of the specified roles.
+ *
* @param context the application context
* @param roles the roles to check
* @return Mono of boolean indicating if any role is present
diff --git a/src/main/java/org/fireflyframework/common/application/security/annotation/RequireContext.java b/src/main/java/org/fireflyframework/common/application/security/annotation/RequireContext.java
deleted file mode 100644
index 210413a..0000000
--- a/src/main/java/org/fireflyframework/common/application/security/annotation/RequireContext.java
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Copyright 2024-2026 Firefly Software Foundation
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.fireflyframework.common.application.security.annotation;
-
-import java.lang.annotation.*;
-
-/**
- * Annotation to indicate that a method requires specific context components.
- * Used to ensure that required context information (contract, product, etc.) is present.
- *
- * Usage example:
- *
- * {@literal @}PostMapping("/transfer")
- * {@literal @}RequireContext(contract = true, product = true)
- * public Mono<Transfer> transfer(@RequestBody TransferRequest request) {
- * // This method requires both contractId and productId in the context
- * }
- *
- *
- * @author Firefly Development Team
- * @since 1.0.0
- */
-@Target({ElementType.METHOD, ElementType.TYPE})
-@Retention(RetentionPolicy.RUNTIME)
-@Documented
-public @interface RequireContext {
-
- /**
- * Whether a contract ID is required in the context.
- *
- * @return true if contractId must be present
- */
- boolean contract() default false;
-
- /**
- * Whether a product ID is required in the context.
- *
- * @return true if productId must be present
- */
- boolean product() default false;
-
- /**
- * Whether tenant configuration must be loaded.
- *
- * @return true if tenant config must be present
- */
- boolean tenantConfig() default true;
-
- /**
- * Specific providers that must be configured for the tenant.
- *
- * @return array of required provider types
- */
- String[] requiredProviders() default {};
-
- /**
- * Whether to fail fast if context requirements are not met.
- * If true, throw an exception immediately.
- * If false, log a warning and continue.
- *
- * @return true to fail fast
- */
- boolean failFast() default true;
-}
diff --git a/src/main/java/org/fireflyframework/common/application/security/annotation/Secure.java b/src/main/java/org/fireflyframework/common/application/security/annotation/Secure.java
index cb09ed5..a9dd15a 100644
--- a/src/main/java/org/fireflyframework/common/application/security/annotation/Secure.java
+++ b/src/main/java/org/fireflyframework/common/application/security/annotation/Secure.java
@@ -21,36 +21,36 @@
/**
* Annotation for declarative endpoint security configuration.
* Can be applied to controller classes or individual methods.
- *
+ *
* When applied at class level, security rules apply to all methods in the class.
* Method-level annotations override class-level security configuration.
- *
+ *
* Usage example:
*
* {@literal @}RestController
- * {@literal @}RequestMapping("/api/v1/accounts")
- * {@literal @}Secure(roles = {"ACCOUNT_OWNER", "ACCOUNT_ADMIN"})
- * public class AccountController {
- *
+ * {@literal @}RequestMapping("/api/v1/resources")
+ * {@literal @}Secure(roles = {"RESOURCE_OWNER", "RESOURCE_ADMIN"})
+ * public class ResourceController {
+ *
* {@literal @}GetMapping("/{id}")
- * public Mono<Account> getAccount(@PathVariable UUID id) {
- * // Only accessible by users with ACCOUNT_OWNER or ACCOUNT_ADMIN roles
+ * public Mono<Resource> getResource(@PathVariable UUID id) {
+ * // Only accessible by users with RESOURCE_OWNER or RESOURCE_ADMIN roles
* }
- *
- * {@literal @}PostMapping("/{id}/transfer")
- * {@literal @}Secure(roles = "ACCOUNT_OWNER", permissions = "TRANSFER_FUNDS")
- * public Mono<Transfer> transfer(@PathVariable UUID id, @RequestBody TransferRequest request) {
- * // Requires both ACCOUNT_OWNER role AND TRANSFER_FUNDS permission
+ *
+ * {@literal @}PostMapping("/{id}/action")
+ * {@literal @}Secure(roles = "RESOURCE_OWNER", permissions = "EXECUTE_ACTION")
+ * public Mono<ActionResult> action(@PathVariable UUID id, @RequestBody ActionRequest request) {
+ * // Requires both RESOURCE_OWNER role AND EXECUTE_ACTION permission
* }
- *
+ *
* {@literal @}GetMapping("/public")
* {@literal @}Secure(allowAnonymous = true)
- * public Mono<List<Account>> listPublicAccounts() {
+ * public Mono<List<Resource>> listPublicResources() {
* // Accessible without authentication
* }
* }
*
- *
+ *
* @author Firefly Development Team
* @since 1.0.0
*/
@@ -58,57 +58,57 @@
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Secure {
-
+
/**
* Roles required to access this endpoint.
* If multiple roles are specified, the user must have at least one of them (OR logic).
- *
+ *
* @return array of required role names
*/
String[] roles() default {};
-
+
/**
* Permissions required to access this endpoint.
* If multiple permissions are specified, the user must have at least one of them (OR logic).
- *
+ *
* @return array of required permission names
*/
String[] permissions() default {};
-
+
/**
* Whether all specified roles are required (AND logic) instead of any (OR logic).
- *
+ *
* @return true to require all roles, false to require any role
*/
boolean requireAllRoles() default false;
-
+
/**
* Whether all specified permissions are required (AND logic) instead of any (OR logic).
- *
+ *
* @return true to require all permissions, false to require any permission
*/
boolean requireAllPermissions() default false;
-
+
/**
* Whether anonymous access is allowed.
* If true, no authentication is required.
- *
+ *
* @return true to allow anonymous access
*/
boolean allowAnonymous() default false;
-
+
/**
* Whether authentication is required.
* If false, the endpoint can be accessed without authentication.
- *
+ *
* @return true if authentication is required
*/
boolean requiresAuthentication() default true;
-
+
/**
* Custom security expression using SpEL.
* Allows for complex authorization logic beyond simple role/permission checks.
- *
+ *
* Available variables in expressions:
*
* - context: The current AppContext
@@ -116,34 +116,26 @@
* - metadata: The current AppMetadata
* - principal: The authenticated principal
*
- *
- * Example: {@code expression = "context.hasRole('ADMIN') or context.partyId == #accountId"}
- *
+ *
+ * Example: {@code expression = "context.hasRole('ADMIN') or context.subject == #ownerId"}
+ *
* @return SpEL expression for custom authorization logic
*/
String expression() default "";
-
- /**
- * Whether to delegate authorization to the Firefly SecurityCenter.
- * If true, the SecurityCenter will be consulted for authorization decisions.
- *
- * @return true to use SecurityCenter for authorization
- */
- boolean useSecurityCenter() default true;
-
+
/**
* Additional security attributes as key=value pairs.
* Can be used for custom security extensions.
- *
+ *
* Example: {@code attributes = {"rateLimit=100", "ipWhitelist=true"}}
- *
+ *
* @return array of key=value security attribute pairs
*/
String[] attributes() default {};
-
+
/**
* Description of the security requirement for documentation purposes.
- *
+ *
* @return human-readable description of security requirements
*/
String description() default "";
diff --git a/src/main/java/org/fireflyframework/common/application/service/AbstractApplicationService.java b/src/main/java/org/fireflyframework/common/application/service/AbstractApplicationService.java
index f3c5bed..24997c7 100644
--- a/src/main/java/org/fireflyframework/common/application/service/AbstractApplicationService.java
+++ b/src/main/java/org/fireflyframework/common/application/service/AbstractApplicationService.java
@@ -17,8 +17,6 @@
package org.fireflyframework.common.application.service;
import org.fireflyframework.common.application.context.AppConfig;
-import org.fireflyframework.common.application.context.AppContext;
-import org.fireflyframework.common.application.context.AppMetadata;
import org.fireflyframework.common.application.context.ApplicationExecutionContext;
import org.fireflyframework.common.application.resolver.ConfigResolver;
import org.fireflyframework.common.application.resolver.ContextResolver;
@@ -30,19 +28,19 @@
/**
* Abstract base class for application layer services.
* Provides common functionality for context resolution, security, and business process orchestration.
- *
+ *
* Application layer services are responsible for:
*
* - Orchestrating business processes across multiple domain services
- * - Managing application context (party, contract, product)
+ * - Managing the application context (subject, tenant, roles, permissions)
* - Enforcing security and authorization policies
* - Coordinating with external platform services
*
- *
+ *
* Typical usage:
*
* public class AccountApplicationService extends AbstractApplicationService {
- *
+ *
* public Mono<Transfer> transferFunds(ServerWebExchange exchange, TransferRequest request) {
* return resolveExecutionContext(exchange)
* .flatMap(context -> {
@@ -52,20 +50,20 @@
* }
* }
*
- *
+ *
* @author Firefly Development Team
* @since 1.0.0
*/
@Slf4j
public abstract class AbstractApplicationService {
-
+
protected final ContextResolver contextResolver;
protected final ConfigResolver configResolver;
protected final SecurityAuthorizationService authorizationService;
-
+
/**
* Constructor with required dependencies.
- *
+ *
* @param contextResolver the context resolver
* @param configResolver the config resolver
* @param authorizationService the authorization service
@@ -77,54 +75,32 @@ protected AbstractApplicationService(ContextResolver contextResolver,
this.configResolver = configResolver;
this.authorizationService = authorizationService;
}
-
+
/**
* Resolves the complete application execution context from the request.
- * This includes metadata, business context, and configuration.
- *
+ * This includes the request context, tenant configuration, and security context.
+ *
* @param exchange the server web exchange
* @return Mono of ApplicationExecutionContext
*/
protected Mono resolveExecutionContext(ServerWebExchange exchange) {
log.debug("Resolving execution context for request");
-
+
return contextResolver.resolveContext(exchange)
- .flatMap(appContext ->
+ .flatMap(appContext ->
configResolver.resolveConfig(appContext.getTenantId())
.map(appConfig -> ApplicationExecutionContext.builder()
.context(appContext)
.config(appConfig)
.build())
)
- .doOnSuccess(ctx -> log.debug("Successfully resolved execution context for party: {}", ctx.getPartyId()))
+ .doOnSuccess(ctx -> log.debug("Successfully resolved execution context for subject: {}", ctx.getSubject()))
.doOnError(error -> log.error("Failed to resolve execution context", error));
}
-
- /**
- * Validates that the execution context has required components.
- *
- * @param context the execution context
- * @param requireContract whether contract ID is required
- * @param requireProduct whether product ID is required
- * @return Mono of validated context
- */
- protected Mono validateContext(ApplicationExecutionContext context,
- boolean requireContract,
- boolean requireProduct) {
- if (requireContract && !context.getContext().hasContract()) {
- return Mono.error(new IllegalStateException("Contract ID is required but not present in context"));
- }
-
- if (requireProduct && !context.getContext().hasProduct()) {
- return Mono.error(new IllegalStateException("Product ID is required but not present in context"));
- }
-
- return Mono.just(context);
- }
-
+
/**
- * Checks if the party has the required role.
- *
+ * Checks if the subject has the required role.
+ *
* @param context the execution context
* @param role the required role
* @return Mono that completes if role is present, errors otherwise
@@ -139,10 +115,10 @@ protected Mono requireRole(ApplicationExecutionContext context, String rol
return Mono.empty();
});
}
-
+
/**
- * Checks if the party has the required permission.
- *
+ * Checks if the subject has the required permission.
+ *
* @param context the execution context
* @param permission the required permission
* @return Mono that completes if permission is granted, errors otherwise
@@ -157,24 +133,24 @@ protected Mono requirePermission(ApplicationExecutionContext context, Stri
return Mono.empty();
});
}
-
+
/**
* Gets a provider configuration for the tenant.
- *
+ *
* @param context the execution context
* @param providerType the provider type
* @return Mono of provider config
*/
- protected Mono getProviderConfig(ApplicationExecutionContext context,
+ protected Mono getProviderConfig(ApplicationExecutionContext context,
String providerType) {
return Mono.justOrEmpty(context.getConfig().getProvider(providerType))
.switchIfEmpty(Mono.error(new IllegalStateException(
"Provider not configured: " + providerType)));
}
-
+
/**
* Checks if a feature is enabled for the tenant.
- *
+ *
* @param context the execution context
* @param feature the feature flag name
* @return Mono of boolean
diff --git a/src/main/java/org/fireflyframework/common/application/spi/SessionContext.java b/src/main/java/org/fireflyframework/common/application/spi/SessionContext.java
deleted file mode 100644
index a5e934e..0000000
--- a/src/main/java/org/fireflyframework/common/application/spi/SessionContext.java
+++ /dev/null
@@ -1,47 +0,0 @@
-package org.fireflyframework.common.application.spi;
-
-import org.fireflyframework.common.application.spi.dto.ContractInfoDTO;
-import lombok.AllArgsConstructor;
-import lombok.Builder;
-import lombok.Data;
-import lombok.NoArgsConstructor;
-
-import java.time.LocalDateTime;
-import java.util.List;
-import java.util.Map;
-import java.util.UUID;
-
-/**
- * Represents a user session context.
- *
- * Carries session metadata, user roles, scopes, contracts, and any domain-specific context.
- * Platform-specific implementations can extend this class to add additional fields.
- *
- *
- * @author Firefly Development Team
- * @since 1.0.0
- */
-@Data
-@Builder
-@NoArgsConstructor
-@AllArgsConstructor
-public class SessionContext {
-
- private String sessionId;
- private UUID partyId;
- private String userId;
- private String tenantId;
- private List roles;
- private List scopes;
- private Map attributes;
- private List activeContracts;
- private LocalDateTime createdAt;
- private SessionStatus status;
-
- /**
- * Session lifecycle status.
- */
- public enum SessionStatus {
- ACTIVE, EXPIRED, INVALIDATED
- }
-}
diff --git a/src/main/java/org/fireflyframework/common/application/spi/SessionManager.java b/src/main/java/org/fireflyframework/common/application/spi/SessionManager.java
deleted file mode 100644
index f66cb7e..0000000
--- a/src/main/java/org/fireflyframework/common/application/spi/SessionManager.java
+++ /dev/null
@@ -1,73 +0,0 @@
-package org.fireflyframework.common.application.spi;
-
-import org.springframework.web.server.ServerWebExchange;
-import reactor.core.publisher.Mono;
-
-import java.util.UUID;
-
-/**
- * SPI interface for session management.
- *
- * Implementations of this interface provide the mechanism for retrieving and managing
- * user session contexts. Platform-specific implementations (e.g., for banking, e-commerce)
- * should implement this interface to integrate with their identity/session infrastructure.
- *
- */
-public interface SessionManager {
-
- /**
- * Retrieves the current session context for the given token.
- *
- * @param token the authentication token
- * @return a Mono emitting the session context
- */
- Mono getSessionContext(String token);
-
- /**
- * Creates or retrieves the session associated with the current web exchange.
- *
- * Extracts the authentication token from the exchange and returns the enriched
- * session context. If no active session exists, a new one may be created.
- *
- *
- * @param exchange the current server web exchange
- * @return a Mono emitting the session context
- */
- Mono createOrGetSession(ServerWebExchange exchange);
-
- /**
- * Validates whether the given token represents a valid session.
- *
- * @param token the authentication token
- * @return a Mono emitting true if the session is valid
- */
- Mono isSessionValid(String token);
-
- /**
- * Checks whether the given party has access to the specified product.
- *
- * @param partyId the party identifier
- * @param productId the product identifier
- * @return a Mono emitting true if the party has access to the product
- */
- Mono hasAccessToProduct(UUID partyId, UUID productId);
-
- /**
- * Checks whether the given party has a specific permission on a product.
- *
- * @param partyId the party identifier
- * @param productId the product identifier
- * @param actionType the action type (e.g., READ, WRITE, DELETE)
- * @param resourceType the resource type (e.g., BALANCE, TRANSACTION)
- * @return a Mono emitting true if the party has the permission
- */
- Mono hasPermission(UUID partyId, UUID productId, String actionType, String resourceType);
-
- /**
- * Invalidates the session associated with the given token.
- *
- * @param token the authentication token
- * @return a Mono completing when the session is invalidated
- */
- Mono invalidateSession(String token);
-}
diff --git a/src/main/java/org/fireflyframework/common/application/spi/dto/ContractInfoDTO.java b/src/main/java/org/fireflyframework/common/application/spi/dto/ContractInfoDTO.java
deleted file mode 100644
index b7cd888..0000000
--- a/src/main/java/org/fireflyframework/common/application/spi/dto/ContractInfoDTO.java
+++ /dev/null
@@ -1,27 +0,0 @@
-package org.fireflyframework.common.application.spi.dto;
-
-import lombok.AllArgsConstructor;
-import lombok.Builder;
-import lombok.Data;
-import lombok.NoArgsConstructor;
-
-import java.util.UUID;
-
-/**
- * DTO representing a contract within a user session.
- *
- * @author Firefly Development Team
- * @since 1.0.0
- */
-@Data
-@Builder
-@NoArgsConstructor
-@AllArgsConstructor
-public class ContractInfoDTO {
-
- private UUID contractId;
- private String contractNumber;
- private RoleInfoDTO roleInContract;
- private ProductInfoDTO product;
- private boolean isActive;
-}
diff --git a/src/main/java/org/fireflyframework/common/application/spi/dto/ProductInfoDTO.java b/src/main/java/org/fireflyframework/common/application/spi/dto/ProductInfoDTO.java
deleted file mode 100644
index 9e6c201..0000000
--- a/src/main/java/org/fireflyframework/common/application/spi/dto/ProductInfoDTO.java
+++ /dev/null
@@ -1,24 +0,0 @@
-package org.fireflyframework.common.application.spi.dto;
-
-import lombok.AllArgsConstructor;
-import lombok.Builder;
-import lombok.Data;
-import lombok.NoArgsConstructor;
-
-import java.util.UUID;
-
-/**
- * DTO representing a product within a contract.
- *
- * @author Firefly Development Team
- * @since 1.0.0
- */
-@Data
-@Builder
-@NoArgsConstructor
-@AllArgsConstructor
-public class ProductInfoDTO {
-
- private UUID productId;
- private String productName;
-}
diff --git a/src/main/java/org/fireflyframework/common/application/spi/dto/RoleInfoDTO.java b/src/main/java/org/fireflyframework/common/application/spi/dto/RoleInfoDTO.java
deleted file mode 100644
index e78f363..0000000
--- a/src/main/java/org/fireflyframework/common/application/spi/dto/RoleInfoDTO.java
+++ /dev/null
@@ -1,28 +0,0 @@
-package org.fireflyframework.common.application.spi.dto;
-
-import lombok.AllArgsConstructor;
-import lombok.Builder;
-import lombok.Data;
-import lombok.NoArgsConstructor;
-
-import java.util.List;
-import java.util.UUID;
-
-/**
- * DTO representing a role within a contract.
- *
- * @author Firefly Development Team
- * @since 1.0.0
- */
-@Data
-@Builder
-@NoArgsConstructor
-@AllArgsConstructor
-public class RoleInfoDTO {
-
- private UUID roleId;
- private String roleCode;
- private String name;
- private boolean isActive;
- private List scopes;
-}
diff --git a/src/main/java/org/fireflyframework/common/application/spi/dto/RoleScopeInfoDTO.java b/src/main/java/org/fireflyframework/common/application/spi/dto/RoleScopeInfoDTO.java
deleted file mode 100644
index 5bad01a..0000000
--- a/src/main/java/org/fireflyframework/common/application/spi/dto/RoleScopeInfoDTO.java
+++ /dev/null
@@ -1,26 +0,0 @@
-package org.fireflyframework.common.application.spi.dto;
-
-import lombok.AllArgsConstructor;
-import lombok.Builder;
-import lombok.Data;
-import lombok.NoArgsConstructor;
-
-import java.util.UUID;
-
-/**
- * DTO representing a role scope (permission) within a role.
- *
- * @author Firefly Development Team
- * @since 1.0.0
- */
-@Data
-@Builder
-@NoArgsConstructor
-@AllArgsConstructor
-public class RoleScopeInfoDTO {
-
- private UUID scopeId;
- private String actionType;
- private String resourceType;
- private boolean isActive;
-}
diff --git a/src/main/java/org/fireflyframework/common/application/util/SessionContextMapper.java b/src/main/java/org/fireflyframework/common/application/util/SessionContextMapper.java
deleted file mode 100644
index f49ae3d..0000000
--- a/src/main/java/org/fireflyframework/common/application/util/SessionContextMapper.java
+++ /dev/null
@@ -1,108 +0,0 @@
-package org.fireflyframework.common.application.util;
-
-import org.fireflyframework.common.application.spi.SessionContext;
-import lombok.extern.slf4j.Slf4j;
-
-import java.util.*;
-import java.util.stream.Collectors;
-
-/**
- * Utility class for mapping SessionContext to roles and permissions.
- *
- * This mapper extracts roles and permissions from the generic {@link SessionContext} SPI.
- * Platform-specific implementations should provide a {@link SessionContext} that
- * populates roles, scopes, and attributes according to their domain model.
- *
- *
- * @author Firefly Framework Team
- * @since 1.0.0
- */
-@Slf4j
-public final class SessionContextMapper {
-
- private SessionContextMapper() {
- // Utility class
- }
-
- /**
- * Extracts roles from the session context.
- *
- * @param sessionContext the session context
- * @param scopeKey optional scope key for filtering (e.g., contractId)
- * @param subScopeKey optional sub-scope key for filtering (e.g., productId)
- * @return set of role strings
- */
- public static Set extractRoles(SessionContext sessionContext, UUID scopeKey, UUID subScopeKey) {
- if (sessionContext == null) {
- log.debug("Session context is null, returning empty roles");
- return Collections.emptySet();
- }
-
- List roles = sessionContext.getRoles();
- if (roles == null || roles.isEmpty()) {
- return Collections.emptySet();
- }
-
- Set result = new HashSet<>(roles);
- log.debug("Extracted {} roles from session context", result.size());
- return result;
- }
-
- /**
- * Extracts permissions/scopes from the session context.
- *
- * @param sessionContext the session context
- * @param scopeKey optional scope key for filtering
- * @param subScopeKey optional sub-scope key for filtering
- * @return set of permission strings
- */
- public static Set extractPermissions(SessionContext sessionContext, UUID scopeKey, UUID subScopeKey) {
- if (sessionContext == null) {
- log.debug("Session context is null, returning empty permissions");
- return Collections.emptySet();
- }
-
- List scopes = sessionContext.getScopes();
- if (scopes == null || scopes.isEmpty()) {
- return Collections.emptySet();
- }
-
- Set result = new HashSet<>(scopes);
- log.debug("Extracted {} permissions from session context", result.size());
- return result;
- }
-
- /**
- * Checks if the session has a specific attribute.
- *
- * @param sessionContext the session context
- * @param attributeKey the attribute key to check
- * @return true if the attribute exists
- */
- public static boolean hasAttribute(SessionContext sessionContext, String attributeKey) {
- if (sessionContext == null || sessionContext.getAttributes() == null) {
- return false;
- }
- return sessionContext.getAttributes().containsKey(attributeKey);
- }
-
- /**
- * Retrieves an attribute value from the session context.
- *
- * @param sessionContext the session context
- * @param attributeKey the attribute key
- * @param type the expected type
- * @return the attribute value, or null if not found
- */
- @SuppressWarnings("unchecked")
- public static T getAttribute(SessionContext sessionContext, String attributeKey, Class type) {
- if (sessionContext == null || sessionContext.getAttributes() == null) {
- return null;
- }
- Object value = sessionContext.getAttributes().get(attributeKey);
- if (value != null && type.isInstance(value)) {
- return (T) value;
- }
- return null;
- }
-}
diff --git a/src/test/java/org/fireflyframework/common/application/config/ApplicationLayerPropertiesTest.java b/src/test/java/org/fireflyframework/common/application/config/ApplicationLayerPropertiesTest.java
index e7592fc..bf5435d 100644
--- a/src/test/java/org/fireflyframework/common/application/config/ApplicationLayerPropertiesTest.java
+++ b/src/test/java/org/fireflyframework/common/application/config/ApplicationLayerPropertiesTest.java
@@ -7,134 +7,134 @@
@DisplayName("ApplicationLayerProperties Tests")
class ApplicationLayerPropertiesTest {
-
+
@Test
@DisplayName("Should create properties with defaults")
void shouldCreatePropertiesWithDefaults() {
ApplicationLayerProperties properties = new ApplicationLayerProperties();
-
+
assertNotNull(properties.getSecurity());
assertNotNull(properties.getContext());
assertNotNull(properties.getConfig());
}
-
+
@Test
@DisplayName("Should have default security settings")
void shouldHaveDefaultSecuritySettings() {
ApplicationLayerProperties properties = new ApplicationLayerProperties();
ApplicationLayerProperties.Security security = properties.getSecurity();
-
+
assertTrue(security.isEnabled());
- assertTrue(security.isUseSecurityCenter());
+ assertTrue(security.isUsePolicyEngine());
assertNotNull(security.getDefaultRoles());
assertEquals(0, security.getDefaultRoles().length);
assertFalse(security.isFailOnMissing());
}
-
+
@Test
@DisplayName("Should have default context settings")
void shouldHaveDefaultContextSettings() {
ApplicationLayerProperties properties = new ApplicationLayerProperties();
ApplicationLayerProperties.Context context = properties.getContext();
-
+
assertTrue(context.isCacheEnabled());
assertEquals(300, context.getCacheTtl());
assertEquals(1000, context.getCacheMaxSize());
}
-
+
@Test
@DisplayName("Should have default config settings")
void shouldHaveDefaultConfigSettings() {
ApplicationLayerProperties properties = new ApplicationLayerProperties();
ApplicationLayerProperties.Config config = properties.getConfig();
-
+
assertTrue(config.isCacheEnabled());
assertEquals(600, config.getCacheTtl());
assertFalse(config.isRefreshOnStartup());
}
-
+
@Test
@DisplayName("Should allow custom security settings")
void shouldAllowCustomSecuritySettings() {
ApplicationLayerProperties properties = new ApplicationLayerProperties();
ApplicationLayerProperties.Security security = properties.getSecurity();
-
+
security.setEnabled(false);
- security.setUseSecurityCenter(false);
+ security.setUsePolicyEngine(false);
security.setDefaultRoles(new String[]{"USER", "GUEST"});
security.setFailOnMissing(true);
-
+
assertFalse(security.isEnabled());
- assertFalse(security.isUseSecurityCenter());
+ assertFalse(security.isUsePolicyEngine());
assertArrayEquals(new String[]{"USER", "GUEST"}, security.getDefaultRoles());
assertTrue(security.isFailOnMissing());
}
-
+
@Test
@DisplayName("Should allow custom context settings")
void shouldAllowCustomContextSettings() {
ApplicationLayerProperties properties = new ApplicationLayerProperties();
ApplicationLayerProperties.Context context = properties.getContext();
-
+
context.setCacheEnabled(false);
context.setCacheTtl(60);
context.setCacheMaxSize(500);
-
+
assertFalse(context.isCacheEnabled());
assertEquals(60, context.getCacheTtl());
assertEquals(500, context.getCacheMaxSize());
}
-
+
@Test
@DisplayName("Should allow custom config settings")
void shouldAllowCustomConfigSettings() {
ApplicationLayerProperties properties = new ApplicationLayerProperties();
ApplicationLayerProperties.Config config = properties.getConfig();
-
+
config.setCacheEnabled(false);
config.setCacheTtl(120);
config.setRefreshOnStartup(true);
-
+
assertFalse(config.isCacheEnabled());
assertEquals(120, config.getCacheTtl());
assertTrue(config.isRefreshOnStartup());
}
-
+
@Test
@DisplayName("Should allow replacing entire security object")
void shouldAllowReplacingEntireSecurityObject() {
ApplicationLayerProperties properties = new ApplicationLayerProperties();
ApplicationLayerProperties.Security newSecurity = new ApplicationLayerProperties.Security();
-
+
newSecurity.setEnabled(false);
properties.setSecurity(newSecurity);
-
+
assertSame(newSecurity, properties.getSecurity());
assertFalse(properties.getSecurity().isEnabled());
}
-
+
@Test
@DisplayName("Should allow replacing entire context object")
void shouldAllowReplacingEntireContextObject() {
ApplicationLayerProperties properties = new ApplicationLayerProperties();
ApplicationLayerProperties.Context newContext = new ApplicationLayerProperties.Context();
-
+
newContext.setCacheEnabled(false);
properties.setContext(newContext);
-
+
assertSame(newContext, properties.getContext());
assertFalse(properties.getContext().isCacheEnabled());
}
-
+
@Test
@DisplayName("Should allow replacing entire config object")
void shouldAllowReplacingEntireConfigObject() {
ApplicationLayerProperties properties = new ApplicationLayerProperties();
ApplicationLayerProperties.Config newConfig = new ApplicationLayerProperties.Config();
-
+
newConfig.setRefreshOnStartup(true);
properties.setConfig(newConfig);
-
+
assertSame(newConfig, properties.getConfig());
assertTrue(properties.getConfig().isRefreshOnStartup());
}
diff --git a/src/test/java/org/fireflyframework/common/application/context/AppContextTest.java b/src/test/java/org/fireflyframework/common/application/context/AppContextTest.java
index 9b30bf8..8021cf7 100644
--- a/src/test/java/org/fireflyframework/common/application/context/AppContextTest.java
+++ b/src/test/java/org/fireflyframework/common/application/context/AppContextTest.java
@@ -12,130 +12,120 @@
@DisplayName("AppContext Tests")
class AppContextTest {
-
+
@Test
@DisplayName("Should create AppContext with builder")
void shouldCreateAppContextWithBuilder() {
- UUID partyId = UUID.randomUUID();
- UUID contractId = UUID.randomUUID();
- UUID productId = UUID.randomUUID();
+ String subject = "user-123";
UUID tenantId = UUID.randomUUID();
Set roles = Set.of("ACCOUNT_OWNER", "ADMIN");
Set permissions = Set.of("READ", "WRITE", "DELETE");
-
+
AppContext context = AppContext.builder()
- .partyId(partyId)
- .contractId(contractId)
- .productId(productId)
+ .subject(subject)
.tenantId(tenantId)
.roles(roles)
.permissions(permissions)
.build();
-
- assertEquals(partyId, context.getPartyId());
- assertEquals(contractId, context.getContractId());
- assertEquals(productId, context.getProductId());
+
+ assertEquals(subject, context.getSubject());
assertEquals(tenantId, context.getTenantId());
assertEquals(roles, context.getRoles());
assertEquals(permissions, context.getPermissions());
}
-
+
@Test
@DisplayName("Should check if context has specific role")
void shouldCheckHasRole() {
AppContext context = AppContext.builder()
- .partyId(UUID.randomUUID())
+ .subject("user-123")
.roles(Set.of("ACCOUNT_OWNER", "VIEWER"))
.build();
-
+
assertTrue(context.hasRole("ACCOUNT_OWNER"));
assertTrue(context.hasRole("VIEWER"));
assertFalse(context.hasRole("ADMIN"));
}
-
+
@Test
@DisplayName("Should check if context has any of specified roles")
void shouldCheckHasAnyRole() {
AppContext context = AppContext.builder()
- .partyId(UUID.randomUUID())
+ .subject("user-123")
.roles(Set.of("ACCOUNT_OWNER"))
.build();
-
+
assertTrue(context.hasAnyRole("ACCOUNT_OWNER", "ADMIN"));
assertTrue(context.hasAnyRole("VIEWER", "ACCOUNT_OWNER"));
assertFalse(context.hasAnyRole("ADMIN", "VIEWER"));
}
-
+
@Test
@DisplayName("Should check if context has all specified roles")
void shouldCheckHasAllRoles() {
AppContext context = AppContext.builder()
- .partyId(UUID.randomUUID())
+ .subject("user-123")
.roles(Set.of("ACCOUNT_OWNER", "ADMIN", "VIEWER"))
.build();
-
+
assertTrue(context.hasAllRoles("ACCOUNT_OWNER", "ADMIN"));
assertTrue(context.hasAllRoles("VIEWER"));
assertFalse(context.hasAllRoles("ACCOUNT_OWNER", "EDITOR"));
}
-
+
@Test
@DisplayName("Should handle null roles gracefully")
void shouldHandleNullRoles() {
AppContext context = AppContext.builder()
- .partyId(UUID.randomUUID())
+ .subject("user-123")
.build();
-
+
assertFalse(context.hasRole("ADMIN"));
assertFalse(context.hasAnyRole("ADMIN", "VIEWER"));
assertFalse(context.hasAllRoles("ADMIN"));
}
-
+
@Test
@DisplayName("Should check if context has specific permission")
void shouldCheckHasPermission() {
AppContext context = AppContext.builder()
- .partyId(UUID.randomUUID())
+ .subject("user-123")
.permissions(Set.of("READ", "WRITE"))
.build();
-
+
assertTrue(context.hasPermission("READ"));
assertTrue(context.hasPermission("WRITE"));
assertFalse(context.hasPermission("DELETE"));
}
-
+
@Test
- @DisplayName("Should check if context has contract")
- void shouldCheckHasContract() {
- AppContext withContract = AppContext.builder()
- .partyId(UUID.randomUUID())
- .contractId(UUID.randomUUID())
- .build();
-
- AppContext withoutContract = AppContext.builder()
- .partyId(UUID.randomUUID())
+ @DisplayName("Should handle null permissions gracefully")
+ void shouldHandleNullPermissions() {
+ AppContext context = AppContext.builder()
+ .subject("user-123")
.build();
-
- assertTrue(withContract.hasContract());
- assertFalse(withoutContract.hasContract());
+
+ assertFalse(context.hasPermission("READ"));
}
-
+
@Test
- @DisplayName("Should check if context has product")
- void shouldCheckHasProduct() {
- AppContext withProduct = AppContext.builder()
- .partyId(UUID.randomUUID())
- .productId(UUID.randomUUID())
+ @DisplayName("Should carry an optional tenant id")
+ void shouldCarryOptionalTenantId() {
+ UUID tenantId = UUID.randomUUID();
+
+ AppContext withTenant = AppContext.builder()
+ .subject("user-123")
+ .tenantId(tenantId)
.build();
-
- AppContext withoutProduct = AppContext.builder()
- .partyId(UUID.randomUUID())
+
+ AppContext withoutTenant = AppContext.builder()
+ .subject("user-123")
.build();
-
- assertTrue(withProduct.hasProduct());
- assertFalse(withoutProduct.hasProduct());
+
+ assertEquals(tenantId, withTenant.getTenantId());
+ assertNull(withoutTenant.getTenantId());
}
-
+
@Test
@DisplayName("Should store and retrieve attributes")
void shouldStoreAndRetrieveAttributes() {
@@ -143,61 +133,61 @@ void shouldStoreAndRetrieveAttributes() {
attributes.put("key1", "value1");
attributes.put("key2", 123);
attributes.put("key3", true);
-
+
AppContext context = AppContext.builder()
- .partyId(UUID.randomUUID())
+ .subject("user-123")
.attributes(attributes)
.build();
-
+
assertEquals("value1", context.getAttribute("key1"));
assertEquals(123, context.getAttribute("key2"));
assertEquals(true, context.getAttribute("key3"));
assertNull(context.getAttribute("nonexistent"));
}
-
+
@Test
@DisplayName("Should handle null attributes gracefully")
void shouldHandleNullAttributes() {
AppContext context = AppContext.builder()
- .partyId(UUID.randomUUID())
+ .subject("user-123")
.build();
-
+
assertNull(context.getAttribute("anyKey"));
}
-
+
@Test
@DisplayName("Should support immutable updates with withers")
void shouldSupportImmutableUpdates() {
- UUID originalPartyId = UUID.randomUUID();
- UUID newPartyId = UUID.randomUUID();
-
+ String originalSubject = "user-original";
+ String newSubject = "user-new";
+
AppContext original = AppContext.builder()
- .partyId(originalPartyId)
+ .subject(originalSubject)
.build();
-
- AppContext updated = original.withPartyId(newPartyId);
-
- assertEquals(originalPartyId, original.getPartyId());
- assertEquals(newPartyId, updated.getPartyId());
+
+ AppContext updated = original.withSubject(newSubject);
+
+ assertEquals(originalSubject, original.getSubject());
+ assertEquals(newSubject, updated.getSubject());
assertNotSame(original, updated);
}
-
+
@Test
@DisplayName("Should support builder pattern with toBuilder")
void shouldSupportToBuilder() {
- UUID originalContractId = UUID.randomUUID();
- UUID newContractId = UUID.randomUUID();
-
+ Set originalRoles = Set.of("VIEWER");
+ Set newRoles = Set.of("ADMIN");
+
AppContext original = AppContext.builder()
- .partyId(UUID.randomUUID())
- .contractId(originalContractId)
+ .subject("user-123")
+ .roles(originalRoles)
.build();
-
+
AppContext updated = original.toBuilder()
- .contractId(newContractId)
+ .roles(newRoles)
.build();
-
- assertEquals(originalContractId, original.getContractId());
- assertEquals(newContractId, updated.getContractId());
+
+ assertEquals(originalRoles, original.getRoles());
+ assertEquals(newRoles, updated.getRoles());
}
}
diff --git a/src/test/java/org/fireflyframework/common/application/context/AppSecurityContextTest.java b/src/test/java/org/fireflyframework/common/application/context/AppSecurityContextTest.java
index 4729fc0..477b22e 100644
--- a/src/test/java/org/fireflyframework/common/application/context/AppSecurityContextTest.java
+++ b/src/test/java/org/fireflyframework/common/application/context/AppSecurityContextTest.java
@@ -14,7 +14,7 @@
@DisplayName("AppSecurityContext Tests")
class AppSecurityContextTest {
-
+
@Test
@DisplayName("Should create security context with builder")
void shouldCreateSecurityContextWithBuilder() {
@@ -26,7 +26,7 @@ void shouldCreateSecurityContextWithBuilder() {
.authorized(true)
.configSource(SecurityConfigSource.ANNOTATION)
.build();
-
+
assertEquals("/api/accounts", context.getEndpoint());
assertEquals("POST", context.getHttpMethod());
assertTrue(context.getRequiredRoles().contains("ADMIN"));
@@ -34,100 +34,100 @@ void shouldCreateSecurityContextWithBuilder() {
assertTrue(context.isAuthorized());
assertEquals(SecurityConfigSource.ANNOTATION, context.getConfigSource());
}
-
+
@Test
@DisplayName("Should check if has required roles")
void shouldCheckHasRequiredRoles() {
AppSecurityContext withRoles = AppSecurityContext.builder()
.requiredRoles(Set.of("USER"))
.build();
-
+
AppSecurityContext withoutRoles = AppSecurityContext.builder()
.build();
-
+
AppSecurityContext withEmptyRoles = AppSecurityContext.builder()
.requiredRoles(Set.of())
.build();
-
+
assertTrue(withRoles.hasRequiredRoles());
assertFalse(withoutRoles.hasRequiredRoles());
assertFalse(withEmptyRoles.hasRequiredRoles());
}
-
+
@Test
@DisplayName("Should check if has required permissions")
void shouldCheckHasRequiredPermissions() {
AppSecurityContext withPermissions = AppSecurityContext.builder()
.requiredPermissions(Set.of("READ"))
.build();
-
+
AppSecurityContext withoutPermissions = AppSecurityContext.builder()
.build();
-
+
assertTrue(withPermissions.hasRequiredPermissions());
assertFalse(withoutPermissions.hasRequiredPermissions());
}
-
+
@Test
@DisplayName("Should check if requires specific role")
void shouldCheckRequiresRole() {
AppSecurityContext context = AppSecurityContext.builder()
.requiredRoles(Set.of("ADMIN", "EDITOR"))
.build();
-
+
assertTrue(context.requiresRole("ADMIN"));
assertTrue(context.requiresRole("EDITOR"));
assertFalse(context.requiresRole("VIEWER"));
}
-
+
@Test
@DisplayName("Should check if requires specific permission")
void shouldCheckRequiresPermission() {
AppSecurityContext context = AppSecurityContext.builder()
.requiredPermissions(Set.of("READ", "WRITE"))
.build();
-
+
assertTrue(context.requiresPermission("READ"));
assertTrue(context.requiresPermission("WRITE"));
assertFalse(context.requiresPermission("DELETE"));
}
-
+
@Test
@DisplayName("Should store and retrieve security attributes")
void shouldStoreAndRetrieveSecurityAttributes() {
Map attributes = new HashMap<>();
attributes.put("key1", "value1");
attributes.put("key2", 123);
-
+
AppSecurityContext context = AppSecurityContext.builder()
.securityAttributes(attributes)
.build();
-
+
assertEquals("value1", context.getSecurityAttribute("key1"));
assertEquals(123, context.getSecurityAttribute("key2"));
assertNull(context.getSecurityAttribute("nonexistent"));
}
-
+
@Test
@DisplayName("Should default requiresAuthentication to true")
void shouldDefaultRequiresAuthenticationToTrue() {
AppSecurityContext context = AppSecurityContext.builder()
.endpoint("/api/test")
.build();
-
+
assertTrue(context.isRequiresAuthentication());
}
-
+
@Test
@DisplayName("Should default allowAnonymous to false")
void shouldDefaultAllowAnonymousToFalse() {
AppSecurityContext context = AppSecurityContext.builder()
.endpoint("/api/test")
.build();
-
+
assertFalse(context.isAllowAnonymous());
}
-
+
@Test
@DisplayName("Should allow anonymous access when configured")
void shouldAllowAnonymousAccessWhenConfigured() {
@@ -136,11 +136,11 @@ void shouldAllowAnonymousAccessWhenConfigured() {
.allowAnonymous(true)
.requiresAuthentication(false)
.build();
-
+
assertTrue(context.isAllowAnonymous());
assertFalse(context.isRequiresAuthentication());
}
-
+
@Test
@DisplayName("Should store authorization failure reason")
void shouldStoreAuthorizationFailureReason() {
@@ -149,53 +149,53 @@ void shouldStoreAuthorizationFailureReason() {
.authorized(false)
.authorizationFailureReason("Insufficient permissions")
.build();
-
+
assertFalse(context.isAuthorized());
assertEquals("Insufficient permissions", context.getAuthorizationFailureReason());
}
-
+
@Test
@DisplayName("Should support different security config sources")
void shouldSupportDifferentSecurityConfigSources() {
AppSecurityContext annotation = AppSecurityContext.builder()
.configSource(SecurityConfigSource.ANNOTATION)
.build();
-
+
AppSecurityContext explicitMap = AppSecurityContext.builder()
.configSource(SecurityConfigSource.EXPLICIT_MAP)
.build();
-
- AppSecurityContext securityCenter = AppSecurityContext.builder()
- .configSource(SecurityConfigSource.SECURITY_CENTER)
+
+ AppSecurityContext policy = AppSecurityContext.builder()
+ .configSource(SecurityConfigSource.POLICY)
.build();
-
+
AppSecurityContext defaultSource = AppSecurityContext.builder()
.configSource(SecurityConfigSource.DEFAULT)
.build();
-
+
assertEquals(SecurityConfigSource.ANNOTATION, annotation.getConfigSource());
assertEquals(SecurityConfigSource.EXPLICIT_MAP, explicitMap.getConfigSource());
- assertEquals(SecurityConfigSource.SECURITY_CENTER, securityCenter.getConfigSource());
+ assertEquals(SecurityConfigSource.POLICY, policy.getConfigSource());
assertEquals(SecurityConfigSource.DEFAULT, defaultSource.getConfigSource());
}
-
+
@Test
@DisplayName("Should store security evaluation result")
void shouldStoreSecurityEvaluationResult() {
Instant now = Instant.now();
-
+
SecurityEvaluationResult evalResult = SecurityEvaluationResult.builder()
.granted(true)
.reason("Policy ALLOW_ADMIN matched")
.evaluatedPolicy("ALLOW_ADMIN")
.evaluatedAt(now)
.build();
-
+
AppSecurityContext context = AppSecurityContext.builder()
.endpoint("/api/test")
.evaluationResult(evalResult)
.build();
-
+
assertNotNull(context.getEvaluationResult());
assertTrue(context.getEvaluationResult().isGranted());
assertEquals("Policy ALLOW_ADMIN matched", context.getEvaluationResult().getReason());
@@ -205,45 +205,45 @@ void shouldStoreSecurityEvaluationResult() {
@DisplayName("SecurityEvaluationResult Tests")
class SecurityEvaluationResultTest {
-
+
@Test
@DisplayName("Should create evaluation result with builder")
void shouldCreateEvaluationResultWithBuilder() {
Instant now = Instant.now();
-
+
SecurityEvaluationResult result = SecurityEvaluationResult.builder()
.granted(true)
.reason("Access granted")
.evaluatedPolicy("POLICY_001")
.evaluatedAt(now)
.build();
-
+
assertTrue(result.isGranted());
assertEquals("Access granted", result.getReason());
assertEquals("POLICY_001", result.getEvaluatedPolicy());
assertEquals(now, result.getEvaluatedAt());
}
-
+
@Test
@DisplayName("Should store evaluation details")
void shouldStoreEvaluationDetails() {
Map details = Map.of(
- "evaluator", "SecurityCenter",
+ "evaluator", "PolicyEngine",
"confidence", 0.95,
"rulesEvaluated", 5
);
-
+
SecurityEvaluationResult result = SecurityEvaluationResult.builder()
.granted(true)
.evaluationDetails(details)
.build();
-
- assertEquals("SecurityCenter", result.getEvaluationDetail("evaluator"));
+
+ assertEquals("PolicyEngine", result.getEvaluationDetail("evaluator"));
assertEquals(0.95, result.getEvaluationDetail("confidence"));
assertEquals(5, result.getEvaluationDetail("rulesEvaluated"));
assertNull(result.getEvaluationDetail("nonexistent"));
}
-
+
@Test
@DisplayName("Should handle denial with reason")
void shouldHandleDenialWithReason() {
@@ -252,12 +252,12 @@ void shouldHandleDenialWithReason() {
.reason("User lacks required role: ADMIN")
.evaluatedPolicy("REQUIRE_ADMIN_ROLE")
.build();
-
+
assertFalse(result.isGranted());
assertEquals("User lacks required role: ADMIN", result.getReason());
assertEquals("REQUIRE_ADMIN_ROLE", result.getEvaluatedPolicy());
}
-
+
@Test
@DisplayName("Should support immutable updates with withers")
void shouldSupportImmutableUpdates() {
@@ -265,9 +265,9 @@ void shouldSupportImmutableUpdates() {
.granted(false)
.reason("Original reason")
.build();
-
+
SecurityEvaluationResult updated = original.withGranted(true);
-
+
assertFalse(original.isGranted());
assertTrue(updated.isGranted());
assertNotSame(original, updated);
diff --git a/src/test/java/org/fireflyframework/common/application/context/ApplicationExecutionContextTest.java b/src/test/java/org/fireflyframework/common/application/context/ApplicationExecutionContextTest.java
index 2763c24..3181615 100644
--- a/src/test/java/org/fireflyframework/common/application/context/ApplicationExecutionContextTest.java
+++ b/src/test/java/org/fireflyframework/common/application/context/ApplicationExecutionContextTest.java
@@ -11,120 +11,81 @@
@DisplayName("ApplicationExecutionContext Tests")
class ApplicationExecutionContextTest {
-
+
@Test
@DisplayName("Should create execution context with builder")
void shouldCreateExecutionContextWithBuilder() {
- UUID partyId = UUID.randomUUID();
+ String subject = "user-123";
UUID tenantId = UUID.randomUUID();
- UUID contractId = UUID.randomUUID();
-
+
AppContext context = AppContext.builder()
- .partyId(partyId)
+ .subject(subject)
.tenantId(tenantId)
- .contractId(contractId)
.roles(Set.of("USER"))
.build();
-
+
AppConfig config = AppConfig.builder()
.tenantId(tenantId)
.tenantName("Test Tenant")
.build();
-
+
AppSecurityContext securityContext = AppSecurityContext.builder()
.endpoint("/api/test")
.httpMethod("GET")
.authorized(true)
.build();
-
+
ApplicationExecutionContext execContext = ApplicationExecutionContext.builder()
.context(context)
.config(config)
.securityContext(securityContext)
.build();
-
+
assertNotNull(execContext.getContext());
assertNotNull(execContext.getConfig());
assertNotNull(execContext.getSecurityContext());
- assertEquals(partyId, execContext.getPartyId());
+ assertEquals(subject, execContext.getSubject());
assertEquals(tenantId, execContext.getTenantId());
- assertEquals(contractId, execContext.getContractId());
}
-
+
@Test
@DisplayName("Should create minimal execution context")
void shouldCreateMinimalExecutionContext() {
- UUID partyId = UUID.randomUUID();
+ String subject = "user-123";
UUID tenantId = UUID.randomUUID();
-
- ApplicationExecutionContext context = ApplicationExecutionContext.createMinimal(partyId, tenantId);
-
+
+ ApplicationExecutionContext context = ApplicationExecutionContext.createMinimal(subject, tenantId);
+
assertNotNull(context);
- assertEquals(partyId, context.getPartyId());
+ assertEquals(subject, context.getSubject());
assertEquals(tenantId, context.getTenantId());
assertNotNull(context.getContext());
assertNotNull(context.getConfig());
assertNull(context.getSecurityContext());
}
-
+
@Test
@DisplayName("Should get tenantId from config")
void shouldGetTenantIdFromConfig() {
UUID tenantId = UUID.randomUUID();
-
+
ApplicationExecutionContext context = ApplicationExecutionContext.createMinimal(
- UUID.randomUUID(), tenantId);
-
+ "user-123", tenantId);
+
assertEquals(tenantId, context.getTenantId());
}
-
+
@Test
- @DisplayName("Should get partyId from context")
- void shouldGetPartyIdFromContext() {
- UUID partyId = UUID.randomUUID();
-
+ @DisplayName("Should get subject from context")
+ void shouldGetSubjectFromContext() {
+ String subject = "user-456";
+
ApplicationExecutionContext context = ApplicationExecutionContext.createMinimal(
- partyId, UUID.randomUUID());
-
- assertEquals(partyId, context.getPartyId());
- }
-
- @Test
- @DisplayName("Should get contractId from context")
- void shouldGetContractIdFromContext() {
- UUID contractId = UUID.randomUUID();
-
- AppContext appContext = AppContext.builder()
- .partyId(UUID.randomUUID())
- .contractId(contractId)
- .build();
-
- ApplicationExecutionContext context = ApplicationExecutionContext.builder()
- .context(appContext)
- .config(AppConfig.builder().tenantId(UUID.randomUUID()).build())
- .build();
-
- assertEquals(contractId, context.getContractId());
- }
-
- @Test
- @DisplayName("Should get productId from context")
- void shouldGetProductIdFromContext() {
- UUID productId = UUID.randomUUID();
-
- AppContext appContext = AppContext.builder()
- .partyId(UUID.randomUUID())
- .productId(productId)
- .build();
-
- ApplicationExecutionContext context = ApplicationExecutionContext.builder()
- .context(appContext)
- .config(AppConfig.builder().tenantId(UUID.randomUUID()).build())
- .build();
-
- assertEquals(productId, context.getProductId());
+ subject, UUID.randomUUID());
+
+ assertEquals(subject, context.getSubject());
}
-
+
@Test
@DisplayName("Should check if context is authorized")
void shouldCheckIfAuthorized() {
@@ -132,52 +93,52 @@ void shouldCheckIfAuthorized() {
.endpoint("/api/test")
.authorized(true)
.build();
-
+
AppSecurityContext unauthorizedSecurity = AppSecurityContext.builder()
.endpoint("/api/test")
.authorized(false)
.build();
-
+
ApplicationExecutionContext authorized = ApplicationExecutionContext.builder()
- .context(AppContext.builder().partyId(UUID.randomUUID()).build())
+ .context(AppContext.builder().subject("user-123").build())
.config(AppConfig.builder().tenantId(UUID.randomUUID()).build())
.securityContext(authorizedSecurity)
.build();
-
+
ApplicationExecutionContext unauthorized = ApplicationExecutionContext.builder()
- .context(AppContext.builder().partyId(UUID.randomUUID()).build())
+ .context(AppContext.builder().subject("user-123").build())
.config(AppConfig.builder().tenantId(UUID.randomUUID()).build())
.securityContext(unauthorizedSecurity)
.build();
-
+
ApplicationExecutionContext noSecurity = ApplicationExecutionContext.builder()
- .context(AppContext.builder().partyId(UUID.randomUUID()).build())
+ .context(AppContext.builder().subject("user-123").build())
.config(AppConfig.builder().tenantId(UUID.randomUUID()).build())
.build();
-
+
assertTrue(authorized.isAuthorized());
assertFalse(unauthorized.isAuthorized());
assertFalse(noSecurity.isAuthorized());
}
-
+
@Test
@DisplayName("Should check if context has role")
void shouldCheckIfHasRole() {
AppContext appContext = AppContext.builder()
- .partyId(UUID.randomUUID())
+ .subject("user-123")
.roles(Set.of("ADMIN", "USER"))
.build();
-
+
ApplicationExecutionContext context = ApplicationExecutionContext.builder()
.context(appContext)
.config(AppConfig.builder().tenantId(UUID.randomUUID()).build())
.build();
-
+
assertTrue(context.hasRole("ADMIN"));
assertTrue(context.hasRole("USER"));
assertFalse(context.hasRole("VIEWER"));
}
-
+
@Test
@DisplayName("Should check if feature is enabled")
void shouldCheckIfFeatureEnabled() {
@@ -185,53 +146,53 @@ void shouldCheckIfFeatureEnabled() {
.tenantId(UUID.randomUUID())
.featureFlags(Map.of("FEATURE_A", true, "FEATURE_B", false))
.build();
-
+
ApplicationExecutionContext context = ApplicationExecutionContext.builder()
- .context(AppContext.builder().partyId(UUID.randomUUID()).build())
+ .context(AppContext.builder().subject("user-123").build())
.config(appConfig)
.build();
-
+
assertTrue(context.isFeatureEnabled("FEATURE_A"));
assertFalse(context.isFeatureEnabled("FEATURE_B"));
assertFalse(context.isFeatureEnabled("FEATURE_C"));
}
-
+
@Test
@DisplayName("Should support immutable updates with withers")
void shouldSupportImmutableUpdates() {
- UUID originalPartyId = UUID.randomUUID();
- UUID newPartyId = UUID.randomUUID();
-
+ String originalSubject = "user-original";
+ String newSubject = "user-new";
+
AppContext originalContext = AppContext.builder()
- .partyId(originalPartyId)
+ .subject(originalSubject)
.build();
-
+
ApplicationExecutionContext original = ApplicationExecutionContext.builder()
.context(originalContext)
.config(AppConfig.builder().tenantId(UUID.randomUUID()).build())
.build();
-
- AppContext newContext = originalContext.withPartyId(newPartyId);
+
+ AppContext newContext = originalContext.withSubject(newSubject);
ApplicationExecutionContext updated = original.withContext(newContext);
-
- assertEquals(originalPartyId, original.getPartyId());
- assertEquals(newPartyId, updated.getPartyId());
+
+ assertEquals(originalSubject, original.getSubject());
+ assertEquals(newSubject, updated.getSubject());
assertNotSame(original, updated);
}
-
+
@Test
@DisplayName("Should support toBuilder pattern")
void shouldSupportToBuilder() {
UUID originalTenantId = UUID.randomUUID();
UUID newTenantId = UUID.randomUUID();
-
+
ApplicationExecutionContext original = ApplicationExecutionContext.createMinimal(
- UUID.randomUUID(), originalTenantId);
-
+ "user-123", originalTenantId);
+
ApplicationExecutionContext updated = original.toBuilder()
.config(AppConfig.builder().tenantId(newTenantId).build())
.build();
-
+
assertEquals(originalTenantId, original.getTenantId());
assertEquals(newTenantId, updated.getTenantId());
}
diff --git a/src/test/java/org/fireflyframework/common/application/controller/AbstractApplicationControllerTest.java b/src/test/java/org/fireflyframework/common/application/controller/AbstractApplicationControllerTest.java
index 6ae19d7..d63853b 100644
--- a/src/test/java/org/fireflyframework/common/application/controller/AbstractApplicationControllerTest.java
+++ b/src/test/java/org/fireflyframework/common/application/controller/AbstractApplicationControllerTest.java
@@ -37,146 +37,144 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
- * Unit tests for AbstractApplicationController.
- * Tests application-layer context resolution (no contract or product).
+ * Unit tests for {@link AbstractApplicationController}.
+ *
+ * The controller resolves the product-agnostic {@link AppContext} (subject, tenant, roles,
+ * permissions) from the {@link ContextResolver} and combines it with the tenant {@link AppConfig}.
+ * There is no contract or product scoping at this layer.
*/
@ExtendWith(MockitoExtension.class)
class AbstractApplicationControllerTest {
-
+
@Mock
private ContextResolver contextResolver;
-
+
@Mock
private ConfigResolver configResolver;
-
+
@Mock
private ServerWebExchange exchange;
-
+
private TestApplicationController controller;
-
- private UUID testPartyId;
+
+ private String testSubject;
private UUID testTenantId;
-
+
@BeforeEach
void setUp() {
controller = new TestApplicationController();
ReflectionTestUtils.setField(controller, "contextResolver", contextResolver);
ReflectionTestUtils.setField(controller, "configResolver", configResolver);
-
- testPartyId = UUID.randomUUID();
+
+ testSubject = "user-" + UUID.randomUUID();
testTenantId = UUID.randomUUID();
}
-
+
@Test
void shouldResolveApplicationLayerContext() {
// Given
AppContext appContext = AppContext.builder()
- .partyId(testPartyId)
+ .subject(testSubject)
.tenantId(testTenantId)
- .contractId(null) // No contract for application-layer
- .productId(null) // No product for application-layer
.roles(Set.of("customer:onboard"))
.permissions(Set.of())
.build();
-
+
AppConfig appConfig = AppConfig.builder()
.tenantId(testTenantId)
.tenantName("Test Tenant")
.build();
-
- when(contextResolver.resolveContext(any(ServerWebExchange.class), isNull(), isNull()))
+
+ when(contextResolver.resolveContext(any(ServerWebExchange.class)))
.thenReturn(Mono.just(appContext));
when(configResolver.resolveConfig(testTenantId))
.thenReturn(Mono.just(appConfig));
-
+
// When
Mono result = controller.resolveExecutionContext(exchange);
-
+
// Then
StepVerifier.create(result)
.assertNext(ctx -> {
assertThat(ctx).isNotNull();
assertThat(ctx.getContext()).isEqualTo(appContext);
assertThat(ctx.getConfig()).isEqualTo(appConfig);
- assertThat(ctx.getContext().getPartyId()).isEqualTo(testPartyId);
+ assertThat(ctx.getContext().getSubject()).isEqualTo(testSubject);
assertThat(ctx.getContext().getTenantId()).isEqualTo(testTenantId);
- assertThat(ctx.getContext().getContractId()).isNull();
- assertThat(ctx.getContext().getProductId()).isNull();
})
.verifyComplete();
-
- verify(contextResolver).resolveContext(eq(exchange), isNull(), isNull());
+
+ verify(contextResolver).resolveContext(eq(exchange));
verify(configResolver).resolveConfig(testTenantId);
}
-
+
@Test
void shouldHandleContextResolutionError() {
// Given
- when(contextResolver.resolveContext(any(ServerWebExchange.class), isNull(), isNull()))
- .thenReturn(Mono.error(new IllegalStateException("X-Party-Id header not found")));
-
+ when(contextResolver.resolveContext(any(ServerWebExchange.class)))
+ .thenReturn(Mono.error(new IllegalStateException("No authenticated principal")));
+
// When
Mono result = controller.resolveExecutionContext(exchange);
-
+
// Then
StepVerifier.create(result)
- .expectErrorMatches(error ->
+ .expectErrorMatches(error ->
error instanceof IllegalStateException &&
- error.getMessage().contains("X-Party-Id header not found"))
+ error.getMessage().contains("No authenticated principal"))
.verify();
}
-
+
@Test
void shouldHandleConfigResolutionError() {
// Given
AppContext appContext = AppContext.builder()
- .partyId(testPartyId)
+ .subject(testSubject)
.tenantId(testTenantId)
.build();
-
- when(contextResolver.resolveContext(any(ServerWebExchange.class), isNull(), isNull()))
+
+ when(contextResolver.resolveContext(any(ServerWebExchange.class)))
.thenReturn(Mono.just(appContext));
when(configResolver.resolveConfig(testTenantId))
.thenReturn(Mono.error(new RuntimeException("Config service unavailable")));
-
+
// When
Mono result = controller.resolveExecutionContext(exchange);
-
+
// Then
StepVerifier.create(result)
- .expectErrorMatches(error ->
+ .expectErrorMatches(error ->
error instanceof RuntimeException &&
error.getMessage().contains("Config service unavailable"))
.verify();
}
-
+
@Test
void shouldResolveContextWithRolesAndPermissions() {
// Given
AppContext appContext = AppContext.builder()
- .partyId(testPartyId)
+ .subject(testSubject)
.tenantId(testTenantId)
.roles(Set.of("customer:onboard", "customer:viewer"))
.permissions(Set.of("profile:read", "profile:update"))
.build();
-
+
AppConfig appConfig = AppConfig.builder()
.tenantId(testTenantId)
.build();
-
- when(contextResolver.resolveContext(any(ServerWebExchange.class), isNull(), isNull()))
+
+ when(contextResolver.resolveContext(any(ServerWebExchange.class)))
.thenReturn(Mono.just(appContext));
when(configResolver.resolveConfig(testTenantId))
.thenReturn(Mono.just(appConfig));
-
+
// When
Mono result = controller.resolveExecutionContext(exchange);
-
+
// Then
StepVerifier.create(result)
.assertNext(ctx -> {
@@ -187,9 +185,9 @@ void shouldResolveContextWithRolesAndPermissions() {
})
.verifyComplete();
}
-
+
/**
- * Concrete test implementation of AbstractApplicationController.
+ * Concrete test implementation of {@link AbstractApplicationController}.
*/
private static class TestApplicationController extends AbstractApplicationController {
// Test implementation - inherits all functionality from AbstractApplicationController
diff --git a/src/test/java/org/fireflyframework/common/application/controller/AbstractResourceControllerTest.java b/src/test/java/org/fireflyframework/common/application/controller/AbstractResourceControllerTest.java
index 242deff..892f94c 100644
--- a/src/test/java/org/fireflyframework/common/application/controller/AbstractResourceControllerTest.java
+++ b/src/test/java/org/fireflyframework/common/application/controller/AbstractResourceControllerTest.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright 2024-2026 Firefly Software Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
package org.fireflyframework.common.application.controller;
import org.fireflyframework.common.application.context.AppConfig;
@@ -25,164 +41,114 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+/**
+ * Unit tests for {@link AbstractResourceController}.
+ *
+ * The resource controller is a thin, product-agnostic base: it resolves the
+ * {@link AppContext} (subject, tenant, roles, permissions) from the {@link ContextResolver}
+ * and provides generic operation logging. It carries no contract/product scoping.
+ */
@DisplayName("AbstractResourceController Tests")
@ExtendWith(MockitoExtension.class)
class AbstractResourceControllerTest {
-
+
@Mock
private ContextResolver contextResolver;
-
+
@Mock
private ConfigResolver configResolver;
-
+
@Mock
private ServerWebExchange exchange;
-
+
private TestResourceController controller;
-
- private UUID testPartyId;
+
+ private String testSubject;
private UUID testTenantId;
- private UUID testContractId;
- private UUID testProductId;
-
+
@BeforeEach
void setUp() {
controller = new TestResourceController();
ReflectionTestUtils.setField(controller, "contextResolver", contextResolver);
ReflectionTestUtils.setField(controller, "configResolver", configResolver);
-
- testPartyId = UUID.randomUUID();
+
+ testSubject = "user-" + UUID.randomUUID();
testTenantId = UUID.randomUUID();
- testContractId = UUID.randomUUID();
- testProductId = UUID.randomUUID();
}
-
- @Test
- @DisplayName("Should validate valid context (both IDs)")
- void shouldValidateValidContext() {
- UUID contractId = UUID.randomUUID();
- UUID productId = UUID.randomUUID();
-
- assertDoesNotThrow(() -> controller.testRequireContext(contractId, productId));
- }
-
- @Test
- @DisplayName("Should throw exception for null contract ID")
- void shouldThrowExceptionForNullContractId() {
- IllegalArgumentException exception = assertThrows(
- IllegalArgumentException.class,
- () -> controller.testRequireContext(null, testProductId)
- );
-
- assertTrue(exception.getMessage().contains("contractId is required"));
- }
-
- @Test
- @DisplayName("Should throw exception for null product ID")
- void shouldThrowExceptionForNullProductId() {
- IllegalArgumentException exception = assertThrows(
- IllegalArgumentException.class,
- () -> controller.testRequireContext(testContractId, null)
- );
-
- assertTrue(exception.getMessage().contains("productId is required"));
- }
-
+
@Test
- @DisplayName("Should log operation with resource context")
- void shouldLogOperationWithResourceContext() {
- UUID contractId = UUID.randomUUID();
- UUID productId = UUID.randomUUID();
-
- assertDoesNotThrow(() -> controller.testLogOperation(contractId, productId, "testOperation"));
+ @DisplayName("Should log operation with operation name")
+ void shouldLogOperation() {
+ assertDoesNotThrow(() -> controller.testLogOperation("testOperation"));
}
-
+
@Test
@DisplayName("Should handle null operation name in logging")
void shouldHandleNullOperationNameInLogging() {
- UUID contractId = UUID.randomUUID();
- UUID productId = UUID.randomUUID();
-
- assertDoesNotThrow(() -> controller.testLogOperation(contractId, productId, null));
+ assertDoesNotThrow(() -> controller.testLogOperation(null));
}
-
+
@Test
- @DisplayName("Should resolve resource context (contract + product)")
- void shouldResolveResourceContext() {
+ @DisplayName("Should resolve execution context (subject + tenant + roles + permissions)")
+ void shouldResolveExecutionContext() {
// Given
AppContext appContext = AppContext.builder()
- .partyId(testPartyId)
+ .subject(testSubject)
.tenantId(testTenantId)
- .contractId(testContractId)
- .productId(testProductId)
.roles(Set.of("transaction:viewer"))
.permissions(Set.of("transaction:read"))
.build();
-
+
AppConfig appConfig = AppConfig.builder()
.tenantId(testTenantId)
.tenantName("Test Tenant")
.build();
-
- when(contextResolver.resolveContext(any(ServerWebExchange.class), eq(testContractId), eq(testProductId)))
+
+ when(contextResolver.resolveContext(any(ServerWebExchange.class)))
.thenReturn(Mono.just(appContext));
when(configResolver.resolveConfig(testTenantId))
.thenReturn(Mono.just(appConfig));
-
+
// When
- Mono result = controller.testResolveExecutionContext(
- exchange, testContractId, testProductId);
-
+ Mono result = controller.testResolveExecutionContext(exchange);
+
// Then
StepVerifier.create(result)
.assertNext(ctx -> {
assertThat(ctx).isNotNull();
- assertThat(ctx.getContext().getPartyId()).isEqualTo(testPartyId);
+ assertThat(ctx.getContext().getSubject()).isEqualTo(testSubject);
assertThat(ctx.getContext().getTenantId()).isEqualTo(testTenantId);
- assertThat(ctx.getContext().getContractId()).isEqualTo(testContractId);
- assertThat(ctx.getContext().getProductId()).isEqualTo(testProductId);
+ assertThat(ctx.getContext().getRoles()).containsExactly("transaction:viewer");
+ assertThat(ctx.getContext().getPermissions()).containsExactly("transaction:read");
})
.verifyComplete();
-
- verify(contextResolver).resolveContext(eq(exchange), eq(testContractId), eq(testProductId));
+
+ verify(contextResolver).resolveContext(eq(exchange));
verify(configResolver).resolveConfig(testTenantId);
}
-
- @Test
- @DisplayName("Should throw exception when contract ID is null in context resolution")
- void shouldThrowExceptionWhenContractIdIsNullInContextResolution() {
- // When & Then
- assertThrows(IllegalArgumentException.class, () -> {
- controller.testResolveExecutionContext(exchange, null, testProductId).block();
- });
- }
-
+
@Test
- @DisplayName("Should throw exception when product ID is null in context resolution")
- void shouldThrowExceptionWhenProductIdIsNullInContextResolution() {
- // When & Then
- assertThrows(IllegalArgumentException.class, () -> {
- controller.testResolveExecutionContext(exchange, testContractId, null).block();
- });
+ @DisplayName("Should propagate context resolution error")
+ void shouldPropagateContextResolutionError() {
+ when(contextResolver.resolveContext(any(ServerWebExchange.class)))
+ .thenReturn(Mono.error(new IllegalStateException("No authenticated principal")));
+
+ assertThrows(IllegalStateException.class,
+ () -> controller.testResolveExecutionContext(exchange).block());
}
-
+
/**
- * Concrete test implementation of AbstractResourceController
- * to expose protected methods for testing
+ * Concrete test implementation of {@link AbstractResourceController}
+ * to expose protected methods for testing.
*/
static class TestResourceController extends AbstractResourceController {
-
- public void testRequireContext(UUID contractId, UUID productId) {
- requireContext(contractId, productId);
- }
-
- public void testLogOperation(UUID contractId, UUID productId, String operation) {
- logOperation(contractId, productId, operation);
+
+ public void testLogOperation(String operation) {
+ logOperation(operation);
}
-
- public Mono testResolveExecutionContext(
- ServerWebExchange exchange, UUID contractId, UUID productId) {
- return resolveExecutionContext(exchange, contractId, productId);
+
+ public Mono testResolveExecutionContext(ServerWebExchange exchange) {
+ return resolveExecutionContext(exchange);
}
}
}
diff --git a/src/test/java/org/fireflyframework/common/application/integration/ControllerIntegrationTest.java b/src/test/java/org/fireflyframework/common/application/integration/ControllerIntegrationTest.java
index f2b3c66..4216364 100644
--- a/src/test/java/org/fireflyframework/common/application/integration/ControllerIntegrationTest.java
+++ b/src/test/java/org/fireflyframework/common/application/integration/ControllerIntegrationTest.java
@@ -29,7 +29,6 @@
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
-import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.server.ServerWebExchange;
@@ -42,204 +41,192 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
- * Integration test demonstrating both controller types:
- * - AbstractApplicationController (application-layer, no contract/product)
- * - AbstractResourceController (contract + product, both required)
- *
- * This test validates the complete architecture:
- * 1. Istio injects X-Party-Id header
- * 2. Config-mgmt resolves tenantId from partyId
- * 3. Controllers extract contractId/productId from path variables
- * 4. FireflySessionManager provides roles/permissions (mocked)
- * 5. Context is fully resolved with complete resource hierarchy
+ * Integration test demonstrating both product-agnostic controller types:
+ *
+ * - {@link AbstractApplicationController} (application-layer endpoints)
+ * - {@link AbstractResourceController} (resource endpoints)
+ *
+ *
+ * This test validates the resolution flow:
+ *
+ * - The validated security principal yields the authenticated subject + tenant
+ * - The {@link ContextResolver} produces a product-agnostic {@link AppContext}
+ * (subject, tenant, roles, permissions)
+ * - The {@link ConfigResolver} loads tenant {@link AppConfig}
+ * - The controller assembles a complete {@link ApplicationExecutionContext}
+ *
*/
@ExtendWith(MockitoExtension.class)
@DisplayName("Controller Integration Test - Two Controller Types")
class ControllerIntegrationTest {
-
+
@Mock
private ContextResolver contextResolver;
-
+
@Mock
private ConfigResolver configResolver;
-
+
@Mock
private ServerWebExchange exchange;
-
+
@Mock
private ServerHttpRequest request;
-
- private UUID testPartyId;
+
+ private String testSubject;
private UUID testTenantId;
- private UUID testContractId;
- private UUID testProductId;
-
+
private TestApplicationController applicationController;
private TestResourceController resourceController;
-
+
@BeforeEach
void setUp() {
- testPartyId = UUID.randomUUID();
+ testSubject = "user-" + UUID.randomUUID();
testTenantId = UUID.randomUUID();
- testContractId = UUID.randomUUID();
- testProductId = UUID.randomUUID();
-
+
// Setup controllers
applicationController = new TestApplicationController();
resourceController = new TestResourceController();
-
+
// Inject dependencies
ReflectionTestUtils.setField(applicationController, "contextResolver", contextResolver);
ReflectionTestUtils.setField(applicationController, "configResolver", configResolver);
ReflectionTestUtils.setField(resourceController, "contextResolver", contextResolver);
ReflectionTestUtils.setField(resourceController, "configResolver", configResolver);
}
-
+
@Test
- @DisplayName("Scenario 1: Application-layer endpoint (Onboarding)")
+ @DisplayName("Scenario 1: Application-layer endpoint")
void testApplicationLayerEndpoint() {
- // Given: Onboarding endpoint with only party context
+ // Given: application-layer endpoint with subject + tenant context
AppContext appContext = AppContext.builder()
- .partyId(testPartyId)
+ .subject(testSubject)
.tenantId(testTenantId)
- .contractId(null) // No contract for onboarding
- .productId(null) // No product for onboarding
.roles(Set.of("customer:onboard"))
.permissions(Set.of("profile:create"))
.build();
-
+
AppConfig appConfig = AppConfig.builder()
.tenantId(testTenantId)
.tenantName("Test Bank")
.build();
-
- when(contextResolver.resolveContext(any(ServerWebExchange.class), isNull(), isNull()))
+
+ when(contextResolver.resolveContext(any(ServerWebExchange.class)))
.thenReturn(Mono.just(appContext));
when(configResolver.resolveConfig(testTenantId))
.thenReturn(Mono.just(appConfig));
-
- // When: Call application-layer controller endpoint
+
+ // When: call application-layer controller endpoint
Mono result = applicationController.handleOnboarding(exchange);
-
- // Then: Context is resolved with party + tenant only
+
+ // Then: context is resolved with subject + tenant + roles
StepVerifier.create(result)
.assertNext(ctx -> {
- assertThat(ctx.getContext().getPartyId()).isEqualTo(testPartyId);
+ assertThat(ctx.getContext().getSubject()).isEqualTo(testSubject);
assertThat(ctx.getContext().getTenantId()).isEqualTo(testTenantId);
- assertThat(ctx.getContext().getContractId()).isNull();
- assertThat(ctx.getContext().getProductId()).isNull();
assertThat(ctx.getContext().getRoles()).contains("customer:onboard");
+ assertThat(ctx.getContext().getPermissions()).contains("profile:create");
})
.verifyComplete();
-
- verify(contextResolver).resolveContext(eq(exchange), isNull(), isNull());
+
+ verify(contextResolver).resolveContext(eq(exchange));
verify(configResolver).resolveConfig(testTenantId);
}
-
+
@Test
- @DisplayName("Scenario 2: Resource endpoint (List Transactions with contract + product)")
+ @DisplayName("Scenario 2: Resource endpoint")
void testResourceEndpoint() {
- // Given: Transaction listing endpoint with full context
+ // Given: resource endpoint with subject + tenant + roles/permissions
AppContext appContext = AppContext.builder()
- .partyId(testPartyId)
+ .subject(testSubject)
.tenantId(testTenantId)
- .contractId(testContractId)
- .productId(testProductId)
.roles(Set.of("owner", "transaction:viewer"))
.permissions(Set.of("transaction:read", "transaction:list"))
.build();
-
+
AppConfig appConfig = AppConfig.builder()
.tenantId(testTenantId)
.tenantName("Test Bank")
.build();
-
- when(contextResolver.resolveContext(any(ServerWebExchange.class), eq(testContractId), eq(testProductId)))
+
+ when(contextResolver.resolveContext(any(ServerWebExchange.class)))
.thenReturn(Mono.just(appContext));
when(configResolver.resolveConfig(testTenantId))
.thenReturn(Mono.just(appConfig));
-
- // When: Call resource controller endpoint with contractId + productId (both required)
- Mono result = resourceController.listTransactions(
- testContractId, testProductId, exchange);
-
- // Then: Context is resolved with complete resource hierarchy
+
+ // When: call resource controller endpoint
+ Mono result = resourceController.listTransactions(exchange);
+
+ // Then: context is resolved with subject + tenant + authorities
StepVerifier.create(result)
.assertNext(ctx -> {
- assertThat(ctx.getContext().getPartyId()).isEqualTo(testPartyId);
+ assertThat(ctx.getContext().getSubject()).isEqualTo(testSubject);
assertThat(ctx.getContext().getTenantId()).isEqualTo(testTenantId);
- assertThat(ctx.getContext().getContractId()).isEqualTo(testContractId);
- assertThat(ctx.getContext().getProductId()).isEqualTo(testProductId);
assertThat(ctx.getContext().getRoles()).contains("owner", "transaction:viewer");
+ assertThat(ctx.getContext().getPermissions()).contains("transaction:read");
})
.verifyComplete();
-
- verify(contextResolver).resolveContext(eq(exchange), eq(testContractId), eq(testProductId));
+
+ verify(contextResolver).resolveContext(eq(exchange));
verify(configResolver).resolveConfig(testTenantId);
}
-
+
@Test
- @DisplayName("Scenario 3: End-to-end flow - Onboarding → Access Resources")
+ @DisplayName("Scenario 3: End-to-end flow across both controller types")
void testEndToEndFlow() {
- // Step 1: Onboarding (application-layer, no contract/product)
+ // Step 1: application-layer endpoint
AppContext onboardingContext = AppContext.builder()
- .partyId(testPartyId)
+ .subject(testSubject)
.tenantId(testTenantId)
.roles(Set.of("customer:onboard"))
.build();
-
+
AppConfig config = AppConfig.builder().tenantId(testTenantId).build();
-
- when(contextResolver.resolveContext(any(), isNull(), isNull()))
+
+ when(contextResolver.resolveContext(any(ServerWebExchange.class)))
.thenReturn(Mono.just(onboardingContext));
when(configResolver.resolveConfig(testTenantId))
.thenReturn(Mono.just(config));
-
+
StepVerifier.create(applicationController.handleOnboarding(exchange))
.assertNext(ctx -> {
- assertThat(ctx.getContext().getContractId()).isNull();
- assertThat(ctx.getContext().getProductId()).isNull();
+ assertThat(ctx.getContext().getSubject()).isEqualTo(testSubject);
+ assertThat(ctx.getContext().getRoles()).contains("customer:onboard");
})
.verifyComplete();
-
- // Step 2: After onboarding, access resources with contract + product (both required)
+
+ // Step 2: resource endpoint resolved from the same authenticated principal
AppContext resourceContext = AppContext.builder()
- .partyId(testPartyId)
+ .subject(testSubject)
.tenantId(testTenantId)
- .contractId(testContractId)
- .productId(testProductId)
.roles(Set.of("owner", "transaction:viewer"))
.build();
-
- when(contextResolver.resolveContext(any(), eq(testContractId), eq(testProductId)))
+
+ when(contextResolver.resolveContext(any(ServerWebExchange.class)))
.thenReturn(Mono.just(resourceContext));
-
- StepVerifier.create(resourceController.listTransactions(testContractId, testProductId, exchange))
+
+ StepVerifier.create(resourceController.listTransactions(exchange))
.assertNext(ctx -> {
- assertThat(ctx.getContext().getContractId()).isEqualTo(testContractId);
- assertThat(ctx.getContext().getProductId()).isEqualTo(testProductId);
+ assertThat(ctx.getContext().getSubject()).isEqualTo(testSubject);
assertThat(ctx.getContext().getRoles()).contains("owner", "transaction:viewer");
})
.verifyComplete();
}
-
+
// Test controller implementations
-
+
static class TestApplicationController extends AbstractApplicationController {
public Mono handleOnboarding(ServerWebExchange exchange) {
return resolveExecutionContext(exchange);
}
}
-
+
static class TestResourceController extends AbstractResourceController {
- public Mono listTransactions(
- UUID contractId, UUID productId, ServerWebExchange exchange) {
- return resolveExecutionContext(exchange, contractId, productId);
+ public Mono listTransactions(ServerWebExchange exchange) {
+ return resolveExecutionContext(exchange);
}
}
}
diff --git a/src/test/java/org/fireflyframework/common/application/integration/SecurityAspectIntegrationTest.java b/src/test/java/org/fireflyframework/common/application/integration/SecurityAspectIntegrationTest.java
index 350f677..179d012 100644
--- a/src/test/java/org/fireflyframework/common/application/integration/SecurityAspectIntegrationTest.java
+++ b/src/test/java/org/fireflyframework/common/application/integration/SecurityAspectIntegrationTest.java
@@ -41,59 +41,59 @@
/**
* Integration tests for {@link SecurityAspect}.
- * Tests AOP interception of @Secure annotations.
+ * Tests AOP interception of {@code @Secure} annotations against the product-agnostic
+ * {@link AppContext} (subject, tenant, roles, permissions).
*/
@DisplayName("SecurityAspect Integration Tests")
class SecurityAspectIntegrationTest {
-
+
private SecurityAuthorizationService authorizationService;
private EndpointSecurityRegistry endpointSecurityRegistry;
private SecurityAspect securityAspect;
private TestService testService;
private TestService proxiedService;
-
+
@BeforeEach
void setUp() {
authorizationService = mock(SecurityAuthorizationService.class);
endpointSecurityRegistry = new EndpointSecurityRegistry();
var applicationProperties = new org.fireflyframework.common.application.config.ApplicationLayerProperties();
- applicationProperties.getSecurity().setUseSecurityCenter(false);
securityAspect = new SecurityAspect(authorizationService, endpointSecurityRegistry, applicationProperties);
-
+
testService = new TestService();
-
+
// Create AOP proxy
AspectJProxyFactory factory = new AspectJProxyFactory(testService);
factory.addAspect(securityAspect);
proxiedService = factory.getProxy();
}
-
+
@Test
@DisplayName("Should allow access when authorization is granted")
void shouldAllowAccessWhenAuthorized() {
// Given
ApplicationExecutionContext context = createExecutionContext();
-
+
when(authorizationService.authorize(any(AppContext.class), any(AppSecurityContext.class)))
.thenReturn(Mono.just(AppSecurityContext.builder()
.endpoint("/test")
.httpMethod("GET")
.authorized(true)
.build()));
-
+
// When
String result = proxiedService.secureMethod(context);
-
+
// Then
assertThat(result).isEqualTo("success");
}
-
+
@Test
@DisplayName("Should deny access when authorization is denied")
void shouldDenyAccessWhenUnauthorized() {
// Given
ApplicationExecutionContext context = createExecutionContext();
-
+
when(authorizationService.authorize(any(AppContext.class), any(AppSecurityContext.class)))
.thenReturn(Mono.just(AppSecurityContext.builder()
.endpoint("/test")
@@ -101,232 +101,232 @@ void shouldDenyAccessWhenUnauthorized() {
.authorized(false)
.authorizationFailureReason("Missing required role")
.build()));
-
+
// When/Then
assertThatThrownBy(() -> proxiedService.secureMethod(context))
.isInstanceOf(AccessDeniedException.class)
.hasMessageContaining("Missing required role");
}
-
+
@Test
@DisplayName("Should intercept method with @Secure annotation")
void shouldInterceptSecureAnnotation() {
// Given
ApplicationExecutionContext context = createExecutionContext();
-
+
when(authorizationService.authorize(any(AppContext.class), any(AppSecurityContext.class)))
.thenReturn(Mono.just(AppSecurityContext.builder()
.endpoint("/test")
.httpMethod("GET")
.authorized(true)
.build()));
-
+
// When
String result = proxiedService.methodWithRoles(context);
-
+
// Then
assertThat(result).isEqualTo("success-with-roles");
}
-
+
@Test
@DisplayName("Should check roles specified in @Secure annotation")
void shouldCheckRolesFromAnnotation() {
// Given
ApplicationExecutionContext context = createExecutionContext();
-
+
when(authorizationService.authorize(any(AppContext.class), any(AppSecurityContext.class)))
.thenAnswer(invocation -> {
AppSecurityContext secContext = invocation.getArgument(1);
-
+
// Verify that the aspect extracted roles from annotation
assertThat(secContext.getRequiredRoles()).containsExactly("ADMIN");
assertThat(secContext.getConfigSource())
.isEqualTo(AppSecurityContext.SecurityConfigSource.ANNOTATION);
-
+
// Simulate authorization success since user has ADMIN role
return Mono.just(secContext.toBuilder()
.authorized(true)
.build());
});
-
+
// When
String result = proxiedService.methodWithRoles(context);
-
+
// Then
assertThat(result).isEqualTo("success-with-roles");
}
-
+
@Test
@DisplayName("Should check permissions specified in @Secure annotation")
void shouldCheckPermissionsFromAnnotation() {
// Given
ApplicationExecutionContext context = createExecutionContext();
-
+
when(authorizationService.authorize(any(AppContext.class), any(AppSecurityContext.class)))
.thenAnswer(invocation -> {
AppSecurityContext secContext = invocation.getArgument(1);
-
+
// Verify that the aspect extracted permissions from annotation
assertThat(secContext.getRequiredPermissions()).containsExactly("WRITE");
assertThat(secContext.getConfigSource())
.isEqualTo(AppSecurityContext.SecurityConfigSource.ANNOTATION);
-
+
// Simulate authorization success since user has WRITE permission
return Mono.just(secContext.toBuilder()
.authorized(true)
.build());
});
-
+
// When
String result = proxiedService.methodWithPermissions(context);
-
+
// Then
assertThat(result).isEqualTo("success-with-permissions");
}
-
+
@Test
@DisplayName("Should skip security check when no execution context is provided")
void shouldSkipSecurityCheckWithoutExecutionContext() {
// When - Call without ExecutionContext
String result = proxiedService.methodWithoutContext();
-
+
// Then - Should execute without security check
assertThat(result).isEqualTo("no-context");
}
-
+
@Test
@DisplayName("Should check both roles and permissions from annotation")
void shouldCheckRolesAndPermissionsFromAnnotation() {
// Given
ApplicationExecutionContext context = createExecutionContext();
-
+
when(authorizationService.authorize(any(AppContext.class), any(AppSecurityContext.class)))
.thenAnswer(invocation -> {
AppSecurityContext secContext = invocation.getArgument(1);
-
+
// Verify that the aspect extracted both roles and permissions
assertThat(secContext.getRequiredRoles()).containsExactly("ADMIN");
assertThat(secContext.getRequiredPermissions()).containsExactly("WRITE");
assertThat(secContext.getConfigSource())
.isEqualTo(AppSecurityContext.SecurityConfigSource.ANNOTATION);
-
+
// Simulate authorization success
return Mono.just(secContext.toBuilder()
.authorized(true)
.build());
});
-
+
// When
String result = proxiedService.methodWithRolesAndPermissions(context);
-
+
// Then
assertThat(result).isEqualTo("success-with-both");
}
-
+
@Test
@DisplayName("Should deny access when user lacks required roles")
void shouldDenyAccessWhenUserLacksRoles() {
// Given - Context with user that has no ADMIN role
ApplicationExecutionContext context = ApplicationExecutionContext.builder()
.context(AppContext.builder()
- .partyId(UUID.randomUUID())
+ .subject("user-" + UUID.randomUUID())
.tenantId(UUID.randomUUID())
.roles(Set.of("USER")) // Only USER role, not ADMIN
.permissions(Set.of("READ"))
.build())
.build();
-
+
when(authorizationService.authorize(any(AppContext.class), any(AppSecurityContext.class)))
.thenAnswer(invocation -> {
AppSecurityContext secContext = invocation.getArgument(1);
-
+
// Simulate authorization service checking and denying
return Mono.just(secContext.toBuilder()
.authorized(false)
.authorizationFailureReason("User does not have required ADMIN role")
.build());
});
-
+
// When/Then
assertThatThrownBy(() -> proxiedService.methodWithRoles(context))
.isInstanceOf(AccessDeniedException.class)
.hasMessageContaining("User does not have required ADMIN role");
}
-
+
@Test
@DisplayName("Should pass AppContext to authorization service")
void shouldPassAppContextToAuthorizationService() {
// Given
- UUID partyId = UUID.randomUUID();
+ String subject = "user-" + UUID.randomUUID();
UUID tenantId = UUID.randomUUID();
ApplicationExecutionContext context = ApplicationExecutionContext.builder()
.context(AppContext.builder()
- .partyId(partyId)
+ .subject(subject)
.tenantId(tenantId)
.roles(Set.of("ADMIN"))
.permissions(Set.of("WRITE"))
.build())
.build();
-
+
when(authorizationService.authorize(any(AppContext.class), any(AppSecurityContext.class)))
.thenAnswer(invocation -> {
AppContext appContext = invocation.getArgument(0);
-
+
// Verify that the aspect passed the correct AppContext
- assertThat(appContext.getPartyId()).isEqualTo(partyId);
+ assertThat(appContext.getSubject()).isEqualTo(subject);
assertThat(appContext.getTenantId()).isEqualTo(tenantId);
assertThat(appContext.getRoles()).contains("ADMIN");
assertThat(appContext.getPermissions()).contains("WRITE");
-
+
AppSecurityContext secContext = invocation.getArgument(1);
return Mono.just(secContext.toBuilder()
.authorized(true)
.build());
});
-
+
// When
String result = proxiedService.secureMethod(context);
-
+
// Then
assertThat(result).isEqualTo("success");
}
-
+
private ApplicationExecutionContext createExecutionContext() {
return ApplicationExecutionContext.builder()
.context(AppContext.builder()
- .partyId(UUID.randomUUID())
+ .subject("user-" + UUID.randomUUID())
.tenantId(UUID.randomUUID())
.roles(Set.of("ADMIN", "USER"))
.permissions(Set.of("READ", "WRITE"))
.build())
.build();
}
-
+
/**
- * Test service with @Secure annotations.
+ * Test service with {@code @Secure} annotations.
*/
static class TestService {
-
+
@Secure
public String secureMethod(ApplicationExecutionContext context) {
return "success";
}
-
+
@Secure(roles = {"ADMIN"})
public String methodWithRoles(ApplicationExecutionContext context) {
return "success-with-roles";
}
-
+
@Secure(permissions = {"WRITE"})
public String methodWithPermissions(ApplicationExecutionContext context) {
return "success-with-permissions";
}
-
+
@Secure(roles = {"ADMIN"}, permissions = {"WRITE"})
public String methodWithRolesAndPermissions(ApplicationExecutionContext context) {
return "success-with-both";
}
-
+
@Secure
public String methodWithoutContext() {
return "no-context";
diff --git a/src/test/java/org/fireflyframework/common/application/integration/SecurityAuthorizationIntegrationTest.java b/src/test/java/org/fireflyframework/common/application/integration/SecurityAuthorizationIntegrationTest.java
index 9af18fa..0b149c5 100644
--- a/src/test/java/org/fireflyframework/common/application/integration/SecurityAuthorizationIntegrationTest.java
+++ b/src/test/java/org/fireflyframework/common/application/integration/SecurityAuthorizationIntegrationTest.java
@@ -19,182 +19,194 @@
import org.fireflyframework.common.application.context.AppContext;
import org.fireflyframework.common.application.context.AppSecurityContext;
import org.fireflyframework.common.application.security.DefaultSecurityAuthorizationService;
-import org.fireflyframework.common.application.spi.SessionManager;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ExtendWith;
-import org.mockito.Mock;
-import org.mockito.junit.jupiter.MockitoExtension;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.util.Set;
import java.util.UUID;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.*;
-
/**
- * Integration test for SessionManager with DefaultSecurityAuthorizationService.
- *
- * Tests the authorization flow including:
+ * Integration tests for {@link DefaultSecurityAuthorizationService}.
+ *
+ * Authorization is decided solely from the roles and permissions already
+ * resolved onto the product-agnostic {@link AppContext}. There is no Security Center / session
+ * dependency and no contract/product scoping.
+ *
*
- * - Product access validation via SessionManager
- * - Role-based authorization
- * - Permission-based authorization
- * - Graceful degradation when Security Center unavailable
+ * - Role-based checks against {@link AppContext#getRoles()}
+ * - Permission-based checks against {@link AppContext#getPermissions()}
+ * - Endpoint authorization via {@link AppSecurityContext} required roles/permissions
*
*/
-@ExtendWith(MockitoExtension.class)
@DisplayName("Security Authorization Integration Tests")
class SecurityAuthorizationIntegrationTest {
-
- @Mock
- private SessionManager sessionManager;
-
+
private DefaultSecurityAuthorizationService authorizationService;
-
- private UUID testPartyId;
+
+ private String testSubject;
private UUID testTenantId;
- private UUID testContractId;
- private UUID testProductId;
-
+
@BeforeEach
void setUp() {
- authorizationService = new DefaultSecurityAuthorizationService(sessionManager);
-
- testPartyId = UUID.randomUUID();
+ authorizationService = new DefaultSecurityAuthorizationService();
+
+ testSubject = "user-" + UUID.randomUUID();
testTenantId = UUID.randomUUID();
- testContractId = UUID.randomUUID();
- testProductId = UUID.randomUUID();
- }
-
- @Test
- @DisplayName("Should validate product access when party has access")
- void shouldValidateProductAccessWhenPartyHasAccess() {
- // Given: Party has product access
- when(sessionManager.hasAccessToProduct(testPartyId, testProductId))
- .thenReturn(Mono.just(true));
-
- // When: Check product access
- Mono result = sessionManager.hasAccessToProduct(testPartyId, testProductId);
-
- // Then: Returns true
- StepVerifier.create(result)
- .expectNext(true)
- .verifyComplete();
-
- verify(sessionManager, times(1)).hasAccessToProduct(testPartyId, testProductId);
- }
-
- @Test
- @DisplayName("Should validate product access when party lacks access")
- void shouldValidateProductAccessWhenPartyLacksAccess() {
- // Given: Party does NOT have product access
- when(sessionManager.hasAccessToProduct(testPartyId, testProductId))
- .thenReturn(Mono.just(false));
-
- // When: Check product access
- Mono result = sessionManager.hasAccessToProduct(testPartyId, testProductId);
-
- // Then: Returns false
- StepVerifier.create(result)
- .expectNext(false)
- .verifyComplete();
-
- verify(sessionManager, times(1)).hasAccessToProduct(testPartyId, testProductId);
- }
-
- @Test
- @DisplayName("Should check specific permission via SessionManager")
- void shouldCheckSpecificPermissionViaSessionManager() {
- // Given: Session manager with permission check
- when(sessionManager.hasPermission(testPartyId, testProductId, "READ", "BALANCE"))
- .thenReturn(Mono.just(true));
-
- // When: Check permission
- Mono result = sessionManager.hasPermission(testPartyId, testProductId, "READ", "BALANCE");
-
- // Then: Returns true
- StepVerifier.create(result)
- .expectNext(true)
- .verifyComplete();
-
- verify(sessionManager, times(1)).hasPermission(testPartyId, testProductId, "READ", "BALANCE");
- }
-
- @Test
- @DisplayName("Should check permission with action and resource type")
- void shouldCheckPermissionWithActionAndResourceType() {
- // Given: Permission check configured
- when(sessionManager.hasPermission(testPartyId, testProductId, "WRITE", "TRANSACTION"))
- .thenReturn(Mono.just(false));
-
- // When: Check permission
- Mono result = sessionManager.hasPermission(testPartyId, testProductId, "WRITE", "TRANSACTION");
-
- // Then: Returns false
- StepVerifier.create(result)
- .expectNext(false)
- .verifyComplete();
-
- verify(sessionManager, times(1)).hasPermission(testPartyId, testProductId, "WRITE", "TRANSACTION");
}
-
-
-
-
-
-
+
@Test
@DisplayName("Should use hasRole from AppContext")
void shouldUseHasRoleFromAppContext() {
// Given
AppContext context = AppContext.builder()
- .partyId(testPartyId)
+ .subject(testSubject)
.tenantId(testTenantId)
.roles(Set.of("owner", "account_viewer"))
.permissions(Set.of())
.build();
-
+
// When
Mono hasOwner = authorizationService.hasRole(context, "owner");
Mono hasAdmin = authorizationService.hasRole(context, "admin");
-
+
// Then
StepVerifier.create(hasOwner)
.expectNext(true)
.verifyComplete();
-
+
StepVerifier.create(hasAdmin)
.expectNext(false)
.verifyComplete();
}
-
+
@Test
@DisplayName("Should use hasPermission from AppContext")
void shouldUseHasPermissionFromAppContext() {
// Given
AppContext context = AppContext.builder()
- .partyId(testPartyId)
+ .subject(testSubject)
.tenantId(testTenantId)
.roles(Set.of("owner"))
.permissions(Set.of("owner:READ:BALANCE", "owner:WRITE:TRANSACTION"))
.build();
-
+
// When
Mono canReadBalance = authorizationService.hasPermission(context, "owner:READ:BALANCE");
Mono canDeleteAccount = authorizationService.hasPermission(context, "owner:DELETE:ACCOUNT");
-
+
// Then
StepVerifier.create(canReadBalance)
.expectNext(true)
.verifyComplete();
-
+
StepVerifier.create(canDeleteAccount)
.expectNext(false)
.verifyComplete();
}
+
+ @Test
+ @DisplayName("Should authorize endpoint when required role is present in AppContext")
+ void shouldAuthorizeWhenRequiredRolePresent() {
+ // Given
+ AppContext context = AppContext.builder()
+ .subject(testSubject)
+ .tenantId(testTenantId)
+ .roles(Set.of("owner", "account_viewer"))
+ .permissions(Set.of())
+ .build();
+
+ AppSecurityContext securityContext = AppSecurityContext.builder()
+ .endpoint("/api/v1/accounts")
+ .httpMethod("GET")
+ .requiredRoles(Set.of("owner"))
+ .configSource(AppSecurityContext.SecurityConfigSource.ANNOTATION)
+ .build();
+
+ // When/Then
+ StepVerifier.create(authorizationService.authorize(context, securityContext))
+ .assertNext(result -> {
+ org.assertj.core.api.Assertions.assertThat(result.isAuthorized()).isTrue();
+ })
+ .verifyComplete();
+ }
+
+ @Test
+ @DisplayName("Should deny endpoint when required role is missing from AppContext")
+ void shouldDenyWhenRequiredRoleMissing() {
+ // Given
+ AppContext context = AppContext.builder()
+ .subject(testSubject)
+ .tenantId(testTenantId)
+ .roles(Set.of("account_viewer"))
+ .permissions(Set.of())
+ .build();
+
+ AppSecurityContext securityContext = AppSecurityContext.builder()
+ .endpoint("/api/v1/accounts")
+ .httpMethod("DELETE")
+ .requiredRoles(Set.of("admin"))
+ .configSource(AppSecurityContext.SecurityConfigSource.ANNOTATION)
+ .build();
+
+ // When/Then
+ StepVerifier.create(authorizationService.authorize(context, securityContext))
+ .assertNext(result -> {
+ org.assertj.core.api.Assertions.assertThat(result.isAuthorized()).isFalse();
+ })
+ .verifyComplete();
+ }
+
+ @Test
+ @DisplayName("Should authorize endpoint when required permission is present in AppContext")
+ void shouldAuthorizeWhenRequiredPermissionPresent() {
+ // Given
+ AppContext context = AppContext.builder()
+ .subject(testSubject)
+ .tenantId(testTenantId)
+ .roles(Set.of("owner"))
+ .permissions(Set.of("owner:READ:BALANCE"))
+ .build();
+
+ AppSecurityContext securityContext = AppSecurityContext.builder()
+ .endpoint("/api/v1/balances")
+ .httpMethod("GET")
+ .requiredPermissions(Set.of("owner:READ:BALANCE"))
+ .configSource(AppSecurityContext.SecurityConfigSource.ANNOTATION)
+ .build();
+
+ // When/Then
+ StepVerifier.create(authorizationService.authorize(context, securityContext))
+ .assertNext(result -> {
+ org.assertj.core.api.Assertions.assertThat(result.isAuthorized()).isTrue();
+ })
+ .verifyComplete();
+ }
+
+ @Test
+ @DisplayName("Should authorize endpoint when no role or permission requirements are declared")
+ void shouldAuthorizeWhenNoRequirements() {
+ // Given
+ AppContext context = AppContext.builder()
+ .subject(testSubject)
+ .tenantId(testTenantId)
+ .roles(Set.of())
+ .permissions(Set.of())
+ .build();
+
+ AppSecurityContext securityContext = AppSecurityContext.builder()
+ .endpoint("/api/v1/public")
+ .httpMethod("GET")
+ .configSource(AppSecurityContext.SecurityConfigSource.ANNOTATION)
+ .build();
+
+ // When/Then
+ StepVerifier.create(authorizationService.authorize(context, securityContext))
+ .assertNext(result -> {
+ org.assertj.core.api.Assertions.assertThat(result.isAuthorized()).isTrue();
+ })
+ .verifyComplete();
+ }
}
diff --git a/src/test/java/org/fireflyframework/common/application/resolver/AbstractContextResolverTest.java b/src/test/java/org/fireflyframework/common/application/resolver/AbstractContextResolverTest.java
index 3fd2ebf..d6ef3e5 100644
--- a/src/test/java/org/fireflyframework/common/application/resolver/AbstractContextResolverTest.java
+++ b/src/test/java/org/fireflyframework/common/application/resolver/AbstractContextResolverTest.java
@@ -20,8 +20,6 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
-import org.springframework.http.HttpHeaders;
-import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
@@ -30,246 +28,154 @@
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.Mockito.*;
+import static org.mockito.Mockito.mock;
/**
* Unit tests for {@link AbstractContextResolver}.
*/
@DisplayName("AbstractContextResolver Tests")
class AbstractContextResolverTest {
-
+
private TestContextResolver contextResolver;
private ServerWebExchange exchange;
- private ServerHttpRequest request;
- private HttpHeaders headers;
-
+
@BeforeEach
void setUp() {
contextResolver = new TestContextResolver();
exchange = mock(ServerWebExchange.class);
- request = mock(ServerHttpRequest.class);
- headers = new HttpHeaders();
-
- when(exchange.getRequest()).thenReturn(request);
- when(request.getHeaders()).thenReturn(headers);
}
-
+
@Test
- @DisplayName("Should resolve context with all IDs")
- void shouldResolveContextWithAllIds() {
+ @DisplayName("Should resolve context with subject and tenant")
+ void shouldResolveContextWithSubjectAndTenant() {
// Given
- UUID partyId = UUID.randomUUID();
+ String subject = "user-123";
UUID tenantId = UUID.randomUUID();
- UUID contractId = UUID.randomUUID();
- UUID productId = UUID.randomUUID();
-
- contextResolver.setPartyId(partyId);
+
+ contextResolver.setSubject(subject);
contextResolver.setTenantId(tenantId);
- contextResolver.setContractId(contractId);
- contextResolver.setProductId(productId);
-
+
// When/Then
StepVerifier.create(contextResolver.resolveContext(exchange))
.assertNext(context -> {
- assertThat(context.getPartyId()).isEqualTo(partyId);
+ assertThat(context.getSubject()).isEqualTo(subject);
assertThat(context.getTenantId()).isEqualTo(tenantId);
- assertThat(context.getContractId()).isEqualTo(contractId);
- assertThat(context.getProductId()).isEqualTo(productId);
})
.verifyComplete();
}
-
- @Test
- @DisplayName("Should extract UUID from attribute")
- void shouldExtractUuidFromAttribute() {
- // Given
- UUID expectedId = UUID.randomUUID();
- when(exchange.getAttribute("testAttribute")).thenReturn(expectedId);
-
- // When/Then
- StepVerifier.create(contextResolver.extractUUID(exchange, "testAttribute", "testHeader"))
- .expectNext(expectedId)
- .verifyComplete();
- }
-
- @Test
- @DisplayName("Should extract UUID from header when attribute is missing")
- void shouldExtractUuidFromHeader() {
- // Given
- UUID expectedId = UUID.randomUUID();
- when(exchange.getAttribute("testAttribute")).thenReturn(null);
- headers.set("testHeader", expectedId.toString());
-
- // When/Then
- StepVerifier.create(contextResolver.extractUUID(exchange, "testAttribute", "testHeader"))
- .expectNext(expectedId)
- .verifyComplete();
- }
-
- @Test
- @DisplayName("Should return empty when UUID not found")
- void shouldReturnEmptyWhenUuidNotFound() {
- // Given
- when(exchange.getAttribute("testAttribute")).thenReturn(null);
-
- // When/Then
- StepVerifier.create(contextResolver.extractUUID(exchange, "testAttribute", "testHeader"))
- .verifyComplete();
- }
-
+
@Test
- @DisplayName("Should return empty when header has invalid UUID format")
- void shouldReturnEmptyForInvalidUuidFormat() {
+ @DisplayName("Should resolve context with empty tenant when single-tenant")
+ void shouldResolveContextWithEmptyTenant() {
// Given
- when(exchange.getAttribute("testAttribute")).thenReturn(null);
- headers.set("testHeader", "invalid-uuid");
-
+ String subject = "user-123";
+ contextResolver.setSubject(subject);
+ contextResolver.setTenantId(null);
+
// When/Then
- StepVerifier.create(contextResolver.extractUUID(exchange, "testAttribute", "testHeader"))
+ StepVerifier.create(contextResolver.resolveContext(exchange))
+ .assertNext(context -> {
+ assertThat(context.getSubject()).isEqualTo(subject);
+ assertThat(context.getTenantId()).isNull();
+ })
.verifyComplete();
}
-
+
@Test
- @DisplayName("Should resolve roles for context")
- void shouldResolveRoles() {
+ @DisplayName("Should default roles to empty when not overridden")
+ void shouldDefaultRolesToEmpty() {
// Given
- UUID partyId = UUID.randomUUID();
- UUID tenantId = UUID.randomUUID();
-
- AppContext context = AppContext.builder()
- .partyId(partyId)
- .tenantId(tenantId)
- .contractId(UUID.randomUUID())
- .productId(UUID.randomUUID())
- .build();
-
+ contextResolver.setSubject("user-123");
+
// When/Then
- StepVerifier.create(contextResolver.resolveRoles(context, exchange))
- .assertNext(roles -> assertThat(roles).isEmpty())
+ StepVerifier.create(contextResolver.resolveContext(exchange))
+ .assertNext(context -> assertThat(context.getRoles()).isEmpty())
.verifyComplete();
}
-
+
@Test
- @DisplayName("Should resolve permissions for context")
- void shouldResolvePermissions() {
+ @DisplayName("Should default permissions to empty when not overridden")
+ void shouldDefaultPermissionsToEmpty() {
// Given
- UUID partyId = UUID.randomUUID();
- UUID tenantId = UUID.randomUUID();
-
- AppContext context = AppContext.builder()
- .partyId(partyId)
- .tenantId(tenantId)
- .contractId(UUID.randomUUID())
- .productId(UUID.randomUUID())
- .build();
-
+ contextResolver.setSubject("user-123");
+
// When/Then
- StepVerifier.create(contextResolver.resolvePermissions(context, exchange))
- .assertNext(permissions -> assertThat(permissions).isEmpty())
+ StepVerifier.create(contextResolver.resolveContext(exchange))
+ .assertNext(context -> assertThat(context.getPermissions()).isEmpty())
.verifyComplete();
}
-
+
@Test
- @DisplayName("Should enrich context with roles and permissions")
- void shouldEnrichContext() {
+ @DisplayName("Should enrich context with resolved roles and permissions")
+ void shouldEnrichContextWithRolesAndPermissions() {
// Given
- UUID partyId = UUID.randomUUID();
- UUID tenantId = UUID.randomUUID();
-
- AppContext basicContext = AppContext.builder()
- .partyId(partyId)
- .tenantId(tenantId)
- .contractId(UUID.randomUUID())
- .productId(UUID.randomUUID())
- .build();
-
Set roles = Set.of("ROLE_ADMIN", "ROLE_USER");
Set permissions = Set.of("READ", "WRITE");
-
+
TestContextResolver enrichedResolver = new TestContextResolver();
+ enrichedResolver.setSubject("user-123");
enrichedResolver.setRoles(roles);
enrichedResolver.setPermissions(permissions);
-
+
// When/Then
- StepVerifier.create(enrichedResolver.enrichContext(basicContext, exchange))
+ StepVerifier.create(enrichedResolver.resolveContext(exchange))
.assertNext(context -> {
assertThat(context.getRoles()).containsExactlyInAnyOrderElementsOf(roles);
assertThat(context.getPermissions()).containsExactlyInAnyOrderElementsOf(permissions);
})
.verifyComplete();
}
-
+
@Test
- @DisplayName("Should return empty when extracting UUID from path fails")
- void shouldReturnEmptyWhenExtractingFromPathFails() {
- // When/Then
- StepVerifier.create(contextResolver.extractUUIDFromPath(exchange, "variableName"))
- .verifyComplete();
+ @DisplayName("Should support by default and have zero priority")
+ void shouldSupportByDefault() {
+ assertThat(contextResolver.supports(exchange)).isTrue();
+ assertThat(contextResolver.getPriority()).isZero();
}
-
+
/**
* Test implementation of AbstractContextResolver.
*/
private static class TestContextResolver extends AbstractContextResolver {
-
- private UUID partyId = UUID.randomUUID();
+
+ private String subject = "subject-" + UUID.randomUUID();
private UUID tenantId = UUID.randomUUID();
- private UUID contractId;
- private UUID productId;
private Set roles = Set.of();
private Set permissions = Set.of();
-
- public void setPartyId(UUID partyId) {
- this.partyId = partyId;
+
+ public void setSubject(String subject) {
+ this.subject = subject;
}
-
+
public void setTenantId(UUID tenantId) {
this.tenantId = tenantId;
}
-
- public void setContractId(UUID contractId) {
- this.contractId = contractId;
- }
-
- public void setProductId(UUID productId) {
- this.productId = productId;
- }
-
+
public void setRoles(Set roles) {
this.roles = roles;
}
-
+
public void setPermissions(Set permissions) {
this.permissions = permissions;
}
-
+
@Override
- public Mono resolvePartyId(ServerWebExchange exchange) {
- return Mono.just(partyId);
+ public Mono resolveSubject(ServerWebExchange exchange) {
+ return Mono.just(subject);
}
-
+
@Override
public Mono resolveTenantId(ServerWebExchange exchange) {
- return Mono.just(tenantId);
- }
-
- @Override
- public Mono resolveContractId(ServerWebExchange exchange) {
- return contractId != null ? Mono.just(contractId) : Mono.empty();
+ return tenantId != null ? Mono.just(tenantId) : Mono.empty();
}
-
- @Override
- public Mono resolveProductId(ServerWebExchange exchange) {
- return productId != null ? Mono.just(productId) : Mono.empty();
- }
-
+
@Override
- protected Mono> resolveRoles(AppContext context, ServerWebExchange exchange) {
+ protected Mono> resolveRoles(String subject, ServerWebExchange exchange) {
return Mono.just(roles);
}
-
+
@Override
- protected Mono> resolvePermissions(AppContext context, ServerWebExchange exchange) {
+ protected Mono> resolvePermissions(String subject, ServerWebExchange exchange) {
return Mono.just(permissions);
}
}
diff --git a/src/test/java/org/fireflyframework/common/application/security/AbstractSecurityAuthorizationServiceTest.java b/src/test/java/org/fireflyframework/common/application/security/AbstractSecurityAuthorizationServiceTest.java
index c31b58f..1ee20e0 100644
--- a/src/test/java/org/fireflyframework/common/application/security/AbstractSecurityAuthorizationServiceTest.java
+++ b/src/test/java/org/fireflyframework/common/application/security/AbstractSecurityAuthorizationServiceTest.java
@@ -30,26 +30,27 @@
/**
* Unit tests for {@link AbstractSecurityAuthorizationService}.
+ *
+ * Authorization is evaluated solely from the {@link AppContext} roles and permissions that were
+ * already resolved for the request.
*/
@DisplayName("AbstractSecurityAuthorizationService Tests")
class AbstractSecurityAuthorizationServiceTest {
-
+
private TestSecurityAuthorizationService authorizationService;
private AppContext context;
-
+
@BeforeEach
void setUp() {
authorizationService = new TestSecurityAuthorizationService();
context = AppContext.builder()
- .partyId(UUID.randomUUID())
+ .subject("user-123")
.tenantId(UUID.randomUUID())
- .contractId(UUID.randomUUID())
- .productId(UUID.randomUUID())
.roles(Set.of("ROLE_USER", "ROLE_ADMIN"))
.permissions(Set.of("READ", "WRITE"))
.build();
}
-
+
@Test
@DisplayName("Should authorize anonymous access when allowed")
void shouldAuthorizeAnonymousAccess() {
@@ -59,7 +60,7 @@ void shouldAuthorizeAnonymousAccess() {
.httpMethod("GET")
.allowAnonymous(true)
.build();
-
+
// When/Then
StepVerifier.create(authorizationService.authorize(context, securityContext))
.assertNext(result -> {
@@ -68,7 +69,7 @@ void shouldAuthorizeAnonymousAccess() {
})
.verifyComplete();
}
-
+
@Test
@DisplayName("Should authorize when required role is present")
void shouldAuthorizeWhenRolePresent() {
@@ -78,7 +79,7 @@ void shouldAuthorizeWhenRolePresent() {
.httpMethod("GET")
.requiredRoles(Set.of("ROLE_ADMIN"))
.build();
-
+
// When/Then
StepVerifier.create(authorizationService.authorize(context, securityContext))
.assertNext(result -> {
@@ -87,7 +88,7 @@ void shouldAuthorizeWhenRolePresent() {
})
.verifyComplete();
}
-
+
@Test
@DisplayName("Should deny when required role is missing")
void shouldDenyWhenRoleMissing() {
@@ -97,7 +98,7 @@ void shouldDenyWhenRoleMissing() {
.httpMethod("GET")
.requiredRoles(Set.of("ROLE_SUPERADMIN"))
.build();
-
+
// When/Then
StepVerifier.create(authorizationService.authorize(context, securityContext))
.assertNext(result -> {
@@ -106,7 +107,7 @@ void shouldDenyWhenRoleMissing() {
})
.verifyComplete();
}
-
+
@Test
@DisplayName("Should authorize when required permission is granted")
void shouldAuthorizeWhenPermissionGranted() {
@@ -116,7 +117,7 @@ void shouldAuthorizeWhenPermissionGranted() {
.httpMethod("GET")
.requiredPermissions(Set.of("READ"))
.build();
-
+
// When/Then
StepVerifier.create(authorizationService.authorize(context, securityContext))
.assertNext(result -> {
@@ -125,7 +126,7 @@ void shouldAuthorizeWhenPermissionGranted() {
})
.verifyComplete();
}
-
+
@Test
@DisplayName("Should deny when required permission is missing")
void shouldDenyWhenPermissionMissing() {
@@ -135,7 +136,7 @@ void shouldDenyWhenPermissionMissing() {
.httpMethod("DELETE")
.requiredPermissions(Set.of("DELETE"))
.build();
-
+
// When/Then
StepVerifier.create(authorizationService.authorize(context, securityContext))
.assertNext(result -> {
@@ -144,7 +145,7 @@ void shouldDenyWhenPermissionMissing() {
})
.verifyComplete();
}
-
+
@Test
@DisplayName("Should check both roles and permissions when required")
void shouldCheckBothRolesAndPermissions() {
@@ -155,13 +156,13 @@ void shouldCheckBothRolesAndPermissions() {
.requiredRoles(Set.of("ROLE_ADMIN"))
.requiredPermissions(Set.of("WRITE"))
.build();
-
+
// When/Then
StepVerifier.create(authorizationService.authorize(context, securityContext))
.assertNext(result -> assertThat(result.isAuthorized()).isTrue())
.verifyComplete();
}
-
+
@Test
@DisplayName("Should deny when role is present but permission is missing")
void shouldDenyWhenRolePresentButPermissionMissing() {
@@ -172,7 +173,7 @@ void shouldDenyWhenRolePresentButPermissionMissing() {
.requiredRoles(Set.of("ROLE_ADMIN"))
.requiredPermissions(Set.of("DELETE"))
.build();
-
+
// When/Then
StepVerifier.create(authorizationService.authorize(context, securityContext))
.assertNext(result -> {
@@ -181,33 +182,33 @@ void shouldDenyWhenRolePresentButPermissionMissing() {
})
.verifyComplete();
}
-
+
@Test
- @DisplayName("Should check if party has specific role")
+ @DisplayName("Should check if subject has specific role")
void shouldCheckHasRole() {
// When/Then
StepVerifier.create(authorizationService.hasRole(context, "ROLE_ADMIN"))
.expectNext(true)
.verifyComplete();
-
+
StepVerifier.create(authorizationService.hasRole(context, "ROLE_SUPERADMIN"))
.expectNext(false)
.verifyComplete();
}
-
+
@Test
- @DisplayName("Should check if party has specific permission")
+ @DisplayName("Should check if subject has specific permission")
void shouldCheckHasPermission() {
// When/Then
StepVerifier.create(authorizationService.hasPermission(context, "READ"))
.expectNext(true)
.verifyComplete();
-
+
StepVerifier.create(authorizationService.hasPermission(context, "DELETE"))
.expectNext(false)
.verifyComplete();
}
-
+
@Test
@DisplayName("Should allow access when no specific requirements")
void shouldAllowAccessWithNoRequirements() {
@@ -216,13 +217,13 @@ void shouldAllowAccessWithNoRequirements() {
.endpoint("/open")
.httpMethod("GET")
.build();
-
+
// When/Then
StepVerifier.create(authorizationService.authorize(context, securityContext))
.assertNext(result -> assertThat(result.isAuthorized()).isTrue())
.verifyComplete();
}
-
+
@Test
@DisplayName("Should evaluate expression and return false by default")
void shouldEvaluateExpression() {
@@ -231,27 +232,7 @@ void shouldEvaluateExpression() {
.expectNext(false)
.verifyComplete();
}
-
- @Test
- @DisplayName("Should deny with SecurityCenter when config source is SECURITY_CENTER")
- void shouldDenyWithSecurityCenter() {
- // Given
- AppSecurityContext securityContext = AppSecurityContext.builder()
- .endpoint("/protected")
- .httpMethod("GET")
- .configSource(AppSecurityContext.SecurityConfigSource.SECURITY_CENTER)
- .build();
-
- // When/Then
- StepVerifier.create(authorizationService.authorize(context, securityContext))
- .assertNext(result -> {
- assertThat(result.isAuthorized()).isFalse();
- assertThat(result.getAuthorizationFailureReason())
- .isEqualTo("SecurityCenter integration not implemented");
- })
- .verifyComplete();
- }
-
+
/**
* Test implementation of AbstractSecurityAuthorizationService.
*/
diff --git a/src/test/java/org/fireflyframework/common/application/service/AbstractApplicationServiceTest.java b/src/test/java/org/fireflyframework/common/application/service/AbstractApplicationServiceTest.java
index d20ee75..146ca25 100644
--- a/src/test/java/org/fireflyframework/common/application/service/AbstractApplicationServiceTest.java
+++ b/src/test/java/org/fireflyframework/common/application/service/AbstractApplicationServiceTest.java
@@ -37,16 +37,19 @@
/**
* Unit tests for {@link AbstractApplicationService}.
+ *
+ * Tests the product-agnostic application-service helpers: context resolution, role/permission
+ * authorization (delegated to {@link SecurityAuthorizationService}) and tenant config access.
*/
@DisplayName("AbstractApplicationService Tests")
class AbstractApplicationServiceTest {
-
+
private ContextResolver contextResolver;
private ConfigResolver configResolver;
private SecurityAuthorizationService authorizationService;
private TestApplicationService applicationService;
private ServerWebExchange exchange;
-
+
@BeforeEach
void setUp() {
contextResolver = mock(ContextResolver.class);
@@ -55,19 +58,19 @@ void setUp() {
applicationService = new TestApplicationService(contextResolver, configResolver, authorizationService);
exchange = mock(ServerWebExchange.class);
}
-
+
@Test
@DisplayName("Should resolve execution context successfully")
void shouldResolveExecutionContext() {
// Given
UUID tenantId = UUID.randomUUID();
AppContext appContext = AppContext.builder()
- .partyId(UUID.randomUUID())
+ .subject("user-" + UUID.randomUUID())
.tenantId(tenantId)
- .contractId(UUID.randomUUID())
- .productId(UUID.randomUUID())
+ .roles(Set.of("ROLE_USER"))
+ .permissions(Set.of("READ"))
.build();
-
+
AppConfig appConfig = AppConfig.builder()
.tenantId(tenantId)
.active(true)
@@ -75,85 +78,21 @@ void shouldResolveExecutionContext() {
.featureFlags(new HashMap<>())
.settings(new HashMap<>())
.build();
-
+
when(contextResolver.resolveContext(exchange)).thenReturn(Mono.just(appContext));
when(configResolver.resolveConfig(tenantId)).thenReturn(Mono.just(appConfig));
-
+
// When/Then
StepVerifier.create(applicationService.resolveExecutionContext(exchange))
.assertNext(executionContext -> {
assertThat(executionContext.getContext()).isEqualTo(appContext);
assertThat(executionContext.getConfig()).isEqualTo(appConfig);
+ assertThat(executionContext.getContext().getSubject()).isEqualTo(appContext.getSubject());
+ assertThat(executionContext.getContext().getTenantId()).isEqualTo(tenantId);
})
.verifyComplete();
}
-
- @Test
- @DisplayName("Should validate context successfully when requirements are met")
- void shouldValidateContextSuccessfully() {
- // Given
- ApplicationExecutionContext context = createExecutionContext();
-
- // When/Then
- StepVerifier.create(applicationService.validateContext(context, true, true))
- .expectNext(context)
- .verifyComplete();
- }
-
- @Test
- @DisplayName("Should fail validation when contract is required but missing")
- void shouldFailValidationWhenContractMissing() {
- // Given
- ApplicationExecutionContext context = ApplicationExecutionContext.builder()
- .context(AppContext.builder()
- .partyId(UUID.randomUUID())
- .tenantId(UUID.randomUUID())
- .productId(UUID.randomUUID())
- .build())
- .config(AppConfig.builder()
- .tenantId(UUID.randomUUID())
- .active(true)
- .providers(new HashMap<>())
- .featureFlags(new HashMap<>())
- .settings(new HashMap<>())
- .build())
- .build();
-
- // When/Then
- StepVerifier.create(applicationService.validateContext(context, true, false))
- .expectErrorMatches(error ->
- error instanceof IllegalStateException &&
- error.getMessage().contains("Contract ID is required"))
- .verify();
- }
-
- @Test
- @DisplayName("Should fail validation when product is required but missing")
- void shouldFailValidationWhenProductMissing() {
- // Given
- ApplicationExecutionContext context = ApplicationExecutionContext.builder()
- .context(AppContext.builder()
- .partyId(UUID.randomUUID())
- .tenantId(UUID.randomUUID())
- .contractId(UUID.randomUUID())
- .build())
- .config(AppConfig.builder()
- .tenantId(UUID.randomUUID())
- .active(true)
- .providers(new HashMap<>())
- .featureFlags(new HashMap<>())
- .settings(new HashMap<>())
- .build())
- .build();
-
- // When/Then
- StepVerifier.create(applicationService.validateContext(context, false, true))
- .expectErrorMatches(error ->
- error instanceof IllegalStateException &&
- error.getMessage().contains("Product ID is required"))
- .verify();
- }
-
+
@Test
@DisplayName("Should require role successfully when present")
void shouldRequireRoleSuccessfully() {
@@ -161,12 +100,12 @@ void shouldRequireRoleSuccessfully() {
ApplicationExecutionContext context = createExecutionContext();
when(authorizationService.hasRole(context.getContext(), "ROLE_ADMIN"))
.thenReturn(Mono.just(true));
-
+
// When/Then
StepVerifier.create(applicationService.requireRole(context, "ROLE_ADMIN"))
.verifyComplete();
}
-
+
@Test
@DisplayName("Should fail when required role is missing")
void shouldFailWhenRequiredRoleMissing() {
@@ -174,13 +113,13 @@ void shouldFailWhenRequiredRoleMissing() {
ApplicationExecutionContext context = createExecutionContext();
when(authorizationService.hasRole(context.getContext(), "ROLE_SUPERADMIN"))
.thenReturn(Mono.just(false));
-
+
// When/Then
StepVerifier.create(applicationService.requireRole(context, "ROLE_SUPERADMIN"))
.expectError(AccessDeniedException.class)
.verify();
}
-
+
@Test
@DisplayName("Should require permission successfully when granted")
void shouldRequirePermissionSuccessfully() {
@@ -188,12 +127,12 @@ void shouldRequirePermissionSuccessfully() {
ApplicationExecutionContext context = createExecutionContext();
when(authorizationService.hasPermission(context.getContext(), "WRITE"))
.thenReturn(Mono.just(true));
-
+
// When/Then
StepVerifier.create(applicationService.requirePermission(context, "WRITE"))
.verifyComplete();
}
-
+
@Test
@DisplayName("Should fail when required permission is missing")
void shouldFailWhenRequiredPermissionMissing() {
@@ -201,19 +140,19 @@ void shouldFailWhenRequiredPermissionMissing() {
ApplicationExecutionContext context = createExecutionContext();
when(authorizationService.hasPermission(context.getContext(), "DELETE"))
.thenReturn(Mono.just(false));
-
+
// When/Then
StepVerifier.create(applicationService.requirePermission(context, "DELETE"))
.expectError(AccessDeniedException.class)
.verify();
}
-
+
@Test
@DisplayName("Should get provider config successfully")
void shouldGetProviderConfig() {
// Given
ApplicationExecutionContext context = createExecutionContextWithProvider();
-
+
// When/Then
StepVerifier.create(applicationService.getProviderConfig(context, "payment"))
.assertNext(providerConfig -> {
@@ -222,44 +161,44 @@ void shouldGetProviderConfig() {
})
.verifyComplete();
}
-
+
@Test
@DisplayName("Should fail when provider is not configured")
void shouldFailWhenProviderNotConfigured() {
// Given
ApplicationExecutionContext context = createExecutionContext();
-
+
// When/Then
StepVerifier.create(applicationService.getProviderConfig(context, "nonexistent"))
- .expectErrorMatches(error ->
+ .expectErrorMatches(error ->
error instanceof IllegalStateException &&
error.getMessage().contains("Provider not configured"))
.verify();
}
-
+
@Test
@DisplayName("Should check if feature is enabled")
void shouldCheckIfFeatureIsEnabled() {
// Given
ApplicationExecutionContext context = createExecutionContextWithFeature();
-
+
// When/Then
StepVerifier.create(applicationService.isFeatureEnabled(context, "new-ui"))
.expectNext(true)
.verifyComplete();
-
+
StepVerifier.create(applicationService.isFeatureEnabled(context, "old-feature"))
.expectNext(false)
.verifyComplete();
}
-
+
private ApplicationExecutionContext createExecutionContext() {
return ApplicationExecutionContext.builder()
.context(AppContext.builder()
- .partyId(UUID.randomUUID())
+ .subject("user-" + UUID.randomUUID())
.tenantId(UUID.randomUUID())
- .contractId(UUID.randomUUID())
- .productId(UUID.randomUUID())
+ .roles(Set.of("ROLE_ADMIN"))
+ .permissions(Set.of("WRITE"))
.build())
.config(AppConfig.builder()
.tenantId(UUID.randomUUID())
@@ -270,7 +209,7 @@ private ApplicationExecutionContext createExecutionContext() {
.build())
.build();
}
-
+
private ApplicationExecutionContext createExecutionContextWithProvider() {
Map providers = new HashMap<>();
providers.put("payment", AppConfig.ProviderConfig.builder()
@@ -279,13 +218,11 @@ private ApplicationExecutionContext createExecutionContextWithProvider() {
.enabled(true)
.properties(new HashMap<>())
.build());
-
+
return ApplicationExecutionContext.builder()
.context(AppContext.builder()
- .partyId(UUID.randomUUID())
+ .subject("user-" + UUID.randomUUID())
.tenantId(UUID.randomUUID())
- .contractId(UUID.randomUUID())
- .productId(UUID.randomUUID())
.build())
.config(AppConfig.builder()
.tenantId(UUID.randomUUID())
@@ -296,17 +233,15 @@ private ApplicationExecutionContext createExecutionContextWithProvider() {
.build())
.build();
}
-
+
private ApplicationExecutionContext createExecutionContextWithFeature() {
Map featureFlags = new HashMap<>();
featureFlags.put("new-ui", true);
-
+
return ApplicationExecutionContext.builder()
.context(AppContext.builder()
- .partyId(UUID.randomUUID())
+ .subject("user-" + UUID.randomUUID())
.tenantId(UUID.randomUUID())
- .contractId(UUID.randomUUID())
- .productId(UUID.randomUUID())
.build())
.config(AppConfig.builder()
.tenantId(UUID.randomUUID())
@@ -317,12 +252,12 @@ private ApplicationExecutionContext createExecutionContextWithFeature() {
.build())
.build();
}
-
+
/**
- * Test implementation of AbstractApplicationService.
+ * Test implementation of {@link AbstractApplicationService}.
*/
private static class TestApplicationService extends AbstractApplicationService {
-
+
protected TestApplicationService(ContextResolver contextResolver,
ConfigResolver configResolver,
SecurityAuthorizationService authorizationService) {