website-htmx-rustelo-code/site/models/no_recipes_rbac.ncl
2026-07-10 03:44:13 +01:00

165 lines
6.3 KiB
Text

# 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