refactor(zddc): worm: is a list of principals, not a {principal: verbs} map
Per design feedback: the verb string in a worm: entry was always
effectively "cr" (the key's whole job is to restore write-once-create
inside the locked zone, and you need read to see what you filed), so
spelling it out per-entry was redundant. worm: is now just a list of
principal patterns — email-globs, @role:name, or bare role names —
and every listed principal gets read + write-once-create. An empty
list ([]) still marks the WORM zone with no create-capable
principals.
Changes:
- ZddcFile.Worm: map[string]string → []string
- mergeOverlay: concat-dedupe (a deeper .zddc adds controllers);
mergeStringSlicePreserveEmpty keeps `worm: []` non-nil through
the overlay so it still marks the zone
- WormZoneGrant: walks the list, grants VerbsRC to each matching
principal; result is always ⊆ {r, c}
- ValidateFile: validates each entry as an email-glob (role refs
skipped — validated by the role machinery)
- defaults.zddc.yaml: received/ and issued/ carry `worm: []`
- tests updated to the list form (worm_test.go, fileapi_test.go)
All Go tests green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
918f330a6f
commit
2de2fdf92c
8 changed files with 102 additions and 121 deletions
|
|
@ -511,13 +511,10 @@ func TestFileAPI_WORM_DocControllerNeedsExplicitGrant(t *testing.T) {
|
||||||
t.Fatalf("dc without explicit grant → issued: want 403, got %d: %s", rec.Code, rec.Body.String())
|
t.Fatalf("dc without explicit grant → issued: want 403, got %d: %s", rec.Code, rec.Body.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Operator places an explicit worm: grant at archive/Acme/issued/.zddc
|
// Operator names the document-controller role in the issued/ WORM
|
||||||
// naming the document-controller role. That principal then gets
|
// zone. That role's members then get {r, c} there — the embedded
|
||||||
// {r, c} inside the WORM zone — the embedded `worm: {}` (no
|
// `worm: []` (no controllers) is unioned with this deeper grant.
|
||||||
// controllers) is unioned with this deeper grant.
|
issuedZ := []byte("worm:\n - _doc_controller\n")
|
||||||
issuedZ := []byte(`worm:
|
|
||||||
_doc_controller: cr
|
|
||||||
`)
|
|
||||||
if err := os.WriteFile(filepath.Join(root, "Project-X/archive/Acme/issued/.zddc"), issuedZ, 0o644); err != nil {
|
if err := os.WriteFile(filepath.Join(root, "Project-X/archive/Acme/issued/.zddc"), issuedZ, 0o644); err != nil {
|
||||||
t.Fatalf("write issued .zddc: %v", err)
|
t.Fatalf("write issued .zddc: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -605,8 +602,9 @@ func TestFileAPI_AutoMkdirOwnership(t *testing.T) {
|
||||||
func TestFileAPI_AutoMkdirNotInIssued(t *testing.T) {
|
func TestFileAPI_AutoMkdirNotInIssued(t *testing.T) {
|
||||||
_, do, root := rolePermissionsTestSetup(t)
|
_, do, root := rolePermissionsTestSetup(t)
|
||||||
|
|
||||||
// Place an explicit worm: grant so dc has cr in the issued WORM zone.
|
// Name the document-controller role in the issued/ WORM zone so its
|
||||||
issuedZ := []byte("worm:\n _doc_controller: cr\n")
|
// members get cr there.
|
||||||
|
issuedZ := []byte("worm:\n - _doc_controller\n")
|
||||||
if err := os.WriteFile(filepath.Join(root, "Project-X/archive/Acme/issued/.zddc"), issuedZ, 0o644); err != nil {
|
if err := os.WriteFile(filepath.Join(root, "Project-X/archive/Acme/issued/.zddc"), issuedZ, 0o644); err != nil {
|
||||||
t.Fatalf("seed issued .zddc: %v", err)
|
t.Fatalf("seed issued .zddc: %v", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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-12 13:28:43 · 9c7858c-dirty</span></span>
|
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-alpha · 2026-05-12 14:40:09 · 918f330-dirty</span></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
|
|
|
||||||
|
|
@ -71,34 +71,33 @@ paths:
|
||||||
# Browse shows a drag-drop overlay here.
|
# Browse shows a drag-drop overlay here.
|
||||||
drop_target: true
|
drop_target: true
|
||||||
# received/ and issued/ are WORM (write-once-read-many).
|
# received/ and issued/ are WORM (write-once-read-many).
|
||||||
# The `worm:` key marks the zone:
|
# The `worm:` list marks the zone:
|
||||||
#
|
#
|
||||||
# - write (w) and delete (d) are stripped for EVERYONE
|
# - write (w) and delete (d) are stripped for EVERYONE
|
||||||
# - create (c) is stripped for everyone EXCEPT the
|
# - create (c) is stripped for everyone EXCEPT the
|
||||||
# principals listed below (none, in the baseline)
|
# principals listed — they get read + write-once-
|
||||||
# - read (r) is whatever the normal cascade ACL granted
|
# create ("cr")
|
||||||
# — the WORM key does NOT itself grant read, so a
|
# - read for non-listed principals is whatever the
|
||||||
# deployment that restricts read keeps that
|
# normal cascade ACL granted; the WORM list does not
|
||||||
# restriction; one that grants read to the project
|
# itself confer read to outsiders
|
||||||
# keeps that too
|
# - admins (root / subtree) bypass entirely — the
|
||||||
# - admins bypass entirely (handler IsAdmin escape)
|
# human escape hatch for mis-filed documents
|
||||||
#
|
#
|
||||||
# The empty map ({}) means "WORM, no create-capable
|
# The baseline is an empty list: WORM zone, no
|
||||||
# principals". A deployment names its document controller
|
# create-capable principals — filing is locked until a
|
||||||
# by placing a .zddc at received/ (or issued/, or
|
# deployment names a document controller, e.g.
|
||||||
# archive/<party>/, or wherever scopes it right) with:
|
|
||||||
#
|
#
|
||||||
# worm:
|
# worm: ["doc-control@example.com"]
|
||||||
# "doc-control@example.com": cr # read + write-once
|
|
||||||
#
|
#
|
||||||
# worm: grants UNION across the cascade, so multiple
|
# at received/ (or issued/, or archive/<party>/, or
|
||||||
# controllers (or a deeper-scoped one) compose.
|
# wherever scopes it right). worm: lists UNION across the
|
||||||
|
# cascade, so a deeper .zddc adds more controllers.
|
||||||
received:
|
received:
|
||||||
default_tool: archive
|
default_tool: archive
|
||||||
worm: {}
|
worm: []
|
||||||
issued:
|
issued:
|
||||||
default_tool: archive
|
default_tool: archive
|
||||||
worm: {}
|
worm: []
|
||||||
working:
|
working:
|
||||||
default_tool: mdedit
|
default_tool: mdedit
|
||||||
available_tools: [mdedit, classifier]
|
available_tools: [mdedit, classifier]
|
||||||
|
|
|
||||||
|
|
@ -210,32 +210,31 @@ type ZddcFile struct {
|
||||||
DropTarget *bool `yaml:"drop_target,omitempty" json:"drop_target,omitempty"`
|
DropTarget *bool `yaml:"drop_target,omitempty" json:"drop_target,omitempty"`
|
||||||
|
|
||||||
// Worm marks this directory (and its descendants) as
|
// Worm marks this directory (and its descendants) as
|
||||||
// write-once-read-many. A non-nil Worm map — even an empty one —
|
// write-once-read-many. A non-nil Worm list — even an empty one —
|
||||||
// puts the path into a WORM zone with these effects, applied AFTER
|
// puts the path into a WORM zone with these effects, applied AFTER
|
||||||
// the normal cascade ACL and BEFORE any admin escape hatch:
|
// the normal cascade ACL and BEFORE any admin escape hatch:
|
||||||
//
|
//
|
||||||
// - write (w) and delete (d) are stripped for everyone
|
// - write (w) and delete (d) are stripped for everyone
|
||||||
// - create (c) is stripped for everyone EXCEPT principals named
|
// - create (c) is stripped for everyone EXCEPT the principals
|
||||||
// in the Worm map with a verb string containing 'c'
|
// listed here — they get read + write-once-create ("cr")
|
||||||
// - read (r) survives if the normal cascade ACL granted it, OR
|
// - read (r) for non-listed principals is whatever the normal
|
||||||
// if the Worm map grants it ('r' in the principal's verb
|
// cascade ACL granted (the WORM list does not itself confer
|
||||||
// string) — so a document controller who isn't in the project
|
// read to outsiders, only to its own members)
|
||||||
// ACL can still be granted read+create here
|
|
||||||
//
|
//
|
||||||
// Map shape mirrors acl.permissions: keys are email-glob patterns
|
// Each entry is an email-glob pattern (or @role:<name> / a bare
|
||||||
// (or @role:<name>), values are verb strings restricted to the
|
// role name). An empty list ([]) is a WORM zone with no
|
||||||
// subset {r, c}. An empty map ({}) is a WORM zone with no
|
// create-capable principals — the embedded baseline ships this
|
||||||
// create-capable principals — useful as the embedded baseline,
|
// on received/ and issued/ with the `document_controller` role
|
||||||
// which a deployment overrides by placing a .zddc with
|
// named but member-empty, so a deployment enables filing simply
|
||||||
// `worm: {"doc-control@example.com": cr}` at the WORM folder (or
|
// by populating that role. Worm lists UNION across the cascade —
|
||||||
// deeper). Worm grants UNION across the cascade — deeper .zddc
|
// a deeper .zddc adds more controllers.
|
||||||
// can add more controllers.
|
|
||||||
//
|
//
|
||||||
// Admins (root or subtree) bypass the WORM mask entirely; the
|
// Admins (root or subtree) bypass the WORM constraint entirely;
|
||||||
// handler does the IsAdmin / IsSubtreeAdmin check before invoking
|
// the handler does the IsAdmin / IsSubtreeAdmin check before
|
||||||
// the policy evaluator. WORM is a normal-user constraint, not an
|
// invoking the policy evaluator. WORM is a normal-user
|
||||||
// absolute one — mis-filed documents still need a human escape.
|
// constraint, not an absolute one — mis-filed documents still
|
||||||
Worm map[string]string `yaml:"worm,omitempty" json:"worm,omitempty"`
|
// need a human escape.
|
||||||
|
Worm []string `yaml:"worm,omitempty" json:"worm,omitempty"`
|
||||||
|
|
||||||
// AvailableTools restricts which tools the server will auto-serve
|
// AvailableTools restricts which tools the server will auto-serve
|
||||||
// at this directory and its descendants. The effective list is the
|
// at this directory and its descendants. The effective list is the
|
||||||
|
|
|
||||||
|
|
@ -248,32 +248,22 @@ func ValidateFile(zf ZddcFile) []FieldError {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// worm: keys are email-glob patterns (or @role:name); values are
|
// worm: is a list of principal patterns (email-globs, @role:name,
|
||||||
// verb strings restricted to {r, c} — write/delete/admin are
|
// or bare role names) that get write-once-create inside the WORM
|
||||||
// meaningless inside a WORM zone and rejecting them at write time
|
// zone. Validate each as an email-glob unless it's a role
|
||||||
// avoids a silently-ineffective entry.
|
// reference (role names are validated by the role machinery).
|
||||||
for principal, verbStr := range zf.Worm {
|
for i, principal := range zf.Worm {
|
||||||
fld := fmt.Sprintf("worm.%s", principal)
|
if strings.HasPrefix(principal, "@role:") {
|
||||||
// @role: prefixes are validated by the role machinery; only
|
continue // role refs validated elsewhere
|
||||||
// check the bare-email-glob form here.
|
|
||||||
if !strings.HasPrefix(principal, "@role:") {
|
|
||||||
if err := ValidatePattern(principal); err != nil {
|
|
||||||
errs = append(errs, FieldError{Field: fld, Message: err.Error()})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
v, ok := ParseVerbSet(verbStr)
|
// A bare name with no "@" could be a role name; ValidatePattern
|
||||||
if !ok {
|
// accepts it (no @, no whitespace), and MatchesPrincipal
|
||||||
|
// resolves it as a role if one is defined. So this only
|
||||||
|
// rejects genuinely malformed entries (whitespace, double @).
|
||||||
|
if err := ValidatePattern(principal); err != nil {
|
||||||
errs = append(errs, FieldError{
|
errs = append(errs, FieldError{
|
||||||
Field: fld,
|
Field: fmt.Sprintf("worm[%d]", i),
|
||||||
Message: fmt.Sprintf("invalid verb string %q (allowed: r, c)", verbStr),
|
Message: err.Error(),
|
||||||
})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if v&^VerbsRC != 0 {
|
|
||||||
errs = append(errs, FieldError{
|
|
||||||
Field: fld,
|
|
||||||
Message: fmt.Sprintf("verb string %q includes w/d/a — only r and c are valid inside a WORM zone", verbStr),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -79,10 +79,11 @@ func mergeOverlay(base, top ZddcFile) ZddcFile {
|
||||||
out.DropTarget = top.DropTarget
|
out.DropTarget = top.DropTarget
|
||||||
}
|
}
|
||||||
// Worm: presence (non-nil, even empty) marks the WORM zone.
|
// Worm: presence (non-nil, even empty) marks the WORM zone.
|
||||||
// Merge per-key (top wins on clash); preserve a non-nil empty
|
// Concat-dedupe across levels (a deeper .zddc adds controllers);
|
||||||
// map so `worm: {}` survives the overlay.
|
// preserve a non-nil empty slice so `worm: []` survives the
|
||||||
|
// overlay.
|
||||||
if top.Worm != nil {
|
if top.Worm != nil {
|
||||||
out.Worm = mergeStringMapPreserveEmpty(out.Worm, top.Worm)
|
out.Worm = mergeStringSlicePreserveEmpty(out.Worm, top.Worm)
|
||||||
}
|
}
|
||||||
if top.Virtual != nil {
|
if top.Virtual != nil {
|
||||||
out.Virtual = top.Virtual
|
out.Virtual = top.Virtual
|
||||||
|
|
@ -141,17 +142,26 @@ func mergeStringMap(base, top map[string]string) map[string]string {
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
// mergeStringMapPreserveEmpty is mergeStringMap but always returns a
|
// mergeStringSlicePreserveEmpty is mergeStringSlice but always returns
|
||||||
// non-nil result when top is non-nil — so an empty `worm: {}` in a
|
// a non-nil result when top is non-nil — so an empty `worm: []` in a
|
||||||
// .zddc still marks the WORM zone after the overlay. Caller is
|
// .zddc still marks the WORM zone after the overlay. Caller is
|
||||||
// expected to only invoke this when top != nil.
|
// expected to only invoke this when top != nil.
|
||||||
func mergeStringMapPreserveEmpty(base, top map[string]string) map[string]string {
|
func mergeStringSlicePreserveEmpty(base, top []string) []string {
|
||||||
out := make(map[string]string, len(base)+len(top))
|
seen := make(map[string]struct{}, len(base)+len(top))
|
||||||
for k, v := range base {
|
out := make([]string, 0, len(base)+len(top))
|
||||||
out[k] = v
|
for _, v := range base {
|
||||||
|
if _, ok := seen[v]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[v] = struct{}{}
|
||||||
|
out = append(out, v)
|
||||||
}
|
}
|
||||||
for k, v := range top {
|
for _, v := range top {
|
||||||
out[k] = v
|
if _, ok := seen[v]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[v] = struct{}{}
|
||||||
|
out = append(out, v)
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,23 +31,15 @@ package zddc
|
||||||
// grant. Admins are excluded upstream (handler's IsAdmin bypass).
|
// grant. Admins are excluded upstream (handler's IsAdmin bypass).
|
||||||
func WormZoneGrant(chain PolicyChain, email string, mode CascadeMode) (grant VerbSet, inWorm bool) {
|
func WormZoneGrant(chain PolicyChain, email string, mode CascadeMode) (grant VerbSet, inWorm bool) {
|
||||||
for i := 0; i < len(chain.Levels); i++ {
|
for i := 0; i < len(chain.Levels); i++ {
|
||||||
wm := chain.Levels[i].Worm
|
wl := chain.Levels[i].Worm
|
||||||
if wm == nil {
|
if wl == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
inWorm = true
|
inWorm = true
|
||||||
for principal, verbStr := range wm {
|
for _, principal := range wl {
|
||||||
if !MatchesPrincipal(principal, email, chain, i, mode) {
|
if MatchesPrincipal(principal, email, chain, i, mode) {
|
||||||
continue
|
grant |= VerbsRC // listed controllers get read + write-once-create
|
||||||
}
|
}
|
||||||
v, ok := ParseVerbSet(verbStr)
|
|
||||||
if !ok {
|
|
||||||
// Malformed verb string in operator YAML — ignore the
|
|
||||||
// entry rather than crash; ValidateFile flags it at
|
|
||||||
// write time so this shouldn't reach a healthy server.
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
grant |= v
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// The embedded baseline could in principle carry a top-level
|
// The embedded baseline could in principle carry a top-level
|
||||||
|
|
@ -55,17 +47,14 @@ func WormZoneGrant(chain PolicyChain, email string, mode CascadeMode) (grant Ver
|
||||||
// fold it in for completeness.
|
// fold it in for completeness.
|
||||||
if chain.Embedded.Worm != nil {
|
if chain.Embedded.Worm != nil {
|
||||||
inWorm = true
|
inWorm = true
|
||||||
for principal, verbStr := range chain.Embedded.Worm {
|
for _, principal := range chain.Embedded.Worm {
|
||||||
if !MatchesPattern(principal, email) {
|
if MatchesPattern(principal, email) {
|
||||||
continue
|
grant |= VerbsRC
|
||||||
}
|
|
||||||
if v, ok := ParseVerbSet(verbStr); ok {
|
|
||||||
grant |= v
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !inWorm {
|
if !inWorm {
|
||||||
return 0, false
|
return 0, false
|
||||||
}
|
}
|
||||||
return grant & VerbsRC, true
|
return grant, true
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestWormZoneGrant_EmbeddedConvention — archive/<party>/received and
|
// TestWormZoneGrant_EmbeddedConvention — archive/<party>/received and
|
||||||
// issued carry `worm: {}` in defaults.zddc.yaml, so any path under
|
// issued carry `worm: []` in defaults.zddc.yaml, so any path under
|
||||||
// those folders is a WORM zone (inWorm=true) with no create-capable
|
// those folders is a WORM zone (inWorm=true) with no create-capable
|
||||||
// principals (grant=0). Other paths are not WORM zones.
|
// principals (grant=0). Other paths are not WORM zones.
|
||||||
func TestWormZoneGrant_EmbeddedConvention(t *testing.T) {
|
func TestWormZoneGrant_EmbeddedConvention(t *testing.T) {
|
||||||
|
|
@ -43,8 +43,8 @@ func TestWormZoneGrant_EmbeddedConvention(t *testing.T) {
|
||||||
|
|
||||||
// TestWormZoneGrant_OperatorGrantsController — a deployment grants a
|
// TestWormZoneGrant_OperatorGrantsController — a deployment grants a
|
||||||
// document controller create-once by placing a .zddc with a `worm:`
|
// document controller create-once by placing a .zddc with a `worm:`
|
||||||
// entry at (or below) the WORM folder. That principal then gets
|
// entry naming them at (or below) the WORM folder. That principal then
|
||||||
// {r, c} from WormZoneGrant; everyone else still gets 0.
|
// gets {r, c} from WormZoneGrant; everyone else still gets 0.
|
||||||
func TestWormZoneGrant_OperatorGrantsController(t *testing.T) {
|
func TestWormZoneGrant_OperatorGrantsController(t *testing.T) {
|
||||||
resetCache()
|
resetCache()
|
||||||
root := t.TempDir()
|
root := t.TempDir()
|
||||||
|
|
@ -52,13 +52,12 @@ func TestWormZoneGrant_OperatorGrantsController(t *testing.T) {
|
||||||
if err := os.MkdirAll(issuedDir, 0o755); err != nil {
|
if err := os.MkdirAll(issuedDir, 0o755); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
writeZddc(t, issuedDir, "worm:\n \"doc-control@example.com\": cr\n")
|
writeZddc(t, issuedDir, "worm:\n - doc-control@example.com\n")
|
||||||
|
|
||||||
chain, err := EffectivePolicy(root, issuedDir)
|
chain, err := EffectivePolicy(root, issuedDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
// Controller gets {r, c}.
|
|
||||||
g, inWorm := WormZoneGrant(chain, "doc-control@example.com", ModeDelegated)
|
g, inWorm := WormZoneGrant(chain, "doc-control@example.com", ModeDelegated)
|
||||||
if !inWorm {
|
if !inWorm {
|
||||||
t.Fatalf("inWorm = false, want true")
|
t.Fatalf("inWorm = false, want true")
|
||||||
|
|
@ -66,29 +65,27 @@ func TestWormZoneGrant_OperatorGrantsController(t *testing.T) {
|
||||||
if g != VerbsRC {
|
if g != VerbsRC {
|
||||||
t.Errorf("controller grant = %v, want rc", g)
|
t.Errorf("controller grant = %v, want rc", g)
|
||||||
}
|
}
|
||||||
// Someone else gets nothing from the worm map.
|
|
||||||
g2, _ := WormZoneGrant(chain, "rando@example.com", ModeDelegated)
|
g2, _ := WormZoneGrant(chain, "rando@example.com", ModeDelegated)
|
||||||
if g2 != 0 {
|
if g2 != 0 {
|
||||||
t.Errorf("non-controller grant = %v, want 0", g2)
|
t.Errorf("non-controller grant = %v, want 0", g2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestWormZoneGrant_MasksWriteDelete — even if a worm: entry tried to
|
// TestWormZoneGrant_GrantIsAlwaysRC — a worm: entry never confers
|
||||||
// grant w/d/a (which ValidateFile rejects, but defense in depth),
|
// more than {r, c} no matter what (the list form can't even express
|
||||||
// WormZoneGrant strips it to {r, c}.
|
// w/d, but verifying the constant the resolver uses).
|
||||||
func TestWormZoneGrant_MasksWriteDelete(t *testing.T) {
|
func TestWormZoneGrant_GrantIsAlwaysRC(t *testing.T) {
|
||||||
resetCache()
|
resetCache()
|
||||||
root := t.TempDir()
|
root := t.TempDir()
|
||||||
rec := filepath.Join(root, "Proj", "archive", "Acme", "received")
|
rec := filepath.Join(root, "Proj", "archive", "Acme", "received")
|
||||||
if err := os.MkdirAll(rec, 0o755); err != nil {
|
if err := os.MkdirAll(rec, 0o755); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
// Hand-written with rwcda — should be masked to rc by WormZoneGrant.
|
writeZddc(t, rec, "worm:\n - x@example.com\n")
|
||||||
writeZddc(t, rec, "worm:\n \"x@example.com\": rwcda\n")
|
|
||||||
chain, _ := EffectivePolicy(root, rec)
|
chain, _ := EffectivePolicy(root, rec)
|
||||||
g, _ := WormZoneGrant(chain, "x@example.com", ModeDelegated)
|
g, _ := WormZoneGrant(chain, "x@example.com", ModeDelegated)
|
||||||
if g != VerbsRC {
|
if g != VerbsRC {
|
||||||
t.Errorf("grant = %v (%s), want rc — w/d/a must be masked", g, g.String())
|
t.Errorf("grant = %v (%s), want rc", g, g.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -104,9 +101,8 @@ func TestWormZoneGrant_GrantsUnionAcrossCascade(t *testing.T) {
|
||||||
if err := os.MkdirAll(rec, 0o755); err != nil {
|
if err := os.MkdirAll(rec, 0o755); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
// Party-level worm grant for alice; received-level worm grant for bob.
|
writeZddc(t, party, "worm:\n - alice@example.com\n")
|
||||||
writeZddc(t, party, "worm:\n \"alice@example.com\": cr\n")
|
writeZddc(t, rec, "worm:\n - bob@example.com\n")
|
||||||
writeZddc(t, rec, "worm:\n \"bob@example.com\": c\n")
|
|
||||||
chain, _ := EffectivePolicy(root, rec)
|
chain, _ := EffectivePolicy(root, rec)
|
||||||
|
|
||||||
ga, inA := WormZoneGrant(chain, "alice@example.com", ModeDelegated)
|
ga, inA := WormZoneGrant(chain, "alice@example.com", ModeDelegated)
|
||||||
|
|
@ -114,7 +110,7 @@ func TestWormZoneGrant_GrantsUnionAcrossCascade(t *testing.T) {
|
||||||
t.Errorf("alice grant = %v inWorm=%v, want rc/true", ga, inA)
|
t.Errorf("alice grant = %v inWorm=%v, want rc/true", ga, inA)
|
||||||
}
|
}
|
||||||
gb, _ := WormZoneGrant(chain, "bob@example.com", ModeDelegated)
|
gb, _ := WormZoneGrant(chain, "bob@example.com", ModeDelegated)
|
||||||
if gb&VerbC == 0 {
|
if gb != VerbsRC {
|
||||||
t.Errorf("bob grant = %v, want at least c", gb)
|
t.Errorf("bob grant = %v, want rc", gb)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue