feat(zddc): incoming/ is a controlled drop zone — project_team read-only, doc controller QCs
Clarify the incoming/ semantics per the workflow: it's the
counterparty's drop zone, not a free-for-all.
- project_team gets read only here (inherited from the project
level — they have no c/w, so they can see what's been dropped
but not touch it). No change in effect; documented explicitly.
- document_controller gets rwcd here (restated at the incoming/
cascade level). The QC + transfer workflow — classifier renames
files in place (w), then they move to received/ (delete here +
worm-create there) — needs the delete bit, which the inherited
project-level `rw` lacked.
- The counterparty's uploader still gets access via a deployment
.zddc (acl: { permissions: { "*@acme.com": cr } } at
archive/Acme/incoming/.zddc) or by mkdir'ing a dated subfolder
under incoming/ and owning it via the existing auto_own — both
flows unchanged.
Test: standardroles_test now asserts the doc controller has rwcd at
incoming/ and a project_team member has only r there.
All Go + Playwright tests green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
54dff4dcd3
commit
9aa587aac0
3 changed files with 29 additions and 4 deletions
|
|
@ -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 15:16:28 · 2de2fdf-dirty</span></span>
|
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-alpha · 2026-05-12 15:29:24 · 54dff4d-dirty</span></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
|
|
|
||||||
|
|
@ -119,13 +119,30 @@ paths:
|
||||||
# spec even when the on-disk folder doesn't exist.
|
# spec even when the on-disk folder doesn't exist.
|
||||||
virtual: true
|
virtual: true
|
||||||
incoming:
|
incoming:
|
||||||
|
# incoming/ is the COUNTERPARTY's drop zone. The flow:
|
||||||
|
# 1. the other party's document controller uploads
|
||||||
|
# files here (a deployment grants them cr at this
|
||||||
|
# path, e.g. acl: { permissions: { "*@acme.com": cr } }
|
||||||
|
# at archive/Acme/incoming/.zddc — or they mkdir a
|
||||||
|
# dated subfolder under incoming/ and own it via
|
||||||
|
# auto_own)
|
||||||
|
# 2. OUR document controller QCs them via classifier
|
||||||
|
# (rename in place) and moves them to received/
|
||||||
|
# (which needs delete here + worm-create there),
|
||||||
|
# ideally returning a signed transmittal in issued/
|
||||||
|
#
|
||||||
|
# The normal project_team has only read here (inherited
|
||||||
|
# from the project level — they have no c/w) so they can
|
||||||
|
# see what's been dropped but not touch it. The
|
||||||
|
# document_controller grant restates rwcd so the QC +
|
||||||
|
# transfer-out workflow has the delete bit it needs.
|
||||||
default_tool: classifier
|
default_tool: classifier
|
||||||
available_tools: [classifier]
|
available_tools: [classifier]
|
||||||
# First write into incoming/ auto-creates an owner
|
|
||||||
# grant so the creator can manage their own drops.
|
|
||||||
auto_own: true
|
auto_own: true
|
||||||
# Browse shows a drag-drop overlay here.
|
|
||||||
drop_target: true
|
drop_target: true
|
||||||
|
acl:
|
||||||
|
permissions:
|
||||||
|
document_controller: rwcd
|
||||||
# received/ and issued/ are WORM (write-once-read-many).
|
# received/ and issued/ are WORM (write-once-read-many).
|
||||||
# The `worm:` list marks the zone:
|
# The `worm:` list marks the zone:
|
||||||
#
|
#
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,8 @@ func TestStandardRoles_DocControllerScopedCreate(t *testing.T) {
|
||||||
mustVerbs(filepath.Join(root, "Proj", "random-folder"), "rw")
|
mustVerbs(filepath.Join(root, "Proj", "random-folder"), "rw")
|
||||||
// archive/: rwc (can create party folders).
|
// archive/: rwc (can create party folders).
|
||||||
mustVerbs(filepath.Join(root, "Proj", "archive"), "rwc")
|
mustVerbs(filepath.Join(root, "Proj", "archive"), "rwc")
|
||||||
|
// incoming/: rwcd — the QC + transfer-out workflow needs delete.
|
||||||
|
mustVerbs(filepath.Join(root, "Proj", "archive", "Acme", "incoming"), "rwcd")
|
||||||
// received/ (WORM): rw masked to r, plus worm-restored c → "rc".
|
// received/ (WORM): rw masked to r, plus worm-restored c → "rc".
|
||||||
mustVerbs(filepath.Join(root, "Proj", "archive", "Acme", "received"), "rc")
|
mustVerbs(filepath.Join(root, "Proj", "archive", "Acme", "received"), "rc")
|
||||||
mustVerbs(filepath.Join(root, "Proj", "archive", "Acme", "issued"), "rc")
|
mustVerbs(filepath.Join(root, "Proj", "archive", "Acme", "issued"), "rc")
|
||||||
|
|
@ -117,4 +119,10 @@ created_by: alice@example.com
|
||||||
if got := EffectiveVerbs(chain2, alice, ModeDelegated); got.String() != "r" {
|
if got := EffectiveVerbs(chain2, alice, ModeDelegated); got.String() != "r" {
|
||||||
t.Errorf("alice in archive/Acme = %q, want r", got.String())
|
t.Errorf("alice in archive/Acme = %q, want r", got.String())
|
||||||
}
|
}
|
||||||
|
// Alice CANNOT write to incoming/ — that's the counterparty's drop
|
||||||
|
// zone, QC'd by the document controller. project_team gets read only.
|
||||||
|
chain3, _ := EffectivePolicy(root, filepath.Join(root, "Proj", "archive", "Acme", "incoming"))
|
||||||
|
if got := EffectiveVerbs(chain3, alice, ModeDelegated); got.String() != "r" {
|
||||||
|
t.Errorf("alice in incoming/ = %q, want r (no create/write for project_team)", got.String())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue