98 lines
3 KiB
Text
98 lines
3 KiB
Text
# Role Registry Type Contracts
|
|
#
|
|
# Schema for site/config/roles.ncl — the canonical role list that
|
|
# rbac.ncl groups, DB user_roles seeds, and the Rust Role enum must agree with.
|
|
|
|
let RustVariants = ["Admin", "Moderator", "User", "Guest", "Custom"] in
|
|
|
|
{
|
|
OneOf = fun allowed => std.contract.from_predicate (fun v => std.array.elem v allowed),
|
|
|
|
# Single role definition — one entry per role in the application
|
|
RoleEntry = {
|
|
name
|
|
| String
|
|
| doc m%"
|
|
Canonical role identifier.
|
|
Must match: JWT claims, DB user_roles.name, and rbac.ncl group names.
|
|
"%,
|
|
|
|
description
|
|
| String
|
|
| doc "Human-readable description for admin UI and documentation",
|
|
|
|
db_seed
|
|
| Bool
|
|
| doc m%"
|
|
Whether this role is inserted into user_roles on initial DB migration.
|
|
Set false for virtual roles (e.g. guest) that have no DB row.
|
|
"%
|
|
| default = true,
|
|
|
|
is_system
|
|
| Bool
|
|
| doc "System roles cannot be deleted or renamed via admin UI"
|
|
| default = false,
|
|
|
|
parent_roles
|
|
| Array String
|
|
| doc m%"
|
|
Role names inherited by this role (BFS expansion, same semantics as
|
|
rbac.ncl parent_groups). Names must resolve to other entries in this array.
|
|
"%
|
|
| default = [],
|
|
|
|
rust_variant
|
|
| OneOf RustVariants
|
|
| doc m%"
|
|
Corresponding Rust enum variant in rustelo_core_lib::auth::Role.
|
|
Use "Custom" for roles that have no dedicated variant.
|
|
"%
|
|
| default = "Custom",
|
|
|
|
db_level
|
|
| Number
|
|
| doc "Privilege level for UI ordering (higher = more privilege)"
|
|
| default = 0,
|
|
|
|
is_default
|
|
| Bool
|
|
| doc m%"
|
|
Role assigned automatically on new user registration.
|
|
Exactly one role in the registry must have is_default = true.
|
|
Must match registration_role in site/config/auth.toml.
|
|
"%
|
|
| default = false,
|
|
},
|
|
|
|
# Validates that exactly one role carries is_default = true.
|
|
ExactlyOneDefaultRole =
|
|
std.contract.custom (fun _label value =>
|
|
let defaults = value.roles |> std.array.filter (fun r => r.is_default) in
|
|
let n = std.array.length defaults in
|
|
if n == 1 then
|
|
'Ok value
|
|
else
|
|
'Error {
|
|
message =
|
|
"Exactly one role must have is_default = true, found "
|
|
++ std.string.from_number n,
|
|
}
|
|
),
|
|
|
|
# Root contract applied to site/config/roles.ncl
|
|
RolesConfig = {
|
|
roles
|
|
| Array RoleEntry
|
|
| doc m%"
|
|
Authoritative role registry.
|
|
|
|
Invariants enforced at eval time:
|
|
- Exactly one entry has is_default = true (enforced by ExactlyOneDefaultRole)
|
|
- Every rbac.ncl group name must appear here
|
|
- Every parent_role reference must resolve to another entry in this array
|
|
- DB migrations must seed all entries where db_seed = true
|
|
- registration_role in auth.toml must match the entry with is_default = true
|
|
"%,
|
|
},
|
|
}
|