Compare commits
No commits in common. "d0d8423ac6dcb69fc5056c5ab368465b81ff0124" and "8ef2ce01d0f7fd70475365c24737bb42c691ca21" have entirely different histories.
d0d8423ac6
...
8ef2ce01d0
5 changed files with 7 additions and 95 deletions
|
|
@ -418,10 +418,6 @@ func runClient(cfg config.Config) {
|
||||||
if err := srv.Shutdown(shutdownCtx); err != nil {
|
if err := srv.Shutdown(shutdownCtx); err != nil {
|
||||||
slog.Error("shutdown error", "err", err)
|
slog.Error("shutdown error", "err", err)
|
||||||
}
|
}
|
||||||
// Drain background cache work (revalidation kicked off on hits)
|
|
||||||
// before exiting, so in-flight sidecar writes finish rather than
|
|
||||||
// being abandoned mid-flight.
|
|
||||||
cacheLayer.Wait()
|
|
||||||
slog.Info("stopped")
|
slog.Info("stopped")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -120,10 +120,8 @@ func walkAndIndex(idx *Index, fsRoot, dirAbs, serverDir string) error {
|
||||||
func indexTransmittalFolder(idx *Index, fsRoot, folderAbs, folderServerPath, date string) error {
|
func indexTransmittalFolder(idx *Index, fsRoot, folderAbs, folderServerPath, date string) error {
|
||||||
return filepath.WalkDir(folderAbs, func(path string, d os.DirEntry, err error) error {
|
return filepath.WalkDir(folderAbs, func(path string, d os.DirEntry, err error) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Log and continue indexing the rest of the folder — a
|
// Log the error but continue indexing other files
|
||||||
// permission or FS error on one entry shouldn't abort the
|
_ = err // would log here: slog.Warn("walkdir error", "path", path, "err", err)
|
||||||
// whole transmittal index or vanish without a trace.
|
|
||||||
slog.Warn("transmittal index: walkdir error", "path", path, "err", err)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if d.IsDir() {
|
if d.IsDir() {
|
||||||
|
|
|
||||||
35
zddc/internal/cache/cache.go
vendored
35
zddc/internal/cache/cache.go
vendored
|
|
@ -67,13 +67,6 @@ type Cache struct {
|
||||||
|
|
||||||
markerOnce sync.Once
|
markerOnce sync.Once
|
||||||
|
|
||||||
// wg tracks background goroutines (cache revalidation on hits,
|
|
||||||
// mirror-walk hooks) so Wait() can drain them. Without this they
|
|
||||||
// outlive the request — fine in production until a graceful
|
|
||||||
// shutdown wants them finished, and in tests they race t.TempDir
|
|
||||||
// cleanup by writing into the cache root after the test returns.
|
|
||||||
wg sync.WaitGroup
|
|
||||||
|
|
||||||
// onAccess is invoked (when non-nil) after a request is dispatched.
|
// onAccess is invoked (when non-nil) after a request is dispatched.
|
||||||
// The walker scheduler installs this hook to kick mirror walks based
|
// The walker scheduler installs this hook to kick mirror walks based
|
||||||
// on incoming traffic. Always called in a goroutine — must not
|
// on incoming traffic. Always called in a goroutine — must not
|
||||||
|
|
@ -92,25 +85,6 @@ type Cache struct {
|
||||||
// offline writes.
|
// offline writes.
|
||||||
func (c *Cache) SetOutbox(o *Outbox) { c.outbox = o }
|
func (c *Cache) SetOutbox(o *Outbox) { c.outbox = o }
|
||||||
|
|
||||||
// goBackground runs fn in a tracked goroutine so Wait can drain
|
|
||||||
// in-flight background work — cache revalidation kicked off on a hit,
|
|
||||||
// and the mirror-walk access hook. These must never block the user
|
|
||||||
// response, but they also shouldn't outlive a graceful shutdown (or,
|
|
||||||
// in tests, a t.TempDir cleanup that they'd race by writing into the
|
|
||||||
// cache root after the test returns).
|
|
||||||
func (c *Cache) goBackground(fn func()) {
|
|
||||||
c.wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer c.wg.Done()
|
|
||||||
fn()
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait blocks until all tracked background goroutines have finished.
|
|
||||||
// Intended for graceful shutdown; tests call it before the temp-dir
|
|
||||||
// cleanup runs.
|
|
||||||
func (c *Cache) Wait() { c.wg.Wait() }
|
|
||||||
|
|
||||||
// New constructs a Cache from the loaded configuration. Validates
|
// New constructs a Cache from the loaded configuration. Validates
|
||||||
// upstream URL, reads the bearer-file (if configured), prepares the
|
// upstream URL, reads the bearer-file (if configured), prepares the
|
||||||
// HTTP client honoring SkipTLSVerify, and ensures the cache root
|
// HTTP client honoring SkipTLSVerify, and ensures the cache root
|
||||||
|
|
@ -207,8 +181,7 @@ func (c *Cache) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
// after we've started serving — the user's request never blocks
|
// after we've started serving — the user's request never blocks
|
||||||
// on walk scheduling.
|
// on walk scheduling.
|
||||||
if c.onAccess != nil {
|
if c.onAccess != nil {
|
||||||
urlPath := r.URL.Path
|
go c.onAccess(r.URL.Path)
|
||||||
c.goBackground(func() { c.onAccess(urlPath) })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Directory listings: try sidecar listing-cache, fall back to
|
// Directory listings: try sidecar listing-cache, fall back to
|
||||||
|
|
@ -228,8 +201,7 @@ func (c *Cache) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
if err == nil && !info.IsDir() {
|
if err == nil && !info.IsDir() {
|
||||||
c.serveFromDisk(w, r, path, info, "hit")
|
c.serveFromDisk(w, r, path, info, "hit")
|
||||||
// Background revalidate; never block the user response.
|
// Background revalidate; never block the user response.
|
||||||
urlPath, mtime := r.URL.Path, info.ModTime()
|
go c.revalidate(r.URL.Path, info.ModTime())
|
||||||
c.goBackground(func() { c.revalidate(urlPath, mtime) })
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -249,8 +221,7 @@ func (c *Cache) serveDirectory(w http.ResponseWriter, r *http.Request) {
|
||||||
info, err := os.Stat(path)
|
info, err := os.Stat(path)
|
||||||
if err == nil && !info.IsDir() {
|
if err == nil && !info.IsDir() {
|
||||||
c.serveListingFromDisk(w, r, path, info, "hit")
|
c.serveListingFromDisk(w, r, path, info, "hit")
|
||||||
urlPath, accept, mtime := r.URL.Path, r.Header.Get("Accept"), info.ModTime()
|
go c.revalidateListing(r.URL.Path, r.Header.Get("Accept"), info.ModTime())
|
||||||
c.goBackground(func() { c.revalidateListing(urlPath, accept, mtime) })
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
8
zddc/internal/cache/cache_test.go
vendored
8
zddc/internal/cache/cache_test.go
vendored
|
|
@ -31,14 +31,6 @@ func newTestCache(t *testing.T, mode string, upstreamHandler http.HandlerFunc) (
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("New: %v", err)
|
t.Fatalf("New: %v", err)
|
||||||
}
|
}
|
||||||
// Drain background revalidation goroutines before the test's
|
|
||||||
// t.TempDir cleanup runs. Cleanups fire LIFO and t.TempDir
|
|
||||||
// registered its RemoveAll first (at the t.TempDir() call above),
|
|
||||||
// so this runs before it — preventing a revalidate goroutine from
|
|
||||||
// recreating the cache dir / dropping a temp file mid-RemoveAll
|
|
||||||
// ("directory not empty"). The upstream stays up (its Close was
|
|
||||||
// registered earliest, so it runs last).
|
|
||||||
t.Cleanup(c.Wait)
|
|
||||||
return c, upstream
|
return c, upstream
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -459,53 +459,8 @@ func TestInvariant_UnelevatedAdminNoSilentBypass(t *testing.T) {
|
||||||
|
|
||||||
func TestInvariant_ProfileAdminEndpointsHideFromNonAdmins(t *testing.T) {
|
func TestInvariant_ProfileAdminEndpointsHideFromNonAdmins(t *testing.T) {
|
||||||
// These checks lock in the existence-hiding property: non-admins must
|
// These checks lock in the existence-hiding property: non-admins must
|
||||||
// see 404, never 403, so they can't probe which admin-only resources
|
// see 404, never 403, so they can't probe which paths exist.
|
||||||
// exist. ServeProfile is the dispatcher (the refactor this test waited
|
t.Skip("requires the profile handler dispatcher entry point; skip until the refactor confirms ServeProfile signature")
|
||||||
// on); its adminOnly wrapper denies with 404 before the sub-handler
|
|
||||||
// runs, so a nil ring/index is safe for the non-admin paths.
|
|
||||||
cfg, _ := invariantsFixture(t)
|
|
||||||
|
|
||||||
adminEndpoints := []string{"/whoami", "/config", "/logs", "/effective-policy", "/reindex"}
|
|
||||||
|
|
||||||
profileGet := func(sub, email string, elevated bool) *httptest.ResponseRecorder {
|
|
||||||
req := httptest.NewRequest(http.MethodGet, ProfilePathPrefix+sub, nil)
|
|
||||||
ctx := context.WithValue(req.Context(), EmailKey, email)
|
|
||||||
ctx = context.WithValue(ctx, ElevatedKey, elevated)
|
|
||||||
req = req.WithContext(ctx)
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
ServeProfile(cfg, nil, nil, rec, req)
|
|
||||||
return rec
|
|
||||||
}
|
|
||||||
|
|
||||||
// Non-admin (eve, project_team only) and anonymous callers must get
|
|
||||||
// 404 on every admin endpoint — never 403, never 200.
|
|
||||||
for _, who := range []struct {
|
|
||||||
email string
|
|
||||||
elevated bool
|
|
||||||
label string
|
|
||||||
}{
|
|
||||||
{"eve@example.com", false, "non-admin"},
|
|
||||||
{"", false, "anonymous"},
|
|
||||||
{"admin@example.com", false, "un-elevated admin"}, // sudo-style: no authority until elevated
|
|
||||||
} {
|
|
||||||
for _, sub := range adminEndpoints {
|
|
||||||
rec := profileGet(sub, who.email, who.elevated)
|
|
||||||
if rec.Code != http.StatusNotFound {
|
|
||||||
t.Errorf("%s GET /.profile%s = %d, want 404 (existence-hiding)", who.label, sub, rec.Code)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Positive control: an elevated root admin must NOT get 404 on the
|
|
||||||
// gated routes that need no ring/index — proving the 404s above are
|
|
||||||
// the admin gate, not a missing route. (/whoami and /config don't
|
|
||||||
// touch the log ring or archive index.)
|
|
||||||
for _, sub := range []string{"/whoami", "/config"} {
|
|
||||||
rec := profileGet(sub, "admin@example.com", true)
|
|
||||||
if rec.Code == http.StatusNotFound {
|
|
||||||
t.Errorf("elevated admin GET /.profile%s = 404; the gate should admit admins", sub)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// dump prints the rec body when t.Logf would help debugging — used in
|
// dump prints the rec body when t.Logf would help debugging — used in
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue