# Reference Rego SKELETON for an external-OPA deployment. It models the # read-ACL cascade ONLY. It is NOT semantically equivalent to zddc-server's # built-in `internal` decider and MUST NOT be deployed as-is for a system # that relies on write authorization. # # Models: the deepest-matching-level read-ACL cascade — glob patterns, # deny-first-within-a-level, default-deny once any .zddc exists. # # Does NOT model (the internal decider in zddc/internal/zddc + internal/policy # does): per-verb authorization (write/create/delete/admin), WORM zones, # roles: membership resolution, inherit:false fences, and standing config-edit # (the `a` verb). Because those are unmodelled this policy is FAIL-CLOSED: # every non-read action is denied, and an elevated admin # (input.user.is_active_admin) is the only write-capable principal. A real # deployment must add the missing semantics before granting writes — see the # parity tests under zddc/internal/policy for the dimensions to cover. The # internal Go decider remains the production source of truth; this file is a # starting point, not a tested mirror of it. # # Input shape (matches zddc/internal/policy.AllowInput JSON encoding): # { # "user": {"email": "alice@example.com", "is_active_admin": false}, # "action": "read", # "" / absent == read; else write|create|delete|admin # "path": "/Project-A/sub/", # "policy_chain": { # "levels": [ # {"acl": {}, "admins": ["admin@example.com"]}, # {"acl": {"permissions": {"*@example.com": "rwcd"}}} # ], # "has_any_file": true # } # } # # acl.permissions maps each principal pattern to a verb string drawn from # {r,w,c,d,a}. An empty verb string is an explicit deny. # # Levels are ordered ROOT → LEAF (deepest level last). Cascade walks # bottom-up (deepest first); first explicit match wins; within a single # level, an explicit-deny entry is checked before a grant entry. # # Default-allow when has_any_file is false (no .zddc anywhere → public); # default-deny when has_any_file is true and nothing matched (the safety # net the file at /.zddc enables). package zddc.access import future.keywords.if import future.keywords.in default allow := false # Elevated admins bypass — mirrors the internal decider's single admin # short-circuit. The caller computes is_active_admin (admin authority on this # chain AND elevated/opted-in); trusting it here is the same trust the # internal decider applies. This is the ONLY path that authorizes a non-read # action under this read-ACL skeleton. allow if { input.user.is_active_admin } # This policy models read-ACL only, so every cascade grant below is gated on a # read action; any write/create/delete/admin falls through to the default-deny # above (fail-closed). Empty/absent action == read. 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 anywhere in the chain AND no rule matches. allow if { is_read_action not input.policy_chain.has_any_file count(matched_levels) == 0 } # Read allowed when the deepest matching level grants. allow if { is_read_action count(matched_levels) > 0 deepest := max(matched_levels) level_grants(input.policy_chain.levels[deepest]) } # Set of level indices where the email matches at least one permission # entry. The deepest-index member is the level whose decision counts. matched_levels := {i | some i level_matches(input.policy_chain.levels[i]) } # A level "matches" if some permission entry's pattern matches the email # (regardless of whether the verb string grants or denies). Whether the # level grants or denies is a separate question (level_grants below). level_matches(level) if { some pattern, _ in level.acl.permissions email_matches(pattern, input.user.email) } # A level grants iff (a) no explicit-deny entry at this level matches AND # (b) some grant entry (non-empty verbs) matches. Mirrors # GrantedVerbsAtLevel in acl.go: explicit deny wins within a level. level_grants(level) if { not level_denies(level) some pattern, verbs in level.acl.permissions verbs != "" email_matches(pattern, input.user.email) } level_denies(level) if { some pattern, verbs in level.acl.permissions verbs == "" email_matches(pattern, input.user.email) } # email_matches: glob-match a pattern against an email, with the @-boundary # rule from acl.go's MatchesPattern: * does not cross @. Four cases: # # 1. exact match (covers patterns with no wildcard) # 2. bare "*" matches any non-empty email (special case because OPA's # glob.match treats empty delimiters [] inconsistently for the # lone-* pattern) # 3. pattern has both * and @: standard glob with @ as a delimiter so # `*@example.com` matches alice@example.com but `*example.com` # does NOT match anything (* won't cross @) # 4. pattern has * but no @: glob against the full email with no # delimiter (so `alice*` matches alice@anything) 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) }