# 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