# 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)
}
