ZDDC/zddc/internal/handler/wormbypass_test.go
2026-06-11 13:32:31 -05:00

88 lines
3.6 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")
// Register the party (party_source: ssr) so the write reaches the WORM
// check this test exercises rather than the registration gate.
if err := os.MkdirAll(filepath.Join(root, "Project-1/ssr"), 0o755); err != nil {
t.Fatalf("mkdir ssr: %v", err)
}
if err := os.WriteFile(filepath.Join(root, "Project-1/ssr/PartyA.yaml"), []byte("kind: SSR\n"), 0o644); err != nil {
t.Fatalf("register party: %v", err)
}
// 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))
}
}