Authorization: RBAC + ABAC
Role-based access control covers most of what an application needs; a real product usually also needs the finer-grained case — “a rep can edit their own leads, not everyone's.” The platform gives you both, and neither is bolted on after the fact.
Role-based access control
Permissions are declared once, on the controller, and enforced before the method body runs. The platform's shared permission catalogue currently defines 50+ fine-grained permissions across 4 platform roles — spanning identity, tenancy, billing, authorization itself, observability, API keys and webhooks, plus per-vertical permissions for whatever product you build (commerce, CRM, and so on). It's a living catalogue: new permissions get added as the platform grows, minted into the JWT by the same service that issues tokens, so a permission that exists is always checkable — there's no separate copy to keep in sync.
@RequirePermission(Permission.BUSINESS_WRITE)
@PostMapping("/leads")
Lead create(@RequestBody CreateLeadRequest req) { ... }
// Declared once on the controller method. The platform checks
// X-User-Permissions before the method body ever runs.Attribute & ownership-based access control
For the “only the owner, unless you're an admin” pattern — and the richer variants (time windows, IP ranges, deny-overrides) — a real ABAC policy engine sits alongside RBAC, not a workaround bolted on top of it.
@RequireOwnership(entity = Lead.class)
@PatchMapping("/leads/{id}")
Lead update(@PathVariable UUID id, @RequestBody UpdateLeadRequest req) {
// A sales rep can only reach this line for a lead they own.
// A TENANT_ADMIN bypasses the ownership check entirely.
}Why this matters in practice: the most common authorization bug in hand-built SaaS isn't a missing role check — it's the ownership check that only some endpoints remember to add. Declaring it as an annotation, enforced by the platform rather than re-implemented per handler, removes an entire class of “user A can edit user B's record” bugs.