Rustelo/resources/nickel/rbac/rbac-compose.ncl
Jesús Pérez f1010e9d07
chore: update
2026-07-18 20:16:16 +01:00

112 lines
3.8 KiB
Text

# RBAC Composition Template
#
# Copy to site/rbac.ncl and activate the features your site uses.
# Remove feature imports you don't need.
#
# Validate: nickel export site/rbac.ncl
let C = import "rbac/contracts.ncl" in
let Roles = import "config/roles.ncl" in
let valid_role_names = Roles.roles |> std.array.map (fun r => r.name) in
let RoleNamesConsistent =
std.contract.custom (fun _label value =>
let unknown_groups =
value.groups
|> std.array.filter (fun g => !(std.array.elem g.name valid_role_names))
|> std.array.map (fun g => g.name)
in
let unknown_parents =
value.groups
|> std.array.flat_map (fun g =>
g.parent_groups
|> std.array.filter (fun p => !(std.array.elem p valid_role_names))
)
in
let all_invalid = unknown_groups @ unknown_parents in
if all_invalid == [] then 'Ok value
else 'Error { message = "Unknown roles: " ++ std.string.join ", " all_invalid }
)
in
# ── Feature fragments — uncomment what your site uses ────────────────────────
# Each fragment exposes permissions arrays keyed by role name.
let base = import "rustelo/resources/nickel/rbac/rbac-base.ncl" in
let auth = import "rustelo/resources/nickel/rbac/features/auth.ncl" in
let content = import "rustelo/resources/nickel/rbac/features/content.ncl" in
let ws = import "rustelo/resources/nickel/rbac/features/websocket.ncl" in
let analytics = import "rustelo/resources/nickel/rbac/features/analytics.ncl" in
# let dashboard = import "rustelo/resources/nickel/rbac/features/user-dashboard.ncl" in
# let admin_ft = import "rustelo/resources/nickel/rbac/features/admin.ncl" in
# ── Site-specific permissions ─────────────────────────────────────────────────
# Add rules specific to your site that don't belong in any framework feature.
let site = {
admin = [],
user = [],
guest = [
{ resource = "page", pattern = "/*", outcome = "allow", methods = [], effects = [] },
],
} in
{
groups = [
{
name = "admin",
parent_groups = [],
permissions =
auth.admin
@ content.admin
@ ws.admin
@ analytics.admin
# @ dashboard.admin
# @ admin_ft.admin
@ site.admin
@ [
# admin catch-all — keep last
{ resource = "endpoint", pattern = "/api/*", outcome = "allow", methods = [], effects = base.effects.audit_allow },
{ resource = "page", pattern = "/*", outcome = "allow", methods = [], effects = [] },
],
},
{
name = "user",
parent_groups = [],
permissions =
auth.user
@ content.user
@ ws.user
@ analytics.user
# @ dashboard.user
# @ admin_ft.user
@ site.user
@ [
# authenticated catch-all deny — keep last
{ resource = "endpoint", pattern = "/api/*", outcome = "deny", methods = [], effects = base.effects.audit_deny @ base.effects.api_forbidden },
],
},
{
name = "guest",
parent_groups = [],
permissions =
auth.guest
@ content.guest
@ ws.guest
@ analytics.guest
# @ dashboard.guest
@ site.guest,
},
],
user_overrides = [],
defaults = base.defaults & {
unauthenticated_allow = [
# Public assets and health — always allow
{ resource = "asset", pattern = "/*", outcome = "allow", methods = [], effects = [] },
{ resource = "endpoint", pattern = "/api/health", outcome = "allow", methods = [], effects = [] },
# Site-specific public routes — add your pages here
],
},
} | C.FileRbacConfig | RoleNamesConsistent