ZDDC/zddc/internal/handler/wormbypass_test.go
ZDDC cfa7732183 test(handler): lock-in invariants for admin/elevation/WORM behavior
Baseline test battery that pins the current auth-decision behavior so
the upcoming consolidation refactor (single bypass site in
InternalDecider.Allow) is validated against a green baseline.

Each test names one invariant; failure messages identify exactly
which property regressed. Coverage:

- Un-elevated admin cannot bypass WORM (PUT to issued/ → 403).
- Un-elevated admin cannot edit .zddc (Principal.gate() blocks).
- Elevated admin bypasses WORM (positive control).
- Elevated subtree admin writes within scope, blocked outside it.
- Strict-ancestor rule: subtree admin cannot edit own subtree's
  .zddc, can edit deeper .zddc.
- Empty email never matches.
- WORM cr survives for un-elevated document_controller (create OK,
  overwrite still stripped).
- project_team has read-only outside their auto-own home.
- Forward-auth /.auth/admin gates strictly on ROOT admins:.

wormbypass_test.go retained as the original repro of the live bitnest
observation (un-elevated user write succeeded under --no-auth=1).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 09:12:37 -05:00

80 lines
3.2 KiB
Go

package handler
import (
"bytes"
"context"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"testing"
"codeberg.org/VARASYS/ZDDC/zddc/internal/config"
"codeberg.org/VARASYS/ZDDC/zddc/internal/zddc"
)
// TestPutToIssuedAsUnelevatedNonAdminUserDenied reproduces the bitnest
// observation: an un-elevated user@bitnest.cc was able to PUT a markdown
// file inside archive/<party>/issued/<transmittal>/ even though the
// embedded defaults declare issued/ as WORM and the user is not in the
// document_controller role. Expectation: 403. If this test passes (i.e.
// 403), the bug is somewhere outside this path; if it fails (200 or
// similar), we've reproduced the bypass.
func TestPutToIssuedAsUnelevatedNonAdminUserDenied(t *testing.T) {
root := t.TempDir()
// Mirror bitnest's current root .zddc shape: user@bitnest.cc is in
// admins (potential admin) but un-elevated — should NOT have admin
// authority. Roles populate document_controller and project_team
// (which matches *@bitnest.cc → user@bitnest.cc gets project_team:r
// at project level via the embedded defaults).
mustWriteHelper(t, filepath.Join(root, ".zddc"),
"admins:\n - user@bitnest.cc\n"+
"roles:\n"+
" document_controller:\n members: [alice@example.com, bob@example.com]\n"+
" project_team:\n members: [\"*@example.com\", \"*@bitnest.cc\"]\n")
// Materialise the exact path shape from the bitnest log entry.
issuedDir := filepath.Join(root, "Project-1/archive/PartyA/issued/2025-09-21_A-FAC2-PM-DRW-0377 (RSB) - Test")
if err := os.MkdirAll(issuedDir, 0o755); err != nil {
t.Fatalf("mkdir issued: %v", err)
}
target := filepath.Join(issuedDir, "A-FAC1-EL-SPC-0469_0A (IFR) - Test.md")
if err := os.WriteFile(target, []byte("# original\n"), 0o644); err != nil {
t.Fatalf("seed target: %v", err)
}
zddc.InvalidateCache(root)
cfg := config.Config{
Root: root,
EmailHeader: "X-Auth-Request-Email",
MaxWriteBytes: 64 * 1024,
}
// Construct the PUT exactly as the markdown editor in browse would —
// PUT to the file's URL with the modified body.
u := &url.URL{Path: "/Project-1/archive/PartyA/issued/2025-09-21_A-FAC2-PM-DRW-0377 (RSB) - Test/A-FAC1-EL-SPC-0469_0A (IFR) - Test.md"}
req := httptest.NewRequest(http.MethodPut, u.RequestURI(), bytes.NewReader([]byte("# modified by un-elevated user\n")))
req.Header.Set("Content-Type", "text/markdown")
// Critical: emulate ACLMiddleware's effect. user@bitnest.cc, elevation = false.
ctx := context.WithValue(req.Context(), EmailKey, "user@bitnest.cc")
ctx = context.WithValue(ctx, ElevatedKey, false)
req = req.WithContext(ctx)
rec := httptest.NewRecorder()
ServeFileAPI(cfg, rec, req)
if rec.Code == http.StatusOK || rec.Code == http.StatusCreated {
t.Errorf("BUG REPRODUCED — un-elevated non-doc-controller wrote to WORM issued/: status=%d body=%q", rec.Code, rec.Body.String())
} else if rec.Code != http.StatusForbidden {
t.Errorf("status=%d (want 403); body=%q", rec.Code, rec.Body.String())
}
// Bytes-on-disk check too: even if the response says forbidden, the
// write must NOT have landed.
gotBytes, _ := os.ReadFile(target)
if string(gotBytes) != "# original\n" {
t.Errorf("file bytes mutated: got %q, want original", string(gotBytes))
}
}