Compare commits

..

No commits in common. "d4f35d9927e61ad87eb70f0d10bcd6194578f118" and "9cec42336155b6e88c1af30ee18bd0a825b5a309" have entirely different histories.

9 changed files with 16 additions and 82 deletions

View file

@ -1160,24 +1160,10 @@ func dispatch(cfg config.Config, idx *archive.Index, ring *handler.LogRing, apps
requestDir := filepath.Join(cfg.Root, filepath.FromSlash(requestDirRel))
if apps.AppAvailableAt(cfg.Root, requestDir, app) {
chain, _ := zddc.EffectivePolicy(cfg.Root, requestDir)
// Root-path tool shells are public, mirroring the
// landing page + ServeDirectory's root bypass: the
// shell is a static app that carries no data, and the
// tool's own per-project/per-dir fetches are
// independently ACL-gated (fs.ListDirectory filters
// per entry). Gating the shell here would block the
// root-level multi-project archive/browse views for
// any caller without a root-level read grant — which
// no normal (per-project-scoped) user has. Non-root
// tool paths (e.g. /<project>/archive.html) keep the
// read gate so a project you can't read won't serve
// its tool there.
if requestDir != cfg.Root {
if allowed, _ := policy.AllowFromChainP(r.Context(), handler.DeciderFromContext(r), chain, handler.PrincipalFromContext(r), urlPath); !allowed {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
}
appsSrv.Serve(w, r, app, chain, requestDir)
return
}

View file

@ -216,58 +216,6 @@ func TestDispatchAppsResolution(t *testing.T) {
}
}
// TestDispatchRootAppShellPublicButDataGated locks in the root-path tool-shell
// bypass: a non-root, un-elevated user (no read grant anywhere at/under root)
// can GET /archive.html — the shell is a static app, served like the landing
// page — but the underlying data stays ACL-gated, so the same user is still
// Forbidden from reading a project directory they have no grant on. This is
// what makes the root-level multi-project archive (/archive.html?projects=A,B)
// usable by per-project-scoped users without admin elevation.
func TestDispatchRootAppShellPublicButDataGated(t *testing.T) {
root := t.TempDir()
// Root grants only alice; eve has no read grant at root or anywhere under it.
zf := zddc.ZddcFile{
ACL: zddc.ACLRules{Permissions: map[string]string{"alice@example.com": "rwcd"}},
}
if err := zddc.WriteFile(root, zf); err != nil {
t.Fatalf("WriteFile: %v", err)
}
mustMkdir(t, filepath.Join(root, "Project-A"))
idx, err := archive.BuildIndex(root)
if err != nil {
t.Fatalf("BuildIndex: %v", err)
}
cfg := config.Config{Root: root, IndexPath: ".archive", EmailHeader: "X-Auth-Request-Email"}
ring := handler.NewLogRing(10)
appsSrv, err := setupApps(cfg)
if err != nil {
t.Fatalf("setupApps: %v", err)
}
// eve: no grant anywhere, NOT elevated (ElevatedKey unset → false).
eveReq := func(method, path string) *http.Request {
req := httptest.NewRequest(method, path, nil)
return req.WithContext(context.WithValue(req.Context(), handler.EmailKey, "eve@example.com"))
}
// The shell at root is served regardless of a root read grant.
rec := httptest.NewRecorder()
dispatch(cfg, idx, ring, appsSrv, nil, rec, eveReq(http.MethodGet, "/archive.html"))
if rec.Code != http.StatusOK {
t.Fatalf("GET /archive.html as non-root user: status=%d, want 200 (root tool shell is public); body=%s",
rec.Code, rec.Body.String())
}
// ...but data is still ACL-gated: eve cannot read a project she has no grant on.
rec2 := httptest.NewRecorder()
dispatch(cfg, idx, ring, appsSrv, nil, rec2, eveReq(http.MethodGet, "/Project-A/"))
if rec2.Code != http.StatusForbidden {
t.Errorf("GET /Project-A/ as non-root user: status=%d, want 403 (data stays ACL-gated)", rec2.Code)
}
}
// silence "imported and not used" if apps not referenced elsewhere — keep
// import even when we trim test cases later.
var _ = apps.DefaultUpstream

View file

@ -2582,7 +2582,7 @@ td[data-field="trackingNumber"] {
</svg>
<div class="header-title-group">
<span class="app-header__title">ZDDC Archive</span>
<span class="build-timestamp">v0.0.23</span>
<span class="build-timestamp">v0.0.22</span>
</div>
<button id="addDirectoryBtn" class="btn btn-primary">Use Local Directory</button>
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh Data"></button>

View file

@ -2344,7 +2344,7 @@ body {
</svg>
<div class="header-title-group">
<span class="app-header__title">ZDDC Browse</span>
<span class="build-timestamp">v0.0.23</span>
<span class="build-timestamp">v0.0.22</span>
</div>
<button id="addDirectoryBtn" class="btn btn-primary">Use Local Directory</button>
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh listing" aria-label="Refresh listing"></button>

View file

@ -1793,7 +1793,7 @@ body.is-elevated::after {
</svg>
<div class="header-title-group">
<span class="app-header__title">ZDDC Classifier</span>
<span class="build-timestamp">v0.0.23</span>
<span class="build-timestamp">v0.0.22</span>
</div>
<button id="addDirectoryBtn" class="btn btn-primary">Use Local Directory</button>
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh and rescan directory" aria-label="Refresh" style="font-size:1.1rem;"></button>

View file

@ -1536,7 +1536,7 @@ body {
</svg>
<div class="header-title-group">
<span class="app-header__title">ZDDC</span>
<span class="build-timestamp">v0.0.23</span>
<span class="build-timestamp">v0.0.22</span>
</div>
</div>
<div class="header-right">

View file

@ -2635,7 +2635,7 @@ dialog.modal--narrow {
</svg>
<div class="header-title-group">
<span class="app-header__title">ZDDC Transmittal</span>
<span class="build-timestamp">v0.0.23</span>
<span class="build-timestamp">v0.0.22</span>
</div>
<span id="no-js-notice" class="text-gray-400 text-xs italic">JavaScript not available</span>
<!-- Publish split-button (Transmittal-specific primary action;

View file

@ -1,8 +1,8 @@
# Generated by build.sh — do not edit. One <app>=<build label> per line.
archive=v0.0.23
transmittal=v0.0.23
classifier=v0.0.23
landing=v0.0.23
form=v0.0.23
tables=v0.0.23
browse=v0.0.23
archive=v0.0.22
transmittal=v0.0.22
classifier=v0.0.22
landing=v0.0.22
form=v0.0.22
tables=v0.0.22
browse=v0.0.22

View file

@ -1534,7 +1534,7 @@ body.is-elevated::after {
</svg>
<div class="header-title-group">
<span class="app-header__title" id="table-title">ZDDC Table</span>
<span class="build-timestamp">v0.0.23</span>
<span class="build-timestamp">v0.0.22</span>
</div>
</div>
<div class="header-right">