# 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 "%, }, }