112 lines
3.8 KiB
Text
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
|