Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,20 @@
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

<!-- Firefly security platform: secure-by-default reactive resource server + method security.
The resource server auto-configures only for reactive web apps and can be disabled via
firefly.security.resource-server.enabled=false. -->
<dependency>
<groupId>org.fireflyframework</groupId>
<artifactId>fireflyframework-security-resource-server</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.fireflyframework</groupId>
<artifactId>fireflyframework-security-method-policy</artifactId>
<version>${project.version}</version>
</dependency>

<!-- Spring Configuration Processor -->
<dependency>
<groupId>org.springframework.boot</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -41,25 +40,25 @@
/**
* Aspect for intercepting and processing @Secure annotations.
* Handles security checks before method execution.
*
*
* <p>This aspect intercepts methods annotated with @Secure and performs
* authorization checks using the SecurityAuthorizationService.</p>
*
*
* @author Firefly Development Team
* @since 1.0.0
*/
@Aspect
@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
Expand All @@ -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 -> {
Expand All @@ -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);
Expand All @@ -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(
Expand Down Expand Up @@ -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
Expand All @@ -153,30 +152,30 @@ 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
*/
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
Expand All @@ -188,11 +187,9 @@ private AppSecurityContext buildSecurityContext(Secure secure, ProceedingJoinPoi
Set<String> roles = new HashSet<>(Arrays.asList(secure.roles()));
Set<String> 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)
Expand All @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -59,6 +58,11 @@
* <li>Banner (Firefly Application Layer banner)</li>
* </ul>
*
* <p>The request context is projected from the <strong>validated</strong> 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}.</p>
*
* @author Firefly Development Team
* @since 1.0.0
*/
Expand Down Expand Up @@ -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
* <p>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.</p>
*
* @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<SessionManager<SessionContext>> 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);
}

/**
Expand All @@ -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
* <p>Authorization is evaluated solely from the roles and permissions already resolved into the
* {@link org.fireflyframework.common.application.context.AppContext}.</p>
*
* @return DefaultSecurityAuthorizationService instance
*/
@Bean
@ConditionalOnMissingBean(SecurityAuthorizationService.class)
public DefaultSecurityAuthorizationService defaultSecurityAuthorizationService(
ObjectProvider<SessionManager<SessionContext>> sessionManagerProvider) {
public DefaultSecurityAuthorizationService defaultSecurityAuthorizationService() {
log.info("Creating DefaultSecurityAuthorizationService bean");
return new DefaultSecurityAuthorizationService(sessionManagerProvider.getIfAvailable());
return new DefaultSecurityAuthorizationService();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,42 +22,42 @@

/**
* Configuration properties for the Application Layer.
*
*
* <p>Configure in application.yml:</p>
* <pre>
* firefly:
* application:
* security:
* enabled: true
* use-security-center: true
* use-policy-engine: true
* context:
* cache-enabled: true
* cache-ttl: 300
* </pre>
*
*
* @author Firefly Development Team
* @since 1.0.0
*/
@Data
@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 {
/**
Expand All @@ -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.
Expand All @@ -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
*/
Expand Down
Loading
Loading