Dynamic URL Authorization
Database-driven URL authorization using CustomDynamicAuthorizationManager. Define access policies through the current Policy Center and related admin policy flows, then have them enforced without rebuilding the application.
What is Dynamic Authorization?
Traditional Spring Security uses static URL matchers defined in Java code. Any change to authorization rules requires a code change, rebuild, and redeployment. Contexa's CustomDynamicAuthorizationManager takes a fundamentally different approach: authorization policies are loaded from the database at startup and evaluated at runtime.
This architecture enables three key capabilities:
- No-redeploy policy changes —Authorization rules can be modified, added, or removed without rebuilding or redeploying the application. After editing, a hot-reload triggers re-initialization.
- Admin policy management —Policies can be created and managed through Policy Center, policy detail flows, and related admin endpoints without changing Java security configuration.
- AI-generated policy integration —AI-generated and AI-evolved policies can be integrated into the authorization pipeline after human approval.
How It Works
CustomDynamicAuthorizationManager implements Spring Security's AuthorizationManager<RequestAuthorizationContext> interface. It bridges the gap between database-stored policies and Spring Security's runtime evaluation engine.
Policy Evaluation Flow
PENDING or REJECTED approval are filtered out before request-time evaluationStep-by-Step Evaluation
- Context refresh triggers
CustomDynamicAuthorizationManager.initialize(), which callsPolicyRetrievalPoint.findUrlPolicies(). - The manager filters out inactive policies and policies whose
approvalStatusisPENDINGorREJECTED. - For each remaining URL policy,
PolicyExpressionConverter.toExpression(policy)builds the SpEL expression string andExpressionAuthorizationManagerResolver.resolve(expression)creates the Spring authorization manager. - The resulting
RequestMatcherEntry<AuthorizationManager<RequestAuthorizationContext>>list is cached in memory. - At request time,
check(...)iterates the cached matchers until it finds the first matching request pattern. - The matched authorization manager evaluates the current
Authenticationand request context. If no URL policy matches, the default decision isAuthorizationDecision(true). - Denied URL authorization results are recorded through
CentralAuditFacadewith request URI, action, outcome, and optional AI assessment context before the decision is returned to Spring Security.
Static + Dynamic Integration
Contexa's dynamic authorization coexists with standard Spring Security static rules. Static rules are evaluated first by the filter chain; only requests that reach .anyRequest() are handled by the dynamic authorization manager.
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(auth -> auth
// Static rules: evaluated first, bypasses dynamic authorization
.requestMatchers("/", "/home", "/css/**", "/js/**").permitAll()
.requestMatchers("/login", "/register").permitAll()
.requestMatchers("/user/list/**").hasRole("USER")
// Dynamic authorization: all remaining requests
// Policies loaded from database, hot-reloadable at runtime
.anyRequest().access(customDynamicAuthorizationManager)
);
return http.build();
}
Evaluation Flow
Important Behaviors
- Order matters —Static
requestMatchersmust be declared before.anyRequest(). Spring Security evaluates them top-to-bottom and uses the first match. - Short-circuit evaluation —Once a static matcher matches, the dynamic manager is never invoked. This means
permitAll()routes cannot be restricted by database policies. - No overlap recommended —Avoid defining database policies for URLs that are already statically matched, as the static rule will always take precedence.
- Audit implications —Statically permitted requests do not pass through the XACML engine and will not appear in the authorization audit trail.
requestMatchers().permitAll(), any database policy targeting that URL will be silently ignored. The static matcher always wins.
Default Decision for Unmatched Requests
When a request URL does not match any policy in the database, CustomDynamicAuthorizationManager returns a default authorization decision. By default, unmatched requests are allowed (AuthorizationDecision(true)), which means only explicitly defined DENY policies block access.
Configuration Patterns
The authorizeHttpRequests block inside the global customizer determines how static matchers interact with the dynamic authorization engine. Choose the pattern that matches your security posture.
Pattern 1: Public Assets + Full Dynamic
The most common pattern. Static assets bypass authorization entirely; everything else is evaluated by the XACML policy engine.
http.authorizeHttpRequests(authReq -> authReq
.requestMatchers("/css/**", "/js/**", "/images/**", "/favicon.ico").permitAll()
.anyRequest().access(customDynamicAuthorizationManager)
);
Pattern 2: Admin Fixed + Rest Dynamic
A hybrid approach where certain well-known routes are statically permitted alongside asset paths, while the remaining routes go through dynamic evaluation. Useful when you want guaranteed access to specific pages regardless of policy state.
http.authorizeHttpRequests(authReq -> authReq
.requestMatchers("/css/**", "/js/**").permitAll()
.requestMatchers("/home", "/user/list/**").permitAll()
.anyRequest().access(customDynamicAuthorizationManager)
);
Pattern 3: Full Dynamic
Pattern for routing all unmatched requests through customDynamicAuthorizationManager. In this setup, any route not statically permitted earlier in the chain is evaluated against the runtime policy mappings.
http.authorizeHttpRequests(authReq -> authReq
.anyRequest().access(customDynamicAuthorizationManager)
);
Pattern Comparison
| Criteria | Pattern 1: Public Assets + Dynamic | Pattern 2: Hybrid | Pattern 3: Full Dynamic |
|---|---|---|---|
| Static asset performance | Best —no policy lookup | Good —no policy lookup for assets | Slowest —all requests evaluated |
| Policy flexibility | High for non-asset routes | Medium —some routes are hardcoded | Maximum —everything is policy-driven |
| Operational safety | Safe —assets always load | Safe —critical pages always reachable | Requires careful policy setup |
| Best for | Most applications | Admin panels with guaranteed access | Enterprise with full policy governance |
| Misconfiguration risk | Low | Low | High —missing policies block everything |
Core Implementation
The manager initializes on Spring context refresh and supports hot-reload for runtime policy updates.
public class CustomDynamicAuthorizationManager
implements AuthorizationManager<RequestAuthorizationContext> {
private final PolicyRetrievalPoint policyRetrievalPoint;
private final ExpressionAuthorizationManagerResolver managerResolver;
private final ContextHandler contextHandler;
private final ZeroTrustEventPublisher zeroTrustEventPublisher;
private final AuthorizationMetrics metricsCollector;
// Initializes on application context refresh
@EventListener
public void onApplicationEvent(ContextRefreshedEvent event) {
initialize();
}
@Override
public AuthorizationDecision check(
Supplier<Authentication> authenticationSupplier,
RequestAuthorizationContext context) {
// 1. Match request URL to cached RequestMatcherEntry
// 2. Resolve the SpEL expression for the matched policy
// 3. Evaluate the expression against the Authentication
// 4. Record denied results through CentralAuditFacade
// 5. Return AuthorizationDecision(granted/denied)
}
// Hot-reload: clears PRP cache and re-initializes mappings
public synchronized void reload() {
policyRetrievalPoint.clearUrlPoliciesCache();
policyRetrievalPoint.clearMethodPoliciesCache();
initialize();
}
}
Policy Model
Each Policy entity stored in the database contains the full authorization definition including targets, rules, and metadata for AI-generated policies.
public class PolicyDto {
Long id;
String name;
String description;
Policy.Effect effect;
int priority;
List targets;
List rules;
Policy.PolicySource source;
Policy.ApprovalStatus approvalStatus;
Boolean isActive;
Double confidenceScore;
String aiModel;
}
Policy Field Reference
| Field | Type | Description |
|---|---|---|
name |
String | A human-readable identifier for the policy, displayed in Policy Center, policy details, and audit output. |
description |
String | A human-readable description of what the policy protects and why it exists. |
effect |
Policy.Effect |
The policy effect. ALLOW grants access when conditions are met; DENY blocks access when conditions are met. |
priority |
int | Determines the evaluation order when multiple policies match the same request. Higher priority policies are evaluated first and take precedence. |
targets |
List<TargetDto> | The resource targets this policy applies to, defined as Ant-style URL patterns paired with HTTP methods. |
rules |
List<PolicyRule> | The authorization rules for this policy. Each rule contains one or more SpEL conditions that are evaluated at request time. |
source |
PolicySource | Indicates how the policy was created: MANUAL, AI_GENERATED, AI_EVOLVED, or IMPORTED. |
approvalStatus |
ApprovalStatus | The approval workflow status: NOT_REQUIRED, PENDING, APPROVED, or REJECTED. AI-generated policies must be APPROVED before they are enforced. |
confidenceScore |
Double | The AI model's confidence score for this policy, ranging from 0.0 to 1.0. Null for manually created policies. |
aiModel |
String | The identifier of the AI model that generated this policy. Null for manually created policies. |
PolicyTarget
Each target captures a resource type, target identifier, optional HTTP method, ordering, and source type.
public class TargetDto {
String targetType; // URL or METHOD
String targetIdentifier; // e.g. "/api/admin/**"
String httpMethod; // GET, POST, PUT, DELETE, ...
int targetOrder;
String sourceType; // RESOURCE or MANUAL
}
PolicyRule and Conditions
Each rule contains one or more conditions. Conditions hold the actual SpEL expression strings that are evaluated at request time.
public class PolicyRule {
String name;
List<PolicyCondition> conditions;
}
public class PolicyCondition {
String expression; // SpEL expression, e.g. "hasAnyAuthority('ROLE_ADMIN')"
}
SpEL Expression Support
Contexa extends the standard Spring Security SpEL expressions with custom methods from AbstractAISecurityExpressionRoot for zero-trust AI action checks.
Standard Spring Security Expressions
| Expression | Description |
|---|---|
hasAnyAuthority('ROLE_ADMIN', 'ROLE_USER') |
Grants access if the principal has any of the listed authorities. |
hasAuthority('ROLE_ADMIN') |
Grants access if the principal has the specified authority. |
hasPermission('resource', 'action') |
Delegates evaluation to the PermissionEvaluator chain. This expression is automatically stripped from URL-level policies since it is only relevant for method-level evaluation. |
permitAll |
Always grants access regardless of authentication state or conditions. |
denyAll |
Always denies access regardless of authentication state or conditions. |
isAuthenticated() |
Grants access if the user is authenticated and not an anonymous user. |
Zero-Trust AI Expressions
These expressions are provided by AbstractAISecurityExpressionRoot and integrate with Contexa's zero-trust AI evaluation pipeline.
| Expression | Description |
|---|---|
isAllowed() |
Returns true if the zero-trust AI action for the current request is ALLOW. |
#ai.isBlocked() |
Returns true if the zero-trust AI action for the current request is BLOCK. |
needsChallenge() |
Returns true if the AI evaluation determined that a step-up MFA challenge is required before granting access. |
#ai.needsEscalation() |
Returns true if the AI evaluation determined that human review or security team escalation is needed. |
#ai.isPendingAnalysis() |
Returns true if asynchronous AI analysis is still in progress for the current request. |
#ai.hasAction('ALLOW') |
Checks whether the zero-trust action matches a specific action by name. |
Policy-to-Expression Conversion Rules
The getExpressionFromPolicy() method in CustomDynamicAuthorizationManager applies the following conversion logic:
| Condition | Resulting Expression |
|---|---|
| No conditions + ALLOW effect | permitAll |
| No conditions + DENY effect | denyAll |
| Single condition | Uses the expression directly |
| Multiple simple authority conditions | hasAnyAuthority('A', 'B', 'C') |
| Multiple mixed conditions | Joined with or operator |
| DENY effect | Wraps the expression in !(expression) |
hasPermission() in URL policy |
Stripped (only relevant for method-level evaluation) |
XACML Integration
Dynamic authorization is powered by the XACML engine (eXtensible Access Control Markup Language). The five XACML components work together to manage the complete policy lifecycle. See the XACML Engine page for full details.
| Component | Full Name | Role in Dynamic Authorization |
|---|---|---|
| PAP | Policy Administration Point | Creates, reads, updates, and deletes policies through Policy Center, policy detail flows, and related admin services. |
| PDP | Policy Decision Point | Evaluates policy conditions as SpEL expressions against the security context and returns a grant or deny decision. |
| PEP | Policy Enforcement Point | Implemented by CustomDynamicAuthorizationManager. Enforces PDP decisions within the Spring Security filter chain. |
| PIP | Policy Information Point | Provides contextual attributes such as user roles, permissions, and request metadata for use in SpEL expression evaluation. |
| PRP | Policy Retrieval Point | Loads policies from the database at application startup and refreshes them on hot-reload events. |
Usage Example
The following example shows the complete lifecycle of creating and enforcing a dynamic authorization policy.
Step 1: Create a Policy
Policies can be created through Policy Center and related server-side admin flows. This OSS page does not describe a separate public PAP REST surface.
GET /admin/policy-center
POST /admin/policy-center/create-policy
POST /admin/policy-center/api/quick-create
POST /admin/api/policies/build-from-business-rule
// Current OSS authoring paths:
// - Policy Center form workflow
// - Quick-create helper inside Policy Center
// - Business-rule helper API for assisted policy generation
Step 2: Policy is Stored in Database
The PAP persists the policy as JPA entities: Policy, PolicyTarget, PolicyRule, and PolicyCondition. For manually created policies, source is set to MANUAL and approvalStatus to NOT_REQUIRED.
Step 3: Manager Picks Up the Policy
On the next hot-reload (or application restart), CustomDynamicAuthorizationManager loads the new policy from the PRP and builds a RequestMatcherEntry mapping:
URL Pattern: /api/admin/**
HTTP Methods: GET, POST, PUT, DELETE
SpEL Expression: hasAuthority('ROLE_ADMIN')
Source: MANUAL
Approval: NOT_REQUIRED (active immediately)
Step 4: Requests are Automatically Secured
When a request arrives at /api/admin/users:
1. Request: GET /api/admin/users
2. CustomDynamicAuthorizationManager.check() is invoked
3. URL matches pattern /api/admin/** (Ant path matching)
4. SpEL expression "hasAuthority('ROLE_ADMIN')" is evaluated
5a. User has ROLE_ADMIN -> AuthorizationDecision(granted=true)
5b. User lacks ROLE_ADMIN -> AuthorizationDecision(granted=false) -> 403
6. If the result is denied, CustomDynamicAuthorizationManager records an audit event with request URI, action, outcome, and available AI context
AI-Generated Policy Example
When an AI model generates a policy, it enters the system with PENDING approval status and is not enforced until a human reviewer approves it.
{
"name": "AI: Restrict Sensitive Reports",
"description": "AI-detected sensitive data endpoint requiring elevated access",
"effect": "ALLOW",
"priority": 90,
"targets": [
{
"targetType": "URL",
"targetIdentifier": "/api/reports/sensitive/**",
"httpMethod": "GET",
"targetOrder": 0,
"sourceType": "RESOURCE",
}
],
"rules": [
{
"name": "Require manager role and AI clearance",
"conditions": [
{ "expression": "hasAuthority('ROLE_MANAGER')" },
{ "expression": "isAllowed()" }
]
}
],
"source": "AI_GENERATED",
"approvalStatus": "PENDING",
"confidenceScore": 0.87,
"aiModel": "contexa-policy-gen-v2"
}
This policy will be skipped during authorization evaluation until an administrator approves it through the current admin policy review flow.
Configuration
Dynamic authorization is enabled automatically when the contexa-iam module is on the classpath. Runtime policy changes are typically made through Policy Center and related admin policy views, while code-level behavior is defined by the IAM and security configuration classes.
Hot-Reload
After modifying policies through Policy Center or related admin flows, the server calls authorizationManager.reload() to rebuild URL and method policy caches without restarting the application:
policyService.createPolicy(policyDto);
customDynamicAuthorizationManager.reload();
This calls CustomDynamicAuthorizationManager.reload(), which clears the PRP cache and re-initializes all RequestMatcherEntry mappings from the database.
Related Configuration
For IAM property configuration, see the Configuration reference. Key properties include:
| Property | Default | Description |
|---|---|---|
contexa.iam.admin.rest-docs-path |
/docs/index.html |
Base documentation path used when MVC-scanned resources generate their API documentation anchors. |
contexa.policy.combining-algorithm |
FIRST_APPLICABLE |
Combining algorithm used when multiple URL policies match the same request. |
security.zerotrust.mode |
ENFORCE |
Controls whether zero-trust decisions are enforced live or observed in shadow mode. |
Zero Trust Properties
| Property | Default | Description |
|---|---|---|
security.zerotrust.enabled | true | Enable or disable the zero-trust authorization framework globally |
security.zerotrust.mode | ENFORCE | Authorization mode: SHADOW (audit only) or ENFORCE (live enforcement) |
security.zerotrust.sampling.rate | 1.0 | Sampling rate for authorization evaluation (1.0 = evaluate all requests) |
security.zerotrust.hotpath.enabled | true | Enable hot-path optimization for frequently accessed resources |
security.zerotrust.threat.initial | 0.3 | Initial threat score assigned to new sessions |
Threshold Properties
| Property | Default | Description |
|---|---|---|
security.zerotrust.thresholds.skip | 0.3 | Threat score below which authorization checks may be skipped |
security.zerotrust.thresholds.optional | 0.5 | Threat score threshold for optional additional verification |
security.zerotrust.thresholds.required | 0.7 | Threat score threshold requiring mandatory verification |
security.zerotrust.thresholds.strict | 0.9 | Threat score threshold triggering strict security measures |
Cache and Session Properties
| Property | Default | Description |
|---|---|---|
security.zerotrust.cache.ttl-hours | 24 | TTL for cached authorization decisions (hours) |
security.zerotrust.cache.session-ttl-minutes | 30 | Session-specific cache TTL (minutes) |
security.zerotrust.cache.invalidated-ttl-minutes | 60 | TTL for invalidated cache entries before removal (minutes) |
security.zerotrust.session.tracking-enabled | true | Enable AI session tracking for behavioral analysis |
security.zerotrust.redis.timeout | 5 | Redis connection timeout (seconds) |
security.zerotrust.redis.update-interval-seconds | 30 | Interval for pushing session metrics to Redis |
contexa.iam.admin.*, see the Admin Console reference page.
See Also
- Authorization Overview —Full XACML architecture, AI expressions, and Quick Start
- Policy Management —Policy lifecycle, Policy Builder, and condition templates
- Resource Scanner —Automatic resource discovery and the end-to-end workflow
- Permission Evaluators —Method-level permission evaluation with
hasPermission() - Admin Console —Current Policy Center, access center, and related IAM operator views
Default Policy Management
When an incoming request does not match any configured URL policy, CustomDynamicAuthorizationManager.check(...) falls back to new AuthorizationDecision(true). That means unmatched requests are allowed by default in the current OSS runtime.
// Default behavior: unmatched requests are allowed
return new AuthorizationDecision(true);
Moving Toward Default Deny
The OSS codebase does not expose a dedicated YAML property such as default-policy for unmatched URL requests. To move toward deny-by-default, create explicit catch-all DENY policies for protected URL spaces or customize the authorization manager in code.
Creating URL Policies in Policy Center
The current OSS operator flow for URL policies is centered on Policy Center. Follow these steps to create a policy that protects a URL pattern:
- Open Policy Center —Go to /admin/policy-center?tab=create and open the integrated create surface.
- Choose a create path —Use quick-create for simple role/permission grants, or use the manual form to edit the full
PolicyDto. - Set Policy Metadata —Enter a descriptive policy name, select the effect (
ALLOWorDENY), and set the priority (lower numbers evaluate first). - Add URL Target —In the Targets section, add a URL target by specifying:
- URL pattern (e.g.,
/api/admin/**) - HTTP method (
GET,POST,PUT,DELETE, orANY)
- URL pattern (e.g.,
- Add Rule with Condition —Add a rule containing a SpEL condition expression. For example:
SpEL
hasAnyAuthority('ROLE_ADMIN') - Save and Verify —Save the policy through Policy Center or a related admin flow. The server rebuilds authorization mappings through
authorizationManager.reload(). Verify enforcement with the list, simulator, impact analysis, and detail views.
#ai.* expressions, role checks, permission checks, time checks, and IP checks can be combined in the same rule.
Policy Hot-Reload
CustomDynamicAuthorizationManager supports hot-reloading of policies without application restart. This ensures authorization changes take effect immediately.
Automatic Reload
Policies are automatically reloaded when the application context fires a ContextRefreshedEvent. The reload process:
PolicyRetrievalPointfetches the latest policies from the database.CustomDynamicAuthorizationManagerrebuilds theRequestMatcherEntrylist.- New requests are evaluated against the updated policy set.
Manual Reload
Reload can be triggered from server-side admin flows that call authorizationManager.reload(), including Policy Center updates and system settings flows. This page does not document a standalone public reload REST endpoint.
Cache Invalidation
When policies are reloaded, CustomDynamicAuthorizationManager.reload() clears URL and method policy caches through PolicyRetrievalPoint and rebuilds request mappings. Shared infrastructure such as Redis may still participate elsewhere in the platform, but the runtime reload contract here is the authorization manager and PRP cache reset.