Multi-Tenancy
jSentinel 00.70 lays a tenant-ready foundation without forcing a tenant-admin model on you. Single-tenant applications use the default tenant transparently; multi-tenant deployments get isolation in every store key and service.
TenantId
public record TenantId(String value) {
public static final TenantId DEFAULT = new TenantId("default");
}Design rule: every user-, role-, session- or security-state-related
store carries TenantId in its key or record. A single-tenant app never
references TenantId directly — the defaults resolve to TenantId.DEFAULT
on every read path, so existing code keeps working unchanged.
Tenant-scoped keys and records
Every Phase-2 persistence store and every Phase-4/7 service is tenant-scoped:
| Key / record | Tenant slot |
|---|---|
LoginAttemptKey(username, clientAddress) | resolved per tenant |
RoleAssignmentKey(tenant, subjectId) | explicit |
SessionRecord(sessionId, subjectId, tenantId, …) | explicit |
SecurityVersionKey(tenant, subjectId) | explicit |
RateLimitKey(tenant, scope) | explicit |
BootstrapState | per tenant |
Resource-aware tenancy
The Policy API’s ResourceRef(resourceType, resourceId, tenant) carries
the tenant alongside the resource, so a policy like
ResourcePredicates.ownerMatchesSubject("document") can never match a
resource from a foreign tenant. See Policy API.
SecurityVersion — role refresh for active sessions
A long-lived session must react when an admin revokes a role mid-flight.
jSentinel solves this with a per-(subject, tenant) security version:
public record SessionRecord(
SessionId sessionId,
SubjectId subjectId,
TenantId tenantId,
Instant createdAt,
Instant lastActivityAt,
SecurityVersion securityVersionAtLogin,
SessionStatus status
) {}The login captures the current SecurityVersion; a role change bumps it;
each request compares the session’s snapshot against the live version:
| Type | Role |
|---|---|
SecurityVersionStore SPI (InMemorySecurityVersionStore default) | Stores the current version per (tenant, subject) |
sealed SecurityVersionStatus | Current(at) or Drifted(snapshot, current) |
SecurityVersionEnforcer | Publishes a SessionStale audit event on drift |
sealed EnforcementOutcome | Continue or SessionStale(status) |
Adapter wiring:
- Vaadin —
SecurityVersionEnforcerListener(@ListenerPriority(MAX_VALUE)) reroutes a drifted session to the login view. - REST —
RestSecurityVersionFilterreturns401 + WWW-Authenticate: SessionStale(RFC 7235).
LoginView captures the snapshot automatically after a successful login
when both SecurityVersionStore and SubjectIdResolver are SPI-wired;
without either, the capture is a strict no-op.
Result
- An admin revokes a role → the next request of the running session is
rejected (Vaadin reroute / REST
401), no waiting for the session to expire. - Cross-tenant data can never leak through a shared key — the tenant is part of the key, not an afterthought.
SecurityVersion are marked
@ExperimentalSecurityApi in 00.70. There is intentionally no
built-in tenant-admin model — that stays an application concern.