The bundled reference Rego (`zddc-server --print-rego`) modeled the read-ACL cascade only, but its header claimed to "mirror the internal decider exactly, validated on every CI run." It is verb-blind, role-blind, WORM-blind, and admin-blind: an external-OPA deployment (ZDDC_OPA_URL=http(s)/unix) loading it granted writes/deletes to read-only principals and ignored WORM zones. The parity tests never exercised a write action, a role principal, a WORM level, or is_active_admin — so the divergence shipped silently behind a false "mirrors exactly" claim. Make both shipped policies fail-closed instead of falsely-complete: - access.rego / access_federal.rego: gate every cascade grant on a read action (empty/absent == read); non-read actions fall through to default-deny. access.rego honors the single is_active_admin bypass (the one write-capable principal); access_federal.rego deliberately has none (strict AC-6). - Rewrite the access.rego / access_federal.rego / rego.go headers: these are read-ACL SKELETONS, NOT a tested mirror of the internal decider; operators must add write/WORM/role/admin semantics before granting writes. - policy.go: fix the stale AllowInput doc claiming the internal decider "treats read and write identically — any allow grants full CRUD" (it honors the action verb, with the WORM clamp and admin/elevation bypass applied). Tests: - rego_failclosed_test.go: pins the contract — reads allowed, every write verb denied, active-admin writes allowed (commercial) / denied (federal). - embedded_neutral_test.go: pins that EmbeddedDefaults() carries no top-level worm: and no role members — the invariant that makes policy.SerializableChain dropping PolicyChain.Embedded behavior-neutral (a latent wire-contract gap). Existing read-cascade parity + federal-divergence tests stay green; full Go suite + vet pass. The default in-process InternalDecider is unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
112 lines
3.6 KiB
Rego
112 lines
3.6 KiB
Rego
# Federal-mode reference SKELETON: parent-deny-is-absolute (NIST AC-6),
|
|
# read-ACL only.
|
|
#
|
|
# Like access.rego this models the read cascade ONLY and is NOT a complete
|
|
# authorization policy — it does not implement per-verb (write/create/delete/
|
|
# admin), WORM, roles, inherit:false fences, or config-edit. It is therefore
|
|
# FAIL-CLOSED: every non-read action is denied. This variant deliberately has
|
|
# NO admin bypass either — under AC-6 least-privilege the default posture is
|
|
# deny, and an operator who needs a write path must add the per-verb (and, if
|
|
# desired, admin-escape) semantics themselves. As shipped it authorizes reads
|
|
# only.
|
|
#
|
|
# The ONE modelled difference from access.rego: any deny anywhere on the chain
|
|
# is absolute — a leaf-level allow does NOT override an ancestor's deny.
|
|
# Required by NIST AC-6: a central admin's root deny must be unbypassable by
|
|
# a tenant who controls a subtree's .zddc.
|
|
# access.rego (commercial): leaf allow CAN override an ancestor deny.
|
|
# access_federal.rego: ancestor deny is absolute.
|
|
#
|
|
# The internal Go evaluator implements neither these federal semantics nor a
|
|
# tested mirror of this file; federal-mode is reachable only by running OPA
|
|
# with this policy and pointing ZDDC_OPA_URL at it. See federal_parity_test.go
|
|
# for the modelled read-cascade divergence fixtures.
|
|
#
|
|
# Input shape: identical to access.rego — see that file's docstring.
|
|
# acl.permissions maps principal patterns to verb strings; an empty
|
|
# verb string is an explicit deny.
|
|
|
|
package zddc.access_federal
|
|
|
|
import future.keywords.if
|
|
import future.keywords.in
|
|
|
|
default allow := false
|
|
|
|
# Read-ACL only: every grant rule is gated on a read action; any write/
|
|
# create/delete/admin falls through to the default-deny above (fail-closed).
|
|
# Empty/absent action == read. (No admin bypass in federal mode — see header.)
|
|
is_read_action if {
|
|
not input.action
|
|
}
|
|
|
|
is_read_action if {
|
|
input.action == ""
|
|
}
|
|
|
|
is_read_action if {
|
|
input.action == "read"
|
|
}
|
|
|
|
# Read allowed when no .zddc files exist anywhere AND no rule matches.
|
|
# Same default-allow case as commercial; preserves the empty-tree
|
|
# behaviour. (zddc-server's --insecure check at startup makes this
|
|
# unreachable in any non-deliberately-public deployment.)
|
|
allow if {
|
|
is_read_action
|
|
not input.policy_chain.has_any_file
|
|
not any_deny_match
|
|
not any_allow_match
|
|
}
|
|
|
|
# Read allowed when files exist, no level (any depth) denies, and at least
|
|
# one level allows. The "any level" check is what makes parent denies
|
|
# absolute — there is no "deepest match wins" rule here.
|
|
allow if {
|
|
is_read_action
|
|
input.policy_chain.has_any_file
|
|
not any_deny_match
|
|
any_allow_match
|
|
}
|
|
|
|
# Any explicit-deny permission entry at ANY level matches the email.
|
|
any_deny_match if {
|
|
some level in input.policy_chain.levels
|
|
some pattern, verbs in level.acl.permissions
|
|
verbs == ""
|
|
email_matches(pattern, input.user.email)
|
|
}
|
|
|
|
# Any grant permission entry (non-empty verbs) at ANY level matches.
|
|
any_allow_match if {
|
|
some level in input.policy_chain.levels
|
|
some pattern, verbs in level.acl.permissions
|
|
verbs != ""
|
|
email_matches(pattern, input.user.email)
|
|
}
|
|
|
|
# email_matches: identical to access.rego — see that file for the
|
|
# rationale on the four cases. Duplicated rather than imported so this
|
|
# file is self-contained for operators who copy it as a starting point.
|
|
|
|
email_matches(pattern, email) if {
|
|
pattern == email
|
|
}
|
|
|
|
email_matches(pattern, email) if {
|
|
pattern == "*"
|
|
email != ""
|
|
}
|
|
|
|
email_matches(pattern, email) if {
|
|
contains(pattern, "*")
|
|
contains(pattern, "@")
|
|
glob.match(pattern, ["@"], email)
|
|
}
|
|
|
|
email_matches(pattern, email) if {
|
|
contains(pattern, "*")
|
|
not contains(pattern, "@")
|
|
pattern != "*"
|
|
glob.match(pattern, [], email)
|
|
}
|