# RBAC Policy Configuration # # Defines access rules for the website. Identity (who the user is) # comes from JWT claims or session; this file defines what each group # is allowed to access. # # Group names must match the role strings derived from JWT: # Role::Admin → "admin" # Role::Moderator → "moderator" # Role::User → "user" # Role::Guest → "guest" # Role::Custom(s) → s # # Validate: nickel export rbac.ncl let C = import "./nickel/rbac/contracts.ncl" in # Reusable effect sets — compose with array concatenation (@) let E = { redirect_login = [{ type = "redirect", to = "/login?return={request.path}" }], audit_deny = [{ type = "audit_log", event = "access_denied", level = "warn" }], audit_allow = [{ type = "audit_log", event = "access_granted", level = "info" }], api_forbidden = [{ type = "response", status = 403, body.error = "Forbidden" }], log_warn = [{ type = "log", level = "warn", message = "Denied: {request.path}" }], security_alert = [{ type = "notify", channel = "security", message = "Admin access: {request.path} by {user.email}", }], } in { groups = [ { name = "admin", parent_groups = [], permissions = [ { resource = "endpoint", pattern = "/api/*", outcome = "allow", methods = [], effects = E.audit_allow, }, { resource = "page", pattern = "/*", outcome = "allow", methods = [], effects = [], }, ], }, { name = "moderator", parent_groups = ["user"], permissions = [ { resource = "endpoint", pattern = "/api/content/*", outcome = "allow", methods = ["GET", "POST", "PUT"], effects = E.audit_allow, }, { resource = "endpoint", pattern = "/api/admin/*", outcome = "deny", methods = [], effects = E.audit_deny @ E.api_forbidden, }, { resource = "page", pattern = "/admin/content/*", outcome = "allow", methods = [], effects = [], }, ], }, { name = "user", parent_groups = [], permissions = [ { resource = "page", pattern = "/*", outcome = "allow", methods = [], effects = [], }, # Allow read access to public content endpoints (blog, recipes, etc.) { resource = "endpoint", pattern = "/api/content-render/*", outcome = "allow", methods = ["GET"], effects = [], }, { resource = "endpoint", pattern = "/api/content-index/*", outcome = "allow", methods = ["GET"], effects = [], }, { resource = "endpoint", pattern = "/api/*", outcome = "deny", methods = [], effects = E.audit_deny @ E.api_forbidden, }, ], }, ], user_overrides = [ # Example: block a specific user and redirect to login # { # id = "blocked@ontoref.dev", # permissions = [{ # resource = "page", # pattern = "/*", # outcome = "deny", # methods = [], # effects = E.audit_deny @ E.security_alert @ E.redirect_login, # }], # }, ], defaults = { unauthenticated_allow = [ # Protected paths evaluated first — first match wins { resource = "page", pattern = "/admin/*", outcome = "deny", methods = [], effects = E.audit_deny @ E.redirect_login }, # Recipes require authentication (EN + ES routes) { resource = "page", pattern = "/recipes", outcome = "deny", methods = [], effects = E.audit_deny @ E.redirect_login }, { resource = "page", pattern = "/recipes/*", outcome = "deny", methods = [], effects = E.audit_deny @ E.redirect_login }, { resource = "page", pattern = "/recetas", outcome = "deny", methods = [], effects = E.audit_deny @ E.redirect_login }, { resource = "page", pattern = "/recetas/*", outcome = "deny", methods = [], effects = E.audit_deny @ E.redirect_login }, # Public pages { resource = "page", pattern = "/login", outcome = "allow", methods = [], effects = [] }, { resource = "page", pattern = "/register", outcome = "allow", methods = [], effects = [] }, { resource = "page", pattern = "/*", outcome = "allow", methods = [], effects = [] }, # Recipes API endpoints also require authentication { resource = "endpoint", pattern = "/api/content-render/recipes/*", outcome = "deny", methods = [], effects = E.audit_deny @ E.api_forbidden }, { resource = "endpoint", pattern = "/api/content-index/recipes/*", outcome = "deny", methods = [], effects = E.audit_deny @ E.api_forbidden }, # Public endpoints and assets { resource = "endpoint", pattern = "/api/health", outcome = "allow", methods = [], effects = [] }, { resource = "endpoint", pattern = "/api/auth/*", outcome = "allow", methods = [], effects = [] }, { resource = "endpoint", pattern = "/api/ws", outcome = "allow", methods = ["GET"], effects = [] }, # Leptos server functions for OTP auth flow — hash suffix varies per build, # glob * matches the numeric hash without requiring a path separator. { resource = "endpoint", pattern = "/api/server_request_otp*", outcome = "allow", methods = ["POST"], effects = [] }, { resource = "endpoint", pattern = "/api/server_verify_otp*", outcome = "allow", methods = ["POST"], effects = [] }, { resource = "endpoint", pattern = "/api/content-render/*", outcome = "allow", methods = ["GET"], effects = [] }, { resource = "endpoint", pattern = "/api/content-index/*", outcome = "allow", methods = ["GET"], effects = [] }, { resource = "asset", pattern = "/*", outcome = "allow", methods = [], effects = [] }, ], unauthenticated_deny = [], authenticated_fallback = "deny", unauthenticated_fallback = "deny", fallback_effects = E.log_warn, }, } | C.FileRbacConfig