Compare commits
No commits in common. "d4f35d9927e61ad87eb70f0d10bcd6194578f118" and "9cec42336155b6e88c1af30ee18bd0a825b5a309" have entirely different histories.
d4f35d9927
...
9cec423361
9 changed files with 16 additions and 82 deletions
|
|
@ -1160,23 +1160,9 @@ func dispatch(cfg config.Config, idx *archive.Index, ring *handler.LogRing, apps
|
||||||
requestDir := filepath.Join(cfg.Root, filepath.FromSlash(requestDirRel))
|
requestDir := filepath.Join(cfg.Root, filepath.FromSlash(requestDirRel))
|
||||||
if apps.AppAvailableAt(cfg.Root, requestDir, app) {
|
if apps.AppAvailableAt(cfg.Root, requestDir, app) {
|
||||||
chain, _ := zddc.EffectivePolicy(cfg.Root, requestDir)
|
chain, _ := zddc.EffectivePolicy(cfg.Root, requestDir)
|
||||||
// Root-path tool shells are public, mirroring the
|
if allowed, _ := policy.AllowFromChainP(r.Context(), handler.DeciderFromContext(r), chain, handler.PrincipalFromContext(r), urlPath); !allowed {
|
||||||
// landing page + ServeDirectory's root bypass: the
|
http.Error(w, "Forbidden", http.StatusForbidden)
|
||||||
// shell is a static app that carries no data, and the
|
return
|
||||||
// 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)
|
appsSrv.Serve(w, r, app, chain, requestDir)
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -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
|
// silence "imported and not used" if apps not referenced elsewhere — keep
|
||||||
// import even when we trim test cases later.
|
// import even when we trim test cases later.
|
||||||
var _ = apps.DefaultUpstream
|
var _ = apps.DefaultUpstream
|
||||||
|
|
|
||||||
|
|
@ -2582,7 +2582,7 @@ td[data-field="trackingNumber"] {
|
||||||
</svg>
|
</svg>
|
||||||
<div class="header-title-group">
|
<div class="header-title-group">
|
||||||
<span class="app-header__title">ZDDC Archive</span>
|
<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>
|
</div>
|
||||||
<button id="addDirectoryBtn" class="btn btn-primary">Use Local Directory</button>
|
<button id="addDirectoryBtn" class="btn btn-primary">Use Local Directory</button>
|
||||||
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh Data">⟳</button>
|
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh Data">⟳</button>
|
||||||
|
|
|
||||||
|
|
@ -2344,7 +2344,7 @@ body {
|
||||||
</svg>
|
</svg>
|
||||||
<div class="header-title-group">
|
<div class="header-title-group">
|
||||||
<span class="app-header__title">ZDDC Browse</span>
|
<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>
|
</div>
|
||||||
<button id="addDirectoryBtn" class="btn btn-primary">Use Local Directory</button>
|
<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>
|
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh listing" aria-label="Refresh listing">⟳</button>
|
||||||
|
|
|
||||||
|
|
@ -1793,7 +1793,7 @@ body.is-elevated::after {
|
||||||
</svg>
|
</svg>
|
||||||
<div class="header-title-group">
|
<div class="header-title-group">
|
||||||
<span class="app-header__title">ZDDC Classifier</span>
|
<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>
|
</div>
|
||||||
<button id="addDirectoryBtn" class="btn btn-primary">Use Local Directory</button>
|
<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>
|
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh and rescan directory" aria-label="Refresh" style="font-size:1.1rem;">⟳</button>
|
||||||
|
|
|
||||||
|
|
@ -1536,7 +1536,7 @@ body {
|
||||||
</svg>
|
</svg>
|
||||||
<div class="header-title-group">
|
<div class="header-title-group">
|
||||||
<span class="app-header__title">ZDDC</span>
|
<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>
|
</div>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
|
|
|
||||||
|
|
@ -2635,7 +2635,7 @@ dialog.modal--narrow {
|
||||||
</svg>
|
</svg>
|
||||||
<div class="header-title-group">
|
<div class="header-title-group">
|
||||||
<span class="app-header__title">ZDDC Transmittal</span>
|
<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>
|
</div>
|
||||||
<span id="no-js-notice" class="text-gray-400 text-xs italic">JavaScript not available</span>
|
<span id="no-js-notice" class="text-gray-400 text-xs italic">JavaScript not available</span>
|
||||||
<!-- Publish split-button (Transmittal-specific primary action;
|
<!-- Publish split-button (Transmittal-specific primary action;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
# Generated by build.sh — do not edit. One <app>=<build label> per line.
|
# Generated by build.sh — do not edit. One <app>=<build label> per line.
|
||||||
archive=v0.0.23
|
archive=v0.0.22
|
||||||
transmittal=v0.0.23
|
transmittal=v0.0.22
|
||||||
classifier=v0.0.23
|
classifier=v0.0.22
|
||||||
landing=v0.0.23
|
landing=v0.0.22
|
||||||
form=v0.0.23
|
form=v0.0.22
|
||||||
tables=v0.0.23
|
tables=v0.0.22
|
||||||
browse=v0.0.23
|
browse=v0.0.22
|
||||||
|
|
|
||||||
|
|
@ -1534,7 +1534,7 @@ body.is-elevated::after {
|
||||||
</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">v0.0.23</span>
|
<span class="build-timestamp">v0.0.22</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue