SECURITY FEATURE
RBAC
Semitexa separates coarse-grained capabilities from fine-grained permission slugs so modules can extend authorization without coupling themselves to one storage model.
Feature Guide
A quick orientation block that answers the essential questions: what this feature does, how it works, why it matters, and the key concepts behind it.
What this does
Semitexa RBAC is intentionally hybrid: coarse-grained capabilities can be represented as internal bitmask grants for very fast broad checks, while business-facing permission slugs such as products.write or settings.smtp.update handle exact, human-readable authorization decisions.
How it works
The payload access policy can declare both #[RequiresCapability] and #[RequiresPermission]. Authorizer evaluates them in order: authentication first, then capability checks, then slug checks. CapabilityRegistry maps Capability enum cases to bit positions inside integer segments, while SubjectGrantResolver builds the current subject grant set and asks a module-level PermissionProviderInterface for the user's slug permissions. That means the RBAC core owns the evaluation pipeline, but storage and permission catalogs stay owned by the modules that actually know the business domain.
Why it matters
This split avoids two common failures. A pure slug model becomes noisy for broad platform-level rights, while a pure bitmask model becomes opaque for audits and product-specific rules. The hybrid model keeps the hot path compact and machine-friendly, but still gives reviewers and module authors explicit permission names and extension points.
Key concepts
- #[RequiresCapability]
- Declares a coarse-grained code-level capability check that is evaluated before slug permissions.
- CapabilityRegistry
- Maps Capability enum cases to bitmask segment and bit positions so capability checks can stay fast and internal.
- #[RequiresPermission]
- Declares an exact slug-based permission such as users.manage or settings.smtp.update.
- PermissionProviderInterface
- Contract implemented by domain modules to supply the current user's permission slugs without coupling RBAC to a specific storage backend.
- SubjectGrantResolver
- Builds the authenticated subject's combined grant set and caches it per request before Authorizer evaluates policy requirements.
- module-owned permission catalog
- Each module can define and expose its own permission slugs, roles, and assignment rules while the shared authorization pipeline keeps one evaluation model.
RBAC
Hybrid grant model
Capabilities cover broad platform rights, permission slugs cover exact business actions, and any module can add its own permission list by implementing the RBAC provider contract.
#[RequiresCapability(AdminCapability::BackofficeAccess)]
#[RequiresPermission('products.write')]
#[AsProtectedPayload(path: '/admin/products/{id}', methods: ['PUT'])]
class UpdateProductPayload { ... }
// A domain module supplies slug permissions through PermissionProviderInterface.
| Permission | Admin | Editor | Viewer |
|---|---|---|---|
products.read
|
Granted | Granted | Granted |
products.write
|
Granted | Granted | Denied |
users.manage
|
Granted | Denied | Denied |
orders.manage
|
Granted | Denied | Denied |
settings.manage
|
Granted | Denied | Denied |
Verified against Semitexa Ultimate 2026.09.19.1020
RBAC
Hybrid RBAC: bitmask-backed capabilities for broad checks, slug permissions for exact business rules, and module-owned catalogs behind one authorizer.
How it works
Semitexa separates two authorization layers. Capabilities are coarse-grained platform rights stored as bitmasks, checked with #[RequiresCapability]. Permission slugs are fine-grained business rules registered by any module implementing PermissionProviderInterface and checked with #[RequiresPermission]. The Authorizer resolves both through the SubjectGrantResolver so handlers never touch the grant resolution logic directly.
Why this matters
A single permission model that forces every module to share one storage schema becomes a coupling problem as the application grows. Separating capabilities from slug permissions lets each module own its permission catalog, extend the authorization surface without modifying core tables, and still share one authorizer that keeps the check consistent across the whole application.
Stored roles
semitexa/platform-user keeps roles in the database and gives them to rbac as one more permission provider (StoredRolePermissionProvider). A role is a name and the permission slugs it grants; a user holds any number of roles and gets every slug they grant, on top of what the project's own providers add.
bin/semitexa role:create curator -p exhibits.read -p exhibits.write --tenant=museum
bin/semitexa role:create auditor -p reports.read --shared
bin/semitexa role:assign curator --email=ada@museum.test
bin/semitexa role:grant curator --tenant=museum -p loans.manage --revoke=exhibits.write
bin/semitexa role:list --email=ada@museum.test
bin/semitexa role:assign curator --email=ada@museum.test --remove
bin/semitexa role:delete curator --tenant=museum
role:createmust be told whose role it is:--tenant=<id>for a site's own, or--sharedfor one every site's users can hold.- A site's role is given only to that site's users, and grants only on that site. A user bound to a site holds nothing on a request for another site (one session can reach several, by path, query or a cookie on the parent domain), nor on the root site, where data is not filtered by tenant: their shared roles count on their own site only. An owner (by role, with or without a site) holds the shared roles on every site and on the root site. Without tenancy at all (a single-site install, the CLI), the user's own site counts.
role:assigntakes the user's site's own role of that name, else the shared one;--sharednames the shared one when both exist.role:grantandrole:deletemust be told which role:--tenant=<id>or--shared.- Role names are lower-case letters, digits and
. _ -; permission slugs, granted or revoked, are lower-cased and may also hold:(orders.manage,reports:read). - Only a semitexa/platform-user account has stored roles; a subject signed in another way gets nothing from them.
- A change is seen on the user's next request: grants are cached for one request only.
role:deletetakes the role from everyone who held it.
The tables are platform_role and platform_user_role; bin/semitexa orm:sync creates them. Until it has, stored roles grant nothing and the error log says why, once per worker; permission checks keep working on the other providers.
How it works
The payload access policy can declare both #[RequiresCapability] and #[RequiresPermission]. Authorizer evaluates them in order: authentication first, then capability checks, then slug checks. CapabilityRegistry maps Capability enum cases to bit positions inside integer segments, while SubjectGrantResolver builds the current subject grant set and asks a module-level PermissionProviderInterface for the user's slug permissions. That means the RBAC core owns the evaluation pipeline, but storage and permission catalogs stay owned by the modules that actually know the business domain.
Why it matters
This split avoids two common failures. A pure slug model becomes noisy for broad platform-level rights, while a pure bitmask model becomes opaque for audits and product-specific rules. The hybrid model keeps the hot path compact and machine-friendly, but still gives reviewers and module authors explicit permission names and extension points.
Key concepts
- #[RequiresCapability]
- Declares a coarse-grained code-level capability check that is evaluated before slug permissions.
- CapabilityRegistry
- Maps Capability enum cases to bitmask segment and bit positions so capability checks can stay fast and internal.
- #[RequiresPermission]
- Declares an exact slug-based permission such as users.manage or settings.smtp.update.
- PermissionProviderInterface
- Contract implemented by domain modules to supply the current user's permission slugs without coupling RBAC to a specific storage backend.
- SubjectGrantResolver
- Builds the authenticated subject's combined grant set and caches it per request before Authorizer evaluates policy requirements.
- module-owned permission catalog
- Each module can define and expose its own permission slugs, roles, and assignment rules while the shared authorization pipeline keeps one evaluation model.