# Federal-mode reference policy: parent-deny-is-absolute (NIST AC-6). # # This is a strict-least-privilege variant of access.rego. The two policies # differ in exactly one rule, but the semantic difference is meaningful for # federal evaluators: # # access.rego (commercial, default): # "Bottom-up walk; first explicit match wins; deny-first within a level. # A leaf-level allow CAN override an ancestor's deny." # Test: cascade_test.go "leaf allows user that parent denies → leaf wins". # # access_federal.rego (federal): # "Any deny anywhere along the chain is absolute. An allow only matters # if no ancestor (or sibling level) has denied the same email. Leaf- # level allows do NOT override ancestor denies." # Required by NIST AC-6 (Least Privilege) default expectations: a # central admin's deny at the root must be unbypassable by a tenant # who controls a subtree's .zddc. # # Why ship two policies? The internal Go evaluator (in zddc/internal/zddc/ # acl.go) implements only the commercial cascade — it's the rule the # default deployment exercises. Federal customers running their own OPA # with this file get the strict variant without any zddc-server code # change. They can also write a hybrid policy (e.g. "deny is absolute # only for emails matching some pattern; cascade rules for everyone # else") since once they're hosting their own OPA, the constraint is # whatever they write. # # 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 # Allow 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 { not input.policy_chain.has_any_file not any_deny_match not any_allow_match } # Allow 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 { 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) }