/**
* @harn-entrypoint-category harness.stdlib
*
* std/harness/policy — imperative route auth-policy guards that compose the
* ambient `harness.auth` principal. Reach for these when an authorization
* decision depends on runtime data (a path or body field, resource
* ownership) that the declarative `@scopes(...)` / `@policy(kinds: ...)`
* route annotations cannot see; the annotations remain the right tool for
* static, request-independent gates.
*
* Each guard returns `nil` when the bound principal and request context
* satisfy the policy, or a ready-to-return HTTP envelope (built by
* `http_error`) when they do not — so a handler denies in one line:
*
* let denial = require_policy({kinds: ["operator"], scopes: ["secrets:write"]})
* if denial != nil { return denial }
* // ...authorized work...
*
* Denials are tenant-safe: the body names the route's requirement or the
* failing comparison label, but never echoes the caller's own principal kind,
* tenant, subject, or resource ids. Guards fail closed — unauthenticated
* callers cannot satisfy scope/kind clauses or auth/tenant-bound matches.
*/
fn __policy_list(value, label) {
if value == nil {
return []
}
if type_of(value) != "list" {
throw label + " must be a list of strings"
}
return value
}
fn __policy_match_list(value, label) {
if value == nil {
return []
}
if type_of(value) != "list" {
throw label + " must be a list of match specs"
}
return value
}
fn __policy_missing_auth() {
return http_error(401, "unauthorized", "authentication required", {kind: "missing_auth"})
}
fn __policy_auth_view() {
return {
subject: harness.auth.try_subject(),
scheme: harness.auth.try_scheme(),
kind: harness.auth.kind(),
scopes: harness.auth.scopes(),
}
}
fn __policy_tenant_view() {
return {id: harness.tenant.try_id()}
}
fn __policy_request_body(req) {
let raw = req?.body
if raw == nil {
return nil
}
if type_of(raw) == "string" {
let text = trim(raw)
if text == "" {
return nil
}
let parsed = try {
json_parse(text)
}
if is_ok(parsed) {
return unwrap(parsed)
}
return nil
}
return raw
}
fn __policy_follow_path(root, parts, start_index) {
var current = root
var index = start_index
while index < len(parts) {
if current == nil || type_of(current) != "dict" {
return nil
}
current = current[parts[index]]
index = index + 1
}
return current
}
fn __policy_path_root(path) {
if type_of(path) != "string" || trim(path) == "" {
return nil
}
return split(path, ".")[0]
}
fn __policy_ref_path(ref) {
if type_of(ref) == "dict" {
return ref.path
}
if type_of(ref) == "string" {
return ref
}
return nil
}
fn __policy_ref_needs_auth(ref) {
let root = __policy_path_root(__policy_ref_path(ref))
return root == "auth" || root == "tenant"
}
fn __policy_resolve_ref(ref, req, ctx) {
if type_of(ref) == "dict" && ref.keys().contains("value") {
return ref.value
}
let path = __policy_ref_path(ref)
if path == nil {
return ref
}
let parts = split(path, ".")
let root = parts[0]
if root == "req" || root == "request" {
return __policy_follow_path(req ?? {}, parts, 1)
}
if root == "body" || root == "json" {
return __policy_follow_path(__policy_request_body(req), parts, 1)
}
if root == "auth" {
return __policy_follow_path(__policy_auth_view(), parts, 1)
}
if root == "tenant" {
return __policy_follow_path(__policy_tenant_view(), parts, 1)
}
if root == "ctx" || root == "context" {
return __policy_follow_path(ctx ?? {}, parts, 1)
}
return __policy_follow_path(req ?? {}, parts, 0)
}
fn __policy_enforce_segment(opts, req, ctx) {
let kinds = __policy_list(opts?.kinds, "require_policy: kinds")
let scopes = __policy_list(opts?.scopes, "require_policy: scopes")
let matches = __policy_match_list(opts?.matches, "require_policy: matches")
if (len(kinds) > 0 || len(scopes) > 0) && !harness.auth.is_authenticated() {
return __policy_missing_auth()
}
if len(kinds) > 0 {
let kind = harness.auth.kind()
if kind == nil || !kinds.contains(kind) {
// Tenant-safe: report the route's allow-set, never the caller's kind.
return http_error(
403,
"forbidden",
"principal kind not permitted for this route",
{kind: "forbidden_principal_kind", allowed_kinds: kinds},
)
}
}
for scope in scopes {
if !harness.auth.has_scope(scope) {
return http_error(
403,
"forbidden",
"missing required scope",
{kind: "missing_scope", missing_scope: scope},
)
}
}
for spec in matches {
if type_of(spec) != "dict" {
throw "require_policy: each match must be a dict"
}
let left = spec.left ?? spec.path
let right = spec.right ?? spec.equals
if left == nil || right == nil {
throw "require_policy: match requires left and right"
}
if (__policy_ref_needs_auth(left) || __policy_ref_needs_auth(right))
&& !harness.auth.is_authenticated() {
return __policy_missing_auth()
}
let left_value = __policy_resolve_ref(left, req, ctx)
let right_value = __policy_resolve_ref(right, req, ctx)
if left_value != right_value {
let kind = spec.kind ?? "resource_mismatch"
let label = spec.label ?? "resource"
let message = spec.message ?? "request is not authorized for this resource"
return http_error(
403,
"forbidden",
message,
{kind: kind, label: label, left: __policy_ref_path(left), right: __policy_ref_path(right)},
)
}
}
return nil
}
/**
* Enforce a principal-kind and/or scope policy against the ambient
* `harness.auth` principal and request context. `opts`:
* {
* kinds?: [string],
* scopes?: [string],
* matches?: [{left, right, kind?, label?, message?}],
* methods?: {selector: policy},
* method_path?: string,
* }
* `kinds` (when non-empty) requires the principal's embedder-assigned kind
* to be one of the listed values; `scopes` requires every listed scope to be
* granted. `matches` compares request/body/auth/tenant/context fields without
* echoing their values in denials. `methods` applies an additional policy
* selected by `req.method` by default, or by `method_path` for JSON-RPC-style
* bodies. Returns `nil` when every clause passes, otherwise an HTTP envelope
* the caller should return verbatim.
*
* @effects: []
* @errors: []
*/
pub fn require_policy(opts, req = nil, ctx = nil) {
if opts != nil && type_of(opts) != "dict" {
throw "require_policy: opts must be a dict"
}
let base = __policy_enforce_segment(opts ?? {}, req, ctx)
if base != nil {
return base
}
let methods = opts?.methods
if methods != nil {
if type_of(methods) != "dict" {
throw "require_policy: methods must be a dict"
}
let selector_path = opts?.method_path ?? "req.method"
let selector = __policy_resolve_ref(selector_path, req, ctx)
if selector != nil && methods[to_string(selector)] != nil {
let method = methods[to_string(selector)]
if type_of(method) != "dict" {
throw "require_policy: method policy must be a dict"
}
return __policy_enforce_segment(method, req, ctx)
}
}
return nil
}