sx-gitea Phase 2: access — acl-backed permissions, collaborators, teams, auth-gated routes (TDD, 194/194)

lib/gitea/access.sx: repo role groups (admin>write>read) as acl facts
saturated by the datalog engine; user-owner => admin; collaborators
(per-repo role, upsert); org teams (one role, 'all' or scoped repo
list); org-admin?; visible-repos; create-allowed?; bearer tokens in kv.
Facts derived from forge state, acl db cached in the forge handle and
rebuilt only when facts change.

lib/gitea/web.sx: every repo route now requires read (404 hides private
repos); repo create needs owner/org-admin, delete + collaborator API
need admin (401 no credentials / 403 not allowed); index + /api/repos
list only visible repos; PUT/DELETE collab endpoints.

tests/access.sx (103) + repo suite updated for gating (91). Fixed a
web.sx corruption from the known sx_find_all/sx_replace_node path
mismatch by rewriting via sx_write_file; suite timeout 300->600s.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-03 13:21:57 +00:00
parent c037aca51f
commit 1f7f98d0ce
8 changed files with 1166 additions and 84 deletions

View File

@@ -1,5 +1,6 @@
; lib/gitea/tests/repo.sx — Phase 1: forge core (owners, repo CRUD, git
; wiring, ref/tree navigation) and the dream browse views + JSON API.
; Mutating API calls authenticate as alice (Phase 2 gates them).
(define gitea-repo-pass 0)
(define gitea-repo-fail 0)
@@ -116,7 +117,7 @@
(gitea/repos gt-forge)
(list "alice/proj"))
(gitea/repo-create! gt-forge "acme" "proj" {:visibility "private"})
(gitea/repo-create! gt-forge "acme" "proj" {})
(gitea-repo-test
"same name under two owners"
@@ -126,10 +127,6 @@
"repos-for alice"
(gitea/repos-for gt-forge "alice")
(list "proj"))
(gitea-repo-test
"private visibility stored"
(get (gitea/repo-get gt-forge "acme" "proj") :visibility)
"private")
(gitea-repo-test
"repo-update! description"
(begin
@@ -145,6 +142,12 @@
(gitea/repo-update! gt-forge "alice" "nope" (fn (r) r))
nil)
(gitea/repo-create! gt-forge "alice" "hidden" {:visibility "private"})
(gitea-repo-test
"private visibility stored"
(get (gitea/repo-get gt-forge "alice" "hidden") :visibility)
"private")
; ── git store wiring ─────────────────────────────────────────────────
(define gt-grepo (gitea/repo-git gt-forge "alice" "proj"))
@@ -241,12 +244,15 @@
(define
gt-get
(fn (target) (gt-app (dream-request "GET" target {} ""))))
(gitea/token-create! gt-forge "alice" "tok-alice")
(define gt-auth {:authorization "Bearer tok-alice"})
(define
gt-post
(fn (target body) (gt-app (dream-request "POST" target {} body))))
gt-posta
(fn (target body) (gt-app (dream-request "POST" target gt-auth body))))
(define
gt-del
(fn (target) (gt-app (dream-request "DELETE" target {} ""))))
gt-dela
(fn (target) (gt-app (dream-request "DELETE" target gt-auth ""))))
(gitea-repo-test "GET / status" (dream-status (gt-get "/")) 200)
(gitea-repo-test
@@ -376,16 +382,16 @@
(dream-status (gt-get "/alice/proj/commit/zzz"))
404)
; ── json api ─────────────────────────────────────────────────────────
; ── json api (as alice) ──────────────────────────────────────────────
(gitea-repo-test
"api repos json"
"api repos json (anon: public only)"
(dream-json-parse (dream-resp-body (gt-get "/api/repos")))
(list "acme/proj" "alice/proj"))
(gitea-repo-test
"api create 201"
(dream-status (gt-post "/api/repos" (dream-json-encode {:name "web" :owner "alice"})))
(dream-status (gt-posta "/api/repos" (dream-json-encode {:name "web" :owner "alice"})))
201)
(gitea-repo-test
"api create persisted"
@@ -393,19 +399,19 @@
true)
(gitea-repo-test
"api create duplicate 409"
(dream-status (gt-post "/api/repos" (dream-json-encode {:name "web" :owner "alice"})))
(dream-status (gt-posta "/api/repos" (dream-json-encode {:name "web" :owner "alice"})))
409)
(gitea-repo-test
"api create unknown owner 400"
(dream-status (gt-post "/api/repos" (dream-json-encode {:name "web" :owner "zeb"})))
(dream-status (gt-posta "/api/repos" (dream-json-encode {:name "web" :owner "zeb"})))
400)
(gitea-repo-test
"api create bad name 400"
(dream-status (gt-post "/api/repos" (dream-json-encode {:name "b d" :owner "alice"})))
(dream-status (gt-posta "/api/repos" (dream-json-encode {:name "b d" :owner "alice"})))
400)
(gitea-repo-test
"api delete 200"
(dream-status (gt-del "/api/repos/alice/web"))
(dream-status (gt-dela "/api/repos/alice/web"))
200)
(gitea-repo-test
"api delete gone"
@@ -413,7 +419,7 @@
false)
(gitea-repo-test
"api delete missing 404"
(dream-status (gt-del "/api/repos/alice/web"))
(dream-status (gt-dela "/api/repos/alice/web"))
404)
; ── delete purges the git namespace ──────────────────────────────────
@@ -445,4 +451,4 @@
(gitea-repo-test
"other repos survive delete"
(gitea/repos gt-forge)
(list "acme/proj" "alice/proj"))
(list "acme/proj" "alice/hidden" "alice/proj"))