feat(zddc): Phase 1 — embedded defaults.zddc + inherit + show-defaults
First step of the .zddc-first-configuration rollout: pure plumbing
that makes the future move-everything-out-of-Go work mechanically
possible without changing any current behaviour.
New pieces:
1. zddc/internal/zddc/defaults.zddc.yaml — a real YAML file in the
repo. Single source of truth for the baked-in baseline; intentionally
minimal in Phase 1 (just title + empty acl) so existing deployments
stay bit-identical until Phase 2 starts populating the schema.
2. //go:embed (defaults.go) bakes the bytes into the binary so
shipped deployments don't need the file. Operators who want a
starting point export with:
zddc-server show-defaults > /var/lib/zddc/root/.zddc
3. PolicyChain gains an Embedded ZddcFile field. EffectivePolicy
layers in the embedded defaults as a baseline below the on-disk
chain. Consumers that want the full effective view consult both;
existing consumers that only read chain.Levels keep working
bit-identically (the new field is additive).
4. New top-level `inherit:` key on ZddcFile. Default true. Set
`inherit: false` on any on-disk .zddc to zero out chain.Embedded
— the operator owns every rule from that level outward. Useful at
the on-disk root to fully reject the embedded defaults; useful at
deeper levels for sandbox subtrees.
5. `zddc-server show-defaults` (also accepts --show-defaults) subcommand
dumps the embedded bytes to stdout — same shape as --print-rego.
No flag plumbing needed beyond the existing args walk.
6. Tests: parse-roundtrip on the embedded file, presence in chain by
default, inherit:false drops it, explicit inherit:true is a no-op
versus the default.
Phase 2 (next): add a `paths:` recursive map + `default_tool:` /
`auto_own:` / `virtual:` keys, populate defaults.zddc.yaml with the
canonical ZDDC convention, and migrate apps.DefaultAppAt /
AutoOwnCanonicalNames / VirtualOnlyCanonicalNames to cascade lookups.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4af0d8ca7c
commit
d84c1908f6
7 changed files with 230 additions and 1 deletions
|
|
@ -50,6 +50,16 @@ func main() {
|
||||||
case "--print-rego=federal":
|
case "--print-rego=federal":
|
||||||
fmt.Print(policy.FederalRego)
|
fmt.Print(policy.FederalRego)
|
||||||
return
|
return
|
||||||
|
case "show-defaults", "--show-defaults":
|
||||||
|
// Dump the embedded baseline .zddc to stdout. Pipe into a
|
||||||
|
// real file (e.g. $ZDDC_ROOT/.zddc) to start from the
|
||||||
|
// shipped defaults and edit; the on-disk copy then
|
||||||
|
// participates in the cascade alongside the embedded
|
||||||
|
// layer (both contribute; child wins). To ignore the
|
||||||
|
// embedded layer entirely after exporting, set
|
||||||
|
// `inherit: false` at the top of the exported file.
|
||||||
|
_, _ = os.Stdout.Write(zddc.EmbeddedDefaultsBytes())
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1300,7 +1300,7 @@ body.help-open .app-header {
|
||||||
</svg>
|
</svg>
|
||||||
<div class="header-title-group">
|
<div class="header-title-group">
|
||||||
<span class="app-header__title" id="table-title">ZDDC Table</span>
|
<span class="app-header__title" id="table-title">ZDDC Table</span>
|
||||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-alpha · 2026-05-11 18:30:39 · ab44d75-dirty</span></span>
|
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-alpha · 2026-05-11 19:40:57 · 4af0d8c-dirty</span></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,21 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// PolicyChain represents a chain of .zddc files from root to leaf.
|
// PolicyChain represents a chain of .zddc files from root to leaf.
|
||||||
|
//
|
||||||
|
// Embedded sits BELOW Levels[0] (the on-disk root .zddc): it's the
|
||||||
|
// baked-in defaults that ship with the binary, used as a baseline
|
||||||
|
// when an on-disk .zddc doesn't specify a rule. Consumers that want
|
||||||
|
// the full effective view should consult Levels then fall back to
|
||||||
|
// Embedded for unresolved lookups.
|
||||||
|
//
|
||||||
|
// Inherit:false on any level in Levels (or at deeper levels of a
|
||||||
|
// future paths: walker) zeroes out Embedded for that policy chain —
|
||||||
|
// the operator has taken full responsibility for spelling out every
|
||||||
|
// rule from scratch.
|
||||||
type PolicyChain struct {
|
type PolicyChain struct {
|
||||||
Levels []ZddcFile // ordered root (index 0) → leaf (last index)
|
Levels []ZddcFile // ordered root (index 0) → leaf (last index)
|
||||||
HasAnyFile bool // true if at least one .zddc file exists in the chain
|
HasAnyFile bool // true if at least one .zddc file exists in the chain
|
||||||
|
Embedded ZddcFile // baked-in defaults; zero ZddcFile{} if any level set inherit:false
|
||||||
}
|
}
|
||||||
|
|
||||||
// VisibleStart returns the lowest level index visible to evaluation at
|
// VisibleStart returns the lowest level index visible to evaluation at
|
||||||
|
|
@ -111,6 +123,20 @@ func EffectivePolicy(fsRoot, dirPath string) (PolicyChain, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Layer in the embedded defaults as the bottom of the cascade
|
||||||
|
// (used as fallback by consumers that consult Embedded). If any
|
||||||
|
// on-disk level set top-level inherit:false, the embedded layer
|
||||||
|
// is dropped — the operator owns every rule.
|
||||||
|
if embedded, err := EmbeddedDefaults(); err == nil {
|
||||||
|
chain.Embedded = embedded
|
||||||
|
for _, lvl := range chain.Levels {
|
||||||
|
if lvl.Inherit != nil && !*lvl.Inherit {
|
||||||
|
chain.Embedded = ZddcFile{}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
policyCache.Store(cacheKey, chain)
|
policyCache.Store(cacheKey, chain)
|
||||||
return chain, nil
|
return chain, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
40
zddc/internal/zddc/defaults.go
Normal file
40
zddc/internal/zddc/defaults.go
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
package zddc
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// defaultsBytes is the embedded baseline .zddc — see defaults.zddc.yaml
|
||||||
|
// for the source-of-truth and a description of its role in the cascade.
|
||||||
|
//
|
||||||
|
//go:embed defaults.zddc.yaml
|
||||||
|
var defaultsBytes []byte
|
||||||
|
|
||||||
|
// EmbeddedDefaultsBytes returns the raw embedded defaults YAML.
|
||||||
|
//
|
||||||
|
// Surface: the show-defaults CLI subcommand dumps these bytes to
|
||||||
|
// stdout so operators can copy them into <ZDDC_ROOT>/.zddc and edit.
|
||||||
|
func EmbeddedDefaultsBytes() []byte {
|
||||||
|
out := make([]byte, len(defaultsBytes))
|
||||||
|
copy(out, defaultsBytes)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
embeddedDefaultsOnce sync.Once
|
||||||
|
embeddedDefaults ZddcFile
|
||||||
|
embeddedDefaultsErr error
|
||||||
|
)
|
||||||
|
|
||||||
|
// EmbeddedDefaults returns the parsed embedded defaults ZddcFile,
|
||||||
|
// memoised. Parse errors surface on the first call and are sticky.
|
||||||
|
//
|
||||||
|
// The cascade walker (EffectivePolicy) consults this as the bottom-
|
||||||
|
// most level unless an on-disk .zddc up the chain sets `inherit: false`.
|
||||||
|
func EmbeddedDefaults() (ZddcFile, error) {
|
||||||
|
embeddedDefaultsOnce.Do(func() {
|
||||||
|
embeddedDefaults, embeddedDefaultsErr = parseBytes(defaultsBytes)
|
||||||
|
})
|
||||||
|
return embeddedDefaults, embeddedDefaultsErr
|
||||||
|
}
|
||||||
29
zddc/internal/zddc/defaults.zddc.yaml
Normal file
29
zddc/internal/zddc/defaults.zddc.yaml
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
# defaults.zddc — embedded baseline configuration for every ZDDC
|
||||||
|
# deployment. Baked into the binary via //go:embed in defaults.go,
|
||||||
|
# loaded as the bottom-most level of the cascade. Operators override
|
||||||
|
# at the on-disk root /.zddc (or any deeper level); to ignore this
|
||||||
|
# file entirely, set `inherit: false` on an on-disk .zddc.
|
||||||
|
#
|
||||||
|
# Phase 1 of the .zddc-first-config rollout. Future phases will move
|
||||||
|
# the hardcoded canonical-folder behaviour (ProjectRootFolders,
|
||||||
|
# PartyFolders, apps.DefaultAppAt, etc.) into this file via a new
|
||||||
|
# `paths:` recursive map + a few new per-directory keys (default_tool,
|
||||||
|
# auto_own, virtual). For now this file is intentionally minimal —
|
||||||
|
# the plumbing exists, the schema doesn't.
|
||||||
|
#
|
||||||
|
# Read-only at runtime; the binary does not write to its embedded
|
||||||
|
# copy. To export an editable copy for an operator:
|
||||||
|
#
|
||||||
|
# zddc-server show-defaults > /var/lib/zddc/root/.zddc
|
||||||
|
#
|
||||||
|
# That places this file at the on-disk root, where the operator can
|
||||||
|
# edit it freely. The new file then takes the place of the embedded
|
||||||
|
# one (no double-counting — both contribute to the cascade, leaf wins).
|
||||||
|
|
||||||
|
title: "ZDDC"
|
||||||
|
|
||||||
|
# Phase 1: empty acl + empty admins, equivalent to "the embedded
|
||||||
|
# layer grants nothing; rules come from on-disk .zddc files above".
|
||||||
|
# This preserves bit-identical behaviour for existing deployments.
|
||||||
|
acl:
|
||||||
|
permissions: {}
|
||||||
100
zddc/internal/zddc/defaults_test.go
Normal file
100
zddc/internal/zddc/defaults_test.go
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
package zddc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestEmbeddedDefaultsParse — the shipped defaults.zddc.yaml must
|
||||||
|
// parse cleanly into a ZddcFile. Regression guard against accidental
|
||||||
|
// YAML syntax errors in the source-of-truth file.
|
||||||
|
func TestEmbeddedDefaultsParse(t *testing.T) {
|
||||||
|
zf, err := EmbeddedDefaults()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("EmbeddedDefaults: %v", err)
|
||||||
|
}
|
||||||
|
if zf.Title == "" {
|
||||||
|
t.Errorf("embedded defaults have no title")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestEmbeddedDefaultsBytesDumpable — the bytes used by the show-
|
||||||
|
// defaults CLI must be non-empty and start with a comment so an
|
||||||
|
// operator pasting them into a real file sees the header.
|
||||||
|
func TestEmbeddedDefaultsBytesDumpable(t *testing.T) {
|
||||||
|
got := EmbeddedDefaultsBytes()
|
||||||
|
if len(got) == 0 {
|
||||||
|
t.Fatal("EmbeddedDefaultsBytes returned empty slice")
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(strings.TrimLeft(string(got), " \t"), "#") {
|
||||||
|
t.Errorf("expected leading comment, got: %q", string(got[:60]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCascadeIncludesEmbeddedByDefault — a fresh deployment with no
|
||||||
|
// on-disk .zddc still gets the embedded defaults reachable via
|
||||||
|
// chain.Embedded.
|
||||||
|
func TestCascadeIncludesEmbeddedByDefault(t *testing.T) {
|
||||||
|
resetCache()
|
||||||
|
root := t.TempDir()
|
||||||
|
leaf := filepath.Join(root, "Proj")
|
||||||
|
if err := mkdirAll(leaf); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
chain, err := EffectivePolicy(root, leaf)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if chain.Embedded.Title == "" {
|
||||||
|
t.Errorf("chain.Embedded.Title empty, want defaults title to populate")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCascadeInheritFalseDropsEmbedded — when an on-disk .zddc sets
|
||||||
|
// top-level `inherit: false`, the embedded layer is zeroed out.
|
||||||
|
func TestCascadeInheritFalseDropsEmbedded(t *testing.T) {
|
||||||
|
resetCache()
|
||||||
|
root := t.TempDir()
|
||||||
|
writeZddc(t, root, "title: 'op-managed'\ninherit: false\n")
|
||||||
|
leaf := filepath.Join(root, "Proj")
|
||||||
|
if err := mkdirAll(leaf); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
chain, err := EffectivePolicy(root, leaf)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if chain.Embedded.Title != "" {
|
||||||
|
t.Errorf("chain.Embedded.Title = %q, want empty (inherit:false should drop embedded)",
|
||||||
|
chain.Embedded.Title)
|
||||||
|
}
|
||||||
|
// On-disk level still present.
|
||||||
|
if got := chain.Levels[0].Title; got != "op-managed" {
|
||||||
|
t.Errorf("Levels[0].Title = %q, want %q", got, "op-managed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCascadeInheritTrueExplicitKeepsEmbedded — `inherit: true`
|
||||||
|
// explicitly is the same as omitting it (default behaviour).
|
||||||
|
func TestCascadeInheritTrueExplicitKeepsEmbedded(t *testing.T) {
|
||||||
|
resetCache()
|
||||||
|
root := t.TempDir()
|
||||||
|
writeZddc(t, root, "title: 'op-managed'\ninherit: true\n")
|
||||||
|
leaf := filepath.Join(root, "Proj")
|
||||||
|
if err := mkdirAll(leaf); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
chain, err := EffectivePolicy(root, leaf)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if chain.Embedded.Title == "" {
|
||||||
|
t.Errorf("chain.Embedded.Title empty, want defaults to remain since inherit: true is the default")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mkdirAll(p string) error {
|
||||||
|
return os.MkdirAll(p, 0o755)
|
||||||
|
}
|
||||||
|
|
@ -154,6 +154,22 @@ type ZddcFile struct {
|
||||||
// it. The auto-generated .zddc grants the creator's email directly via
|
// it. The auto-generated .zddc grants the creator's email directly via
|
||||||
// ACL.Permissions, the same way operators grant access to anyone else.
|
// ACL.Permissions, the same way operators grant access to anyone else.
|
||||||
CreatedBy string `yaml:"created_by,omitempty" json:"created_by,omitempty"`
|
CreatedBy string `yaml:"created_by,omitempty" json:"created_by,omitempty"`
|
||||||
|
|
||||||
|
// Inherit controls whether the cascade walker descends below this
|
||||||
|
// .zddc into lower (ancestor) levels — including the embedded
|
||||||
|
// defaults that sit at the bottom. Defaults to true (cascade walks
|
||||||
|
// to the root + embedded layer).
|
||||||
|
//
|
||||||
|
// Set to false on a .zddc to stop the descent: that level becomes
|
||||||
|
// the bottom of the effective cascade. Use this at the on-disk
|
||||||
|
// root /.zddc to fully ignore the embedded defaults (the operator
|
||||||
|
// takes full responsibility for spelling out every rule from
|
||||||
|
// scratch). Useful at deeper levels too — e.g. a sandbox subtree
|
||||||
|
// that wants none of the project's policy.
|
||||||
|
//
|
||||||
|
// Pointer so an unset value (nil) is distinguishable from explicit
|
||||||
|
// false. nil == defaults to true.
|
||||||
|
Inherit *bool `yaml:"inherit,omitempty" json:"inherit,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseFile reads and parses a .zddc YAML file.
|
// ParseFile reads and parses a .zddc YAML file.
|
||||||
|
|
@ -166,7 +182,15 @@ func ParseFile(path string) (ZddcFile, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ZddcFile{}, err
|
return ZddcFile{}, err
|
||||||
}
|
}
|
||||||
|
return parseBytes(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseBytes is the shared YAML→ZddcFile path used by ParseFile and
|
||||||
|
// EmbeddedDefaults. Returns a zero ZddcFile if data is empty.
|
||||||
|
func parseBytes(data []byte) (ZddcFile, error) {
|
||||||
|
if len(data) == 0 {
|
||||||
|
return ZddcFile{}, nil
|
||||||
|
}
|
||||||
var zf ZddcFile
|
var zf ZddcFile
|
||||||
if err := yaml.Unmarshal(data, &zf); err != nil {
|
if err := yaml.Unmarshal(data, &zf); err != nil {
|
||||||
return ZddcFile{}, err
|
return ZddcFile{}, err
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue