@Protectable Annotation
AI-driven method-level authorization that intercepts method invocations, evaluates SpEL-based security rules, and enforces rapid reentry protection with optional synchronous AI decision support.
Overview
The @Protectable annotation marks methods for AI-based authorization enforcement. Defined in contexa-common and processed by contexa-iam, it integrates with the XACML PEP layer to intercept method calls, evaluate security policies through SpEL expressions, and optionally invoke synchronous AI analysis for high-risk operations.
The processing pipeline consists of three key components working in sequence:
- ProtectableRapidReentryGuard —Prevents duplicate requests within a configurable time window
- ProtectableMethodAuthorizationManager —Evaluates SpEL-based security expressions against the current authentication context
- AuthorizationManagerMethodInterceptor —Orchestrates the full authorization flow as a Spring Security
AuthorizationAdvisor
When to Use @Protectable
| Approach | Scope | Policy Source | AI Integration | Best For |
|---|---|---|---|---|
@Protectable |
Method-level | Database (dynamic) | Full (sync, #ai.* expressions) |
Business-critical operations, ownership-aware permission checks, AI-monitored endpoints |
@PreAuthorize |
Method-level | Annotations (static) | None | Simple role/permission checks that rarely change |
| Dynamic URL Auth | URL-level | Database (dynamic) | Full (#ai.* expressions) |
URL-pattern-based access control, API endpoint protection |
Quick Start
Basic Usage
@Service
public class UserService {
@Protectable
public User getUser(Long userId) {
// Protected by method-level policies from the database.
// Policies are created through the IAM admin surfaces, primarily Policy Center and related policy authoring flows.
return userRepository.findById(userId).orElseThrow();
}
}
With Owner Verification
@Service
public class OrderService {
@Protectable(ownerField = "userId")
public Order updateOrder(Order order) {
// Enables owner-aware permission checks when the method policy uses
// hasPermission(...) against the protected Order resource.
return orderRepository.save(order);
}
}
With Synchronous AI Analysis
@Service
public class PaymentService {
@Protectable(sync = true)
public PaymentResult processPayment(PaymentRequest request) {
// The request blocks until the AI Zero Trust analysis completes.
// If the AI returns BLOCK, CHALLENGE, or ESCALATE,
// ZeroTrustAccessDeniedException is thrown before execution.
return paymentGateway.process(request);
}
}
sync = true blocks the request until the synchronous Zero Trust decision path finishes. Use it only for operations that truly need pre-execution enforcement.
Annotation Definition
The annotation is declared in io.contexa.contexacommon.annotation and targets methods at runtime.
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Protectable {
String ownerField() default "";
boolean sync() default false;
}
Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
ownerField |
String |
"" |
Field or getter name used by ownership-aware hasPermission(...) evaluation. The value is resolved from the authorization target object (or a resolved entity for ID-based checks). When empty, ownership-aware permission checks are not applied. |
sync |
boolean |
false |
When true, the Zero Trust evaluation completes synchronously before the method returns —the user's request blocks until LLM analysis finishes, so choose this option carefully. When false (default), evaluation runs asynchronously and the method proceeds immediately. |
AuthorizationManagerMethodInterceptor
The core AOP interceptor that implements MethodInterceptor and AuthorizationAdvisor. It is registered at AuthorizationInterceptorsOrder.FIRST + 1 priority, ensuring it runs before standard Spring Security method interceptors.
Authorization Flow
ALLOW —proceed | BLOCK / CHALLENGE / ESCALATE —ZeroTrustAccessDeniedException
Constructor
public AuthorizationManagerMethodInterceptor(
Pointcut pointcut,
ProtectableMethodAuthorizationManager authorizationManager,
ProtectableRapidReentryGuard rapidReentryGuard)
Optional Dependencies (setter injection)
| Setter | Type | Purpose |
|---|---|---|
setZeroTrustEventPublisher |
ZeroTrustEventPublisher |
Publishes authorization events for audit logging and AI-driven threat analysis. When absent, event publishing is silently skipped. |
setMetricsCollector |
AuthorizationMetrics |
Records invocation duration in nanoseconds and authorization decision counts for monitoring and alerting. When absent, metrics collection is silently skipped. |
setSynchronousProtectableDecisionService |
SynchronousProtectableDecisionService |
Required for sync=true. If absent, sync requests throw ZeroTrustAccessDeniedException. |
Annotation Resolution
The interceptor resolves @Protectable annotations using a multi-level lookup strategy:
- Direct method annotation via
AnnotationUtils.findAnnotation(method) - Most specific method on the target class (handles proxy scenarios via
AopProxyUtils) - Class-level annotation on the target class
- Class-level annotation on the declaring class
ProtectableMethodAuthorizationManager
Evaluates SpEL-based security expressions against the current method invocation context. It relies on Spring Security's MethodSecurityExpressionHandler to create an evaluation context, then looks up the #protectableRule variable.
public class ProtectableMethodAuthorizationManager {
private final MethodSecurityExpressionHandler expressionHandler;
public void protectable(
Supplier<Authentication> authentication,
MethodInvocation mi) {
// Creates evaluation context with authentication and method invocation
// Looks up #protectableRule variable (set by policy engine)
// Evaluates expression; throws AuthorizationDeniedException on failure
}
}
The #protectableRule variable is an SpEL Expression injected into the evaluation context by the XACML policy engine. If the variable is missing or not of type Expression, access is denied.
ProtectableRapidReentryGuard
Prevents the same authenticated user from rapidly re-entering the same protected method within the configured guard window. The default window is 5 seconds, but the guard can be constructed with a different Duration. This protects high-risk operations from accidental duplicate submissions and burst retries.
Guard Logic
public void check(Authentication authentication, MethodInvocation methodInvocation) {
// 1. Skip if not authenticated
// 2. Resolve current HttpServletRequest from RequestContextHolder
// 3. Generate context binding hash via SessionFingerprintUtil
// 4. Build resource key: "ClassName.methodName[optional-arg0-hash]|HTTP_METHOD /uri"
// 5. Attempt tryAcquire(userId, contextBindingHash, resourceKey, configured window)
// 6. If not acquired, throw RapidProtectableReentryDeniedException
}
The resource key combines the method signature with the HTTP request details, ensuring that different URLs invoking the same method are tracked independently. The context binding hash incorporates session fingerprint data (User-Agent, IP, etc.) to prevent cross-session interference.
Repository Interface
public interface ProtectableRapidReentryRepository {
boolean tryAcquire(String userId, String contextBindingHash,
String resourceKey, Duration window);
}
Implementations
| Implementation | Mode | Storage | Key Format |
|---|---|---|---|
InMemoryProtectableRapidReentryRepository |
Standalone | ConcurrentHashMap<String, Instant> |
userId:contextHash:resourceKey |
RedisProtectableRapidReentryRepository |
Distributed | Redis SET NX EX |
security:protectable:rapid-reentry:{md5} |
The in-memory implementation uses lock-free CAS operations with ConcurrentHashMap.putIfAbsent for thread safety. The Redis implementation uses setIfAbsent with TTL for distributed lock semantics, falling back to allow on Redis errors to prevent service disruption.
Connecting Policies to @Protectable Methods
@Protectable methods are enforced by policies stored in the database. The Resource Scanner discovers all @Protectable methods and synchronizes them as METHOD resources. In the current OSS UI, those resources are managed primarily through Policy Center. The underlying resource-management endpoints under /admin/workbench/resources/* are still used behind that integrated screen.
Method Identifier Format
Each @Protectable method is identified by its fully qualified signature:
com.example.service.OrderService.updateOrder(Order)
Creating a Policy for a Method
Two approaches to create policies for @Protectable methods:
- Policy Center (recommended) —Open the Policy Center create surface, locate the scanned
METHODresource in the integrated resources area, define a permission, and continue into the same creation flow. This is the primary OSS screen that combines resource review, permission linkage, quick policy creation, AI-assisted setup, and policy listing. - Policy service / direct creation —Create a policy with a
METHODtarget matching the method identifier and a valid SpEL condition (for examplehasAuthority('ROLE_ADMIN') and #ai.isAllowed()). This is the underlying model used by Policy Center.
Behavior Matrix
ownerField | sync | Behavior |
|---|---|---|
"" (default) | false | SpEL policy evaluation only, async AI analysis |
"" | true | SpEL policy + synchronous AI blocking |
"userId" | false | SpEL policy + ownership-aware hasPermission(...) checks, async AI |
"userId" | true | SpEL policy + ownership-aware hasPermission(...) checks + synchronous AI blocking |
Additional Usage Examples
Basic Protection
Protects a method with default settings: asynchronous evaluation, no owner-based filtering.
@Protectable
@PostMapping("/api/users/{id}/disable")
public void disableUser(@PathVariable Long id) {
userService.disable(id);
}
Owner-Scoped with Synchronous AI Analysis
Combines ownership-aware permission checks with synchronous Zero Trust analysis before the operation proceeds.
@Protectable(ownerField = "userId", sync = true)
@PutMapping("/api/accounts/{id}")
public Account updateAccount(@PathVariable Long id,
@RequestBody AccountDto dto) {
return accountService.update(id, dto);
}
ZeroTrustAction Outcomes (sync mode)
| Action | Behavior |
|---|---|
ALLOW | Method proceeds normally and the result is returned to the caller |
BLOCK | Throws ZeroTrustAccessDeniedException.blocked(resourceId) before the method executes |
CHALLENGE | Throws ZeroTrustAccessDeniedException requesting step-up authentication from the caller |
ESCALATE | Throws ZeroTrustAccessDeniedException.pendingReview(resourceId) before the method executes |
PENDING_ANALYSIS | Throws ZeroTrustAccessDeniedException if the synchronous AI analysis service is unavailable or timed out |
Event Publishing and Metrics
Authorization events are published via ZeroTrustEventPublisher.publishMethodAuthorization() after each invocation. Events are suppressed in the following cases:
RapidProtectableReentryDeniedException—reentry denials are not publishedZeroTrustAccessDeniedException—synchronous AI decisions handle their own event lifecycle- Synchronous protectable methods that return
ALLOW—handled by the synchronous pipeline
When AuthorizationMetrics is available, the interceptor records:
recordProtectable(duration)—invocation duration in nanosecondsrecordAuthzDecision()—authorization decision count
ownerField Deep Dive
The ownerField attribute does not trigger an automatic post-return ownership scan. In the current OSS implementation it is injected into the method-security evaluation context and is used when the active method policy calls ownership-aware hasPermission(...) checks.
How ownerField Is Applied
When ownerField is configured, the method-expression handler passes it into CustomMethodSecurityExpressionRoot. Ownership is then enforced during policy expression evaluation, not after the method returns. The flow is:
CustomMethodSecurityExpressionHandlerresolves the@Protectablemethod and extractsownerField.- The handler injects
#protectableRuleand#ownerFieldinto the evaluation context. - If the policy expression calls
hasPermission(target, permission)orhasPermission(targetId, targetType, permission), the root first delegates to the configured permission evaluator. - If the permission evaluator grants access and
ownerFieldis configured, the root resolves a direct field or matching getter from the authorization target object and compares it withAuthentication.getName().
Comparison with Authenticated User
The resolved owner value is compared with Authentication.getName(). Users with ROLE_ADMIN bypass the ownership check. For non-admin users, a missing owner value or a mismatch fails closed and hasPermission(...) returns false.
When ownerField Does Not Exist
getCreatedBy(). If neither exists, the ownership-aware permission check fails closed and access is denied for non-admin users.
Nested Field Access
The current OSS implementation does not walk nested dot-notation paths such as creator.id. Use a direct field or getter on the authorization target object instead.
@Protectable(ownerField = "createdBy")
public Document getDocument(Long documentId) {
// Ownership-aware checks resolve a direct field or getter such as getCreatedBy().
return documentRepository.findById(documentId).orElseThrow();
}
If your domain model stores ownership in a nested object, expose a direct getter or dedicated field that returns the comparable owner identifier.
sync Mode Guidelines
When to Use Synchronous Mode
The sync attribute controls whether the AI Zero Trust assessment completes before or after the method returns. Choosing the right mode depends on the sensitivity of the operation and the acceptable latency.
| Mode | Behavior | Latency Impact | Security Guarantee |
|---|---|---|---|
sync = true |
Blocks until the AI assessment completes. If the AI returns BLOCK, CHALLENGE, or ESCALATE, the method never executes. | Higher latency (depends on LLM response time, typically 200ms—s) | Strong —the operation is prevented before any side effects occur |
sync = false (default) |
Uses cached or asynchronous AI assessment. The method proceeds immediately; AI analysis happens in the background. | Minimal latency impact | Eventual consistency —threats are detected after the fact and handled through event-driven remediation |
Recommended Use Cases
Use sync = true for operations where preventing unauthorized actions is more critical than response time:
- Financial transactions —Payment processing, fund transfers, refund operations
- Data deletion —Permanent deletion of records, account closure, data purge operations
- Privilege escalation —Role assignment, permission grants, admin-level operations
- Data export —Bulk data downloads, report generation with sensitive data
Use sync = false (default) for read-heavy operations, listing endpoints, and actions where eventual consistency is acceptable and low latency is prioritized.
Managing @Protectable Methods via Admin
@Protectable methods are automatically discovered by the Resource Scanner and synchronized as METHOD resources. In the current OSS UI, the primary administration surface is Policy Center. Its integrated resources area works with the same managed-resource data that the legacy /admin/workbench/resources/* endpoints expose.
Step 1: Review METHOD Resources in Policy Center
Navigate to Admin > ID & Access Management > Policy Center and open the Resources tab. Filter by resource type METHOD to review discovered @Protectable methods. Each entry shows the fully qualified method signature (for example com.example.service.OrderService.updateOrder(Order)).
Step 2: Define a Permission for the METHOD Resource
From the integrated resources area, use Create Permission & Policy to define a permission for the selected method resource. Policy Center persists the permission-to-resource link by calling the managed-resource endpoints behind the integrated UI.
Step 3: Create or Refine the Policy in Policy Center
After the permission is linked, continue in Policy Center to create the actual policy. The current OSS flow supports quick creation, manual creation, AI-assisted setup, list review, simulation, and matrix analysis from the same integrated screen.
Step 4: Match the Policy to METHOD Targets
Method policies must use a METHOD target that matches the discovered method identifier. The underlying policy model remains the same whether the policy is created from quick mode, the AI-assisted create flow, or the manual create tab.
Current OSS UI Notes
- Policy Center is the primary current UI and combines resource review, permission definition, policy creation, policy list, simulation, and matrix analysis.
- The backend still exposes
/admin/workbench/resources/*endpoints, and Policy Center uses them for resource-definition actions. - Standalone
Resource WorkbenchandPolicy Buildercontroller routes still exist in source, but the current OSS template set centers the user workflow on Policy Center.