# 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 "rbac/contracts.ncl" in
let Roles = import "config/roles.ncl" in

# Derive valid role names from the canonical registry.
# Every group name and every parent_groups reference must be present here.
let valid_role_names = Roles.roles |> std.array.map (fun r => r.name) in

# Cross-validates that all group names and parent_groups in this file
# resolve to a role defined in site/config/roles.ncl.
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 =
          "rbac.ncl references roles not defined in site/config/roles.ncl: "
          ++ std.string.join ", " all_invalid,
      }
  )
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 = "supervisor",
      parent_groups = ["user"],
      permissions = [
        # Can broadcast notifications (belt-and-suspenders alongside Axum handler check).
        {
          resource = "endpoint",
          pattern  = "/api/notify",
          outcome  = "allow",
          methods  = ["POST"],
          effects  = E.audit_allow,
        },
        # Service token management is admin-only — explicitly deny.
        { resource = "endpoint", pattern = "/api/server_list_service_tokens*",  outcome = "deny", methods = ["POST"], effects = E.audit_deny @ E.api_forbidden },
        { resource = "endpoint", pattern = "/api/server_create_service_token*", outcome = "deny", methods = ["POST"], effects = E.audit_deny @ E.api_forbidden },
        { resource = "endpoint", pattern = "/api/server_revoke_service_token*", outcome = "deny", methods = ["POST"], effects = E.audit_deny @ E.api_forbidden },
        {
          resource = "endpoint",
          pattern  = "/api/*",
          outcome  = "deny",
          methods  = [],
          effects  = E.audit_deny @ E.api_forbidden,
        },
      ],
    },
    {
      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/notify",
          outcome  = "allow",
          methods  = ["POST"],
          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  = [],
        },
        # Public assets — must be explicitly allowed for authenticated users
        # (unauthenticated_allow only applies to anonymous requests)
        { resource = "asset", pattern = "/*", outcome = "allow", methods = [], effects = [] },
        # WebSocket — must be allowed explicitly; unauthenticated_allow doesn't apply to authed users
        { resource = "endpoint", pattern = "/api/ws", outcome = "allow", methods = ["GET"], effects = [] },
        # Authenticated user endpoints (Leptos server functions — hash suffix varies per build)
        { resource = "endpoint", pattern = "/api/track_page_view*",              outcome = "allow", methods = ["POST"], effects = [] },
        { resource = "endpoint", pattern = "/api/server_send_contact_form*",   outcome = "allow", methods = ["POST"], effects = [] },
        { resource = "endpoint", pattern = "/api/server_get_bookmarks*",         outcome = "allow", methods = ["POST"], effects = [] },
        { resource = "endpoint", pattern = "/api/server_add_bookmark*",          outcome = "allow", methods = ["POST"], effects = [] },
        { resource = "endpoint", pattern = "/api/server_remove_bookmark*",       outcome = "allow", methods = ["POST"], effects = [] },
        { resource = "endpoint", pattern = "/api/server_delete_account*",        outcome = "allow", methods = ["POST"], effects = [] },
        { resource = "endpoint", pattern = "/api/server_logout*",                outcome = "allow", methods = ["POST"], effects = [] },
        { resource = "endpoint", pattern = "/api/server_logout_all*",            outcome = "allow", methods = ["POST"], effects = [] },
        { resource = "endpoint", pattern = "/api/server_update_display_name*",   outcome = "allow", methods = ["POST"], effects = [] },
        { resource = "endpoint", pattern = "/api/server_refresh_token*",         outcome = "allow", methods = ["POST"], effects = [] },
        { resource = "endpoint", pattern = "/api/server_get_session_user*",     outcome = "allow", methods = ["POST"], effects = [] },
        { resource = "endpoint", pattern = "/api/server_get_messages*",         outcome = "allow", methods = ["POST"], effects = [] },
        { resource = "endpoint", pattern = "/api/server_mark_message_read*",    outcome = "allow", methods = ["POST"], effects = [] },
        { resource = "endpoint", pattern = "/api/server_get_notes*",            outcome = "allow", methods = ["POST"], effects = [] },
        { resource = "endpoint", pattern = "/api/server_create_note*",          outcome = "allow", methods = ["POST"], effects = [] },
        { resource = "endpoint", pattern = "/api/server_update_note*",          outcome = "allow", methods = ["POST"], effects = [] },
        { resource = "endpoint", pattern = "/api/server_delete_note*",          outcome = "allow", methods = ["POST"], effects = [] },
        { resource = "endpoint", pattern = "/api/server_get_resources*",        outcome = "allow", methods = ["POST"], effects = [] },
        { resource = "endpoint", pattern = "/api/server_delete_message*",       outcome = "allow", methods = ["POST"], effects = [] },
        # Activities — authenticated access (download gated by handler logic)
        { resource = "endpoint", pattern = "/api/server_check_activity_access*",       outcome = "allow", methods = ["POST"], effects = [] },
        { resource = "endpoint", pattern = "/api/server_record_activity_completion*",  outcome = "allow", methods = ["POST"], effects = [] },
        { resource = "endpoint", pattern = "/api/server_submit_internal_questionnaire*", outcome = "allow", methods = ["POST"], effects = [] },
        { resource = "endpoint", pattern = "/api/activities/download/*",               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@example.com",
    #   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 = [] },
      # HTMX public endpoints (htmx-ssr rendering profile, ADR-005). User-
      # scoped endpoints under /api/htmx/user/* are intentionally NOT here;
      # they fall through to the auth required deny.
      { resource = "endpoint", pattern = "/api/htmx/auth/*",          outcome = "allow", methods = [],        effects = [] },
      { resource = "endpoint", pattern = "/api/htmx/theme/*",         outcome = "allow", methods = [],        effects = [] },
      { resource = "endpoint", pattern = "/api/htmx/lang/*",          outcome = "allow", methods = [],        effects = [] },
      { resource = "endpoint", pattern = "/api/htmx/cookies/*",       outcome = "allow", methods = [],        effects = [] },
      { resource = "endpoint", pattern = "/api/htmx/validate/*",      outcome = "allow", methods = [],        effects = [] },
      { resource = "endpoint", pattern = "/api/htmx/content/*",       outcome = "allow", methods = ["GET"],   effects = [] },
      { resource = "endpoint", pattern = "/api/htmx/notifications/*", 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/server_accept_gdpr*",       outcome = "allow", methods = ["POST"],  effects = [] },
      # Session hydration — called by WASM on every new tab to restore AuthState from existing cookie
      { resource = "endpoint", pattern = "/api/server_get_session_user*",  outcome = "allow", methods = ["POST"],  effects = [] },
      { resource = "endpoint", pattern = "/api/track_page_view*",          outcome = "allow", methods = ["POST"],  effects = [] },
      # Contact/work-request form — public submission, no auth required
      { resource = "endpoint", pattern = "/api/server_send_contact_form*", outcome = "allow", methods = ["POST"],  effects = [] },
      { resource = "endpoint", pattern = "/api/server_get_bookmarks*",outcome = "allow", methods = ["POST"],  effects = [] },
      # Activities — check access is a read-only status fn, public
      { resource = "endpoint", pattern = "/api/server_check_activity_access*", outcome = "allow", methods = ["POST"], effects = [] },
      # Activity callback is HMAC-verified, no session required
      { resource = "endpoint", pattern = "/api/activities/callback",            outcome = "allow", methods = ["GET"],  effects = [] },
      # Slidev static builds — public, no auth
      { resource = "asset",    pattern = "/slides/*",                           outcome = "allow", methods = [],       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 | RoleNamesConsistent
