Skip to content

Role-based access control

Fluxzero allows you to restrict message handling based on the authenticated user’s roles. This access control happens before the message reaches the handler — similar to how payload validation is enforced.

There are several annotations for declaring user and role requirements.

Use this annotation to ensure that a handler is only invoked if the user has at least one of the specified roles.

@HandleCommand
@RequiresAnyRole({"admin", "editor"})
void handle(UpdateArticle command) { ... }
@RequiresAnyRole("admin")
public record DeleteAccount(String userId) {
}

This annotation works the other way around — it prevents message handling if the user has any of the specified roles.

@ForbidsAnyRole("guest")
@HandleCommand
void handle(SensitiveOperation command) { ... }

Ensures that a message can only be handled if an authenticated user is present. If no user is found, the message is rejected with an UnauthenticatedException.

@RequiresUser
@HandleCommand
void handle(UpdateProfile command) { ... }

Allows a message to be processed even if no authenticated user is present — ideal for public APIs or health checks.

@NoUserRequired
@HandleCommand
void handle(SignUpUser command) { ... }

Prevents message handling if an authenticated user is present. This is useful for restricting certain flows to unauthenticated users — such as guest signups.

@ForbidsUser
@HandleCommand
void handle(SignUpAsGuest command) { ... }

Controlling behavior on unauthorized access

Section titled “Controlling behavior on unauthorized access”

All authorization annotations include an optional throwIfUnauthorized() property (default: true) that controls what happens when access is denied.

  • If throwIfUnauthorized = true:

    • If a user is required but not present, an UnauthenticatedException is thrown.
    • If a user is present but lacks required roles, an UnauthorizedException is thrown.
  • If throwIfUnauthorized = false:

    • The message is silently skipped, allowing delegation to other eligible handlers (if any).

Role annotations support nesting and overrides

Section titled “Role annotations support nesting and overrides”

Fluxzero evaluates annotations hierarchically:

  • If @RequiresAnyRole("admin") is placed on a package, it applies to all handlers and payloads in that package.
  • You can override it on specific classes or methods.
package-info.java
@RequiresUser
package com.myapp.handlers;
@NoUserRequired
@HandleCommand
void handle(PublicPing ping) { ... } // Overrides the package-level requirement

You can define custom annotations using enums for structured roles.

public enum Role {
ADMIN, EDITOR, USER
}
@RequiresAnyRole
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface RequiresRole {
Role[] value();
}
@HandleCommand
@RequiresRole(Role.ADMIN)
void handle(DeleteAccount command) { ... }

  • Use role annotations on payload classes to guarantee strict access checks across environments.
  • Apply them on handlers to allow fallback logic or specialization by role.
  • Set default access rules at the package level, and override them as needed.
  • Create custom annotations to avoid repeating role strings throughout your codebase.


User roles are resolved by the configured UserProvider, which extracts user info from message metadata (e.g. headers or tokens). Fluxzero uses a pluggable SPI to register this provider.

Use User.id() for ownership checks, audit references, and other behavior that needs a stable user identity. In the 1.x SDK it defaults to Principal.getName() for compatibility with existing User implementations. Override id() when the principal name is a display or provider-facing name. AbstractUserProvider stores this ID in user metadata and resolves it through getUserById(...); keep accepting earlier getName() values while older messages can still be in flight.


You can implement a custom UserProvider to extract users from headers, JWT tokens, cookies, etc.

public class MyUserProvider extends AbstractUserProvider {
public MyUserProvider() {
super("Authorization", MyUser.class);
}
@Override
public User fromMessage(HasMessage message) {
if (message.toMessage() instanceof WebRequest request) {
return decodeToken(request.getHeader("Authorization"));
}
return super.fromMessage(message);
}
private User decodeToken(String header) {
// Parse and validate JWT token here
return ...;
}
}

Your UserProvider can also implement:

  • getSystemUser() — returns the default system-level user (used in tests and scheduled handlers)
  • getUserById(...) — used by test utilities like fixture.whenCommandByUser(...)

This ensures consistent behavior across environments.


To register your custom UserProvider, use Java’s SPI mechanism:

src/main/resources/META-INF/services/io.fluxzero.sdk.tracking.handling.authentication.UserProvider

Add each provider class (one per line):

com

© 2026 Fluxzero