Skip to content
Flag of Europe
Made in the European Union · Independently built · Released under EUPL 1.2
Multi-Tenancy

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 / recordTenant slot
LoginAttemptKey(username, clientAddress)resolved per tenant
RoleAssignmentKey(tenant, subjectId)explicit
SessionRecord(sessionId, subjectId, tenantId, …)explicit
SecurityVersionKey(tenant, subjectId)explicit
RateLimitKey(tenant, scope)explicit
BootstrapStateper 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:

TypeRole
SecurityVersionStore SPI (InMemorySecurityVersionStore default)Stores the current version per (tenant, subject)
sealed SecurityVersionStatusCurrent(at) or Drifted(snapshot, current)
SecurityVersionEnforcerPublishes a SessionStale audit event on drift
sealed EnforcementOutcomeContinue or SessionStale(status)

Adapter wiring:

  • VaadinSecurityVersionEnforcerListener (@ListenerPriority(MAX_VALUE)) reroutes a drifted session to the login view.
  • RESTRestSecurityVersionFilter returns 401 + 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.
Multi-tenancy and SecurityVersion are marked @ExperimentalSecurityApi in 00.70. There is intentionally no built-in tenant-admin model — that stays an application concern.