Files
rose-ash/lib/gitea/tests/access.sx
giles 1f7f98d0ce 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>
2026-07-03 13:21:57 +00:00

547 lines
16 KiB
Plaintext

; lib/gitea/tests/access.sx — Phase 2: visibility, collaborators, org
; teams, acl-backed can?, tokens, and auth-gated web routes.
(define gitea-access-pass 0)
(define gitea-access-fail 0)
(define gitea-access-fails (list))
(define
gitea-access-test
(fn
(name actual expected)
(if
(= actual expected)
(set! gitea-access-pass (+ gitea-access-pass 1))
(begin
(set! gitea-access-fail (+ gitea-access-fail 1))
(set!
gitea-access-fails
(append gitea-access-fails (list {:name name :expected (inspect expected) :actual (inspect actual)})))))))
(define ga-db (persist/mem-backend))
(define ga-forge (gitea/forge ga-db))
(gitea/user-create! ga-forge "alice")
(gitea/user-create! ga-forge "bob")
(gitea/user-create! ga-forge "carol")
(gitea/user-create! ga-forge "eve")
(gitea/org-create! ga-forge "acme")
(gitea/repo-create! ga-forge "alice" "pub" {})
(gitea/repo-create! ga-forge "alice" "sec" {:visibility "private"})
(gitea/repo-create! ga-forge "acme" "app" {:visibility "private"})
; ── can? basics ──────────────────────────────────────────────────────
(gitea-access-test
"public read anon"
(gitea/can? ga-forge nil "read" "alice" "pub")
true)
(gitea-access-test
"public read any user"
(gitea/can? ga-forge "eve" "read" "alice" "pub")
true)
(gitea-access-test
"public write anon denied"
(gitea/can? ga-forge nil "write" "alice" "pub")
false)
(gitea-access-test
"public write stranger denied"
(gitea/can? ga-forge "eve" "write" "alice" "pub")
false)
(gitea-access-test
"private read anon denied"
(gitea/can? ga-forge nil "read" "alice" "sec")
false)
(gitea-access-test
"private read stranger denied"
(gitea/can? ga-forge "eve" "read" "alice" "sec")
false)
(gitea-access-test
"owner reads private"
(gitea/can? ga-forge "alice" "read" "alice" "sec")
true)
(gitea-access-test
"owner writes private"
(gitea/can? ga-forge "alice" "write" "alice" "sec")
true)
(gitea-access-test
"owner admins private"
(gitea/can? ga-forge "alice" "admin" "alice" "sec")
true)
(gitea-access-test
"owner admins public"
(gitea/can? ga-forge "alice" "admin" "alice" "pub")
true)
(gitea-access-test
"missing repo denied"
(gitea/can? ga-forge "alice" "read" "alice" "nope")
false)
; ── collaborators ────────────────────────────────────────────────────
(gitea-access-test
"collab-add! bob write"
(get (gitea/collab-add! ga-forge "alice" "sec" "bob" "write") :role)
"write")
(gitea-access-test
"collab-role"
(gitea/collab-role ga-forge "alice" "sec" "bob")
"write")
(gitea-access-test
"collabs list"
(gitea/collabs ga-forge "alice" "sec")
(list "bob"))
(gitea-access-test
"write collab reads"
(gitea/can? ga-forge "bob" "read" "alice" "sec")
true)
(gitea-access-test
"write collab writes"
(gitea/can? ga-forge "bob" "write" "alice" "sec")
true)
(gitea-access-test
"write collab cannot admin"
(gitea/can? ga-forge "bob" "admin" "alice" "sec")
false)
(gitea/collab-add! ga-forge "alice" "sec" "carol" "read")
(gitea-access-test
"read collab reads"
(gitea/can? ga-forge "carol" "read" "alice" "sec")
true)
(gitea-access-test
"read collab cannot write"
(gitea/can? ga-forge "carol" "write" "alice" "sec")
false)
(gitea/collab-add! ga-forge "alice" "sec" "carol" "write")
(gitea-access-test
"collab upsert to write"
(gitea/can? ga-forge "carol" "write" "alice" "sec")
true)
(gitea-access-test
"collab-add! missing repo"
(get (gitea/collab-add! ga-forge "alice" "nope" "bob" "read") :error)
"no-such-repo")
(gitea-access-test
"collab-add! missing user"
(get (gitea/collab-add! ga-forge "alice" "sec" "zeb" "read") :error)
"no-such-user")
(gitea-access-test
"collab-add! bad role"
(get (gitea/collab-add! ga-forge "alice" "sec" "bob" "boss") :error)
"invalid-role")
(gitea-access-test
"collab-remove! carol"
(gitea/collab-remove! ga-forge "alice" "sec" "carol")
true)
(gitea-access-test
"removed collab cannot write"
(gitea/can? ga-forge "carol" "write" "alice" "sec")
false)
(gitea-access-test
"removed collab cannot read private"
(gitea/can? ga-forge "carol" "read" "alice" "sec")
false)
(gitea-access-test
"collab-remove! again false"
(gitea/collab-remove! ga-forge "alice" "sec" "carol")
false)
; ── teams ────────────────────────────────────────────────────────────
(gitea-access-test
"team-create! owners"
(get (gitea/team-create! ga-forge "acme" "owners" "admin") :role)
"admin")
(gitea-access-test
"team-create! duplicate conflicts"
(get (gitea/team-create! ga-forge "acme" "owners" "read") :conflict)
true)
(gitea-access-test
"team-create! on user rejected"
(get (gitea/team-create! ga-forge "alice" "crew" "read") :error)
"no-such-org")
(gitea-access-test
"team-create! bad role"
(get (gitea/team-create! ga-forge "acme" "crew" "boss") :error)
"invalid-role")
(gitea/team-add-member! ga-forge "acme" "owners" "alice")
(gitea-access-test
"team-members"
(gitea/team-members ga-forge "acme" "owners")
(list "alice"))
(gitea-access-test
"team-add-member! missing team"
(get (gitea/team-add-member! ga-forge "acme" "ghosts" "bob") :error)
"no-such-team")
(gitea-access-test
"team-add-member! missing user"
(get (gitea/team-add-member! ga-forge "acme" "owners" "zeb") :error)
"no-such-user")
(gitea-access-test
"owners member admins org repo"
(gitea/can? ga-forge "alice" "admin" "acme" "app")
true)
(gitea-access-test
"owners member reads org repo"
(gitea/can? ga-forge "alice" "read" "acme" "app")
true)
(gitea-access-test
"non-member cannot read org private"
(gitea/can? ga-forge "bob" "read" "acme" "app")
false)
(gitea-access-test
"org-admin? alice"
(gitea/org-admin? ga-forge "acme" "alice")
true)
(gitea-access-test
"org-admin? bob"
(gitea/org-admin? ga-forge "acme" "bob")
false)
(gitea/team-create! ga-forge "acme" "devs" "write")
(gitea/team-set-repos! ga-forge "acme" "devs" (list "app"))
(gitea/team-add-member! ga-forge "acme" "devs" "bob")
(gitea-access-test
"devs member writes covered repo"
(gitea/can? ga-forge "bob" "write" "acme" "app")
true)
(gitea-access-test
"devs member cannot admin"
(gitea/can? ga-forge "bob" "admin" "acme" "app")
false)
(gitea-access-test
"org-admin? devs member"
(gitea/org-admin? ga-forge "acme" "bob")
false)
(gitea/repo-create! ga-forge "acme" "site" {:visibility "private"})
(gitea-access-test
"scoped team does not cover new repo"
(gitea/can? ga-forge "bob" "read" "acme" "site")
false)
(gitea-access-test
"all-repos team covers new repo"
(gitea/can? ga-forge "alice" "admin" "acme" "site")
true)
(gitea/team-set-repos! ga-forge "acme" "devs" "all")
(gitea-access-test
"widened team covers site"
(gitea/can? ga-forge "bob" "write" "acme" "site")
true)
(gitea/team-set-repos! ga-forge "acme" "devs" (list "app"))
(gitea-access-test
"re-narrowed team loses site"
(gitea/can? ga-forge "bob" "write" "acme" "site")
false)
(gitea/team-remove-member! ga-forge "acme" "devs" "bob")
(gitea-access-test
"removed member loses access"
(gitea/can? ga-forge "bob" "write" "acme" "app")
false)
(gitea/team-add-member! ga-forge "acme" "devs" "bob")
(gitea-access-test
"team-delete!"
(gitea/team-delete! ga-forge "acme" "devs")
true)
(gitea-access-test
"deleted team gone"
(gitea/teams ga-forge "acme")
(list "owners"))
(gitea-access-test
"deleted team members purged"
(gitea/team-members ga-forge "acme" "devs")
(list))
(gitea-access-test
"deleted team access revoked"
(gitea/can? ga-forge "bob" "write" "acme" "app")
false)
(gitea-access-test
"team-delete! missing false"
(gitea/team-delete! ga-forge "acme" "devs")
false)
; ── visibility ───────────────────────────────────────────────────────
(gitea-access-test
"visible anon"
(gitea/visible-repos ga-forge nil)
(list "alice/pub"))
(gitea-access-test
"visible eve"
(gitea/visible-repos ga-forge "eve")
(list "alice/pub"))
(gitea-access-test
"visible bob (collab on sec)"
(gitea/visible-repos ga-forge "bob")
(list "alice/pub" "alice/sec"))
(gitea-access-test
"visible alice (owner + org admin)"
(gitea/visible-repos ga-forge "alice")
(list "acme/app" "acme/site" "alice/pub" "alice/sec"))
; ── create permission ────────────────────────────────────────────────
(gitea-access-test
"create under self"
(gitea/create-allowed? ga-forge "alice" "alice")
true)
(gitea-access-test
"create under other user"
(gitea/create-allowed? ga-forge "bob" "alice")
false)
(gitea-access-test
"create anon"
(gitea/create-allowed? ga-forge nil "alice")
false)
(gitea-access-test
"org admin creates in org"
(gitea/create-allowed? ga-forge "alice" "acme")
true)
(gitea-access-test
"non-admin cannot create in org"
(gitea/create-allowed? ga-forge "eve" "acme")
false)
(gitea-access-test
"create under unknown owner"
(gitea/create-allowed? ga-forge "alice" "zeb")
false)
; ── tokens ───────────────────────────────────────────────────────────
(gitea/token-create! ga-forge "alice" "tok-a")
(gitea/token-create! ga-forge "bob" "tok-b")
(gitea/token-create! ga-forge "eve" "tok-e")
(gitea-access-test
"token resolves user"
(gitea/token-user ga-forge "tok-a")
"alice")
(gitea-access-test "unknown token" (gitea/token-user ga-forge "tok-zzz") nil)
(gitea-access-test
"token for unknown user"
(get (gitea/token-create! ga-forge "zeb" "tok-z") :error)
"no-such-user")
(gitea/token-create! ga-forge "carol" "tok-c")
(gitea/token-revoke! ga-forge "tok-c")
(gitea-access-test "revoked token" (gitea/token-user ga-forge "tok-c") nil)
; ── auth-gated web routes ────────────────────────────────────────────
; content in the private repo, to browse
(define ga-gsec (gitea/repo-git ga-forge "alice" "sec"))
(git/add! ga-gsec "secret.txt" "s3cret\n")
(git/commit! ga-gsec {:message "hide" :time 1 :author "alice"})
(define ga-app (gitea/app ga-forge))
(define ga-hdr (fn (tok) (if (nil? tok) {} {:authorization (str "Bearer " tok)})))
(define
ga-get
(fn (target tok) (ga-app (dream-request "GET" target (ga-hdr tok) ""))))
(define
ga-post
(fn
(target tok body)
(ga-app (dream-request "POST" target (ga-hdr tok) body))))
(define
ga-put
(fn
(target tok body)
(ga-app (dream-request "PUT" target (ga-hdr tok) body))))
(define
ga-del
(fn
(target tok)
(ga-app (dream-request "DELETE" target (ga-hdr tok) ""))))
(gitea-access-test
"web: public repo anon"
(dream-status (ga-get "/alice/pub" nil))
200)
(gitea-access-test
"web: private repo anon hidden"
(dream-status (ga-get "/alice/sec" nil))
404)
(gitea-access-test
"web: private repo stranger hidden"
(dream-status (ga-get "/alice/sec" "tok-e"))
404)
(gitea-access-test
"web: private repo collab"
(dream-status (ga-get "/alice/sec" "tok-b"))
200)
(gitea-access-test
"web: private repo owner"
(dream-status (ga-get "/alice/sec" "tok-a"))
200)
(gitea-access-test
"web: private tree anon hidden"
(dream-status (ga-get "/alice/sec/tree/main" nil))
404)
(gitea-access-test
"web: private tree collab"
(dream-status (ga-get "/alice/sec/tree/main" "tok-b"))
200)
(gitea-access-test
"web: private raw stranger hidden"
(dream-status (ga-get "/alice/sec/raw/main/secret.txt" "tok-e"))
404)
(gitea-access-test
"web: private raw collab exact"
(dream-resp-body (ga-get "/alice/sec/raw/main/secret.txt" "tok-b"))
"s3cret\n")
(gitea-access-test
"web: private commits owner"
(dream-status (ga-get "/alice/sec/commits/main" "tok-a"))
200)
(gitea-access-test
"web: index anon shows public"
(contains? (dream-resp-body (ga-get "/" nil)) "alice/pub")
true)
(gitea-access-test
"web: index anon hides private"
(contains? (dream-resp-body (ga-get "/" nil)) "alice/sec")
false)
(gitea-access-test
"web: index owner shows private"
(contains? (dream-resp-body (ga-get "/" "tok-a")) "alice/sec")
true)
(gitea-access-test
"web: api repos anon"
(dream-json-parse (dream-resp-body (ga-get "/api/repos" nil)))
(list "alice/pub"))
(gitea-access-test
"web: api repos owner"
(dream-json-parse (dream-resp-body (ga-get "/api/repos" "tok-a")))
(list "acme/app" "acme/site" "alice/pub" "alice/sec"))
(gitea-access-test
"web: create anon 401"
(dream-status (ga-post "/api/repos" nil (dream-json-encode {:name "x" :owner "alice"})))
401)
(gitea-access-test
"web: create for other user 403"
(dream-status
(ga-post "/api/repos" "tok-b" (dream-json-encode {:name "x" :owner "alice"})))
403)
(gitea-access-test
"web: create own 201"
(dream-status
(ga-post "/api/repos" "tok-a" (dream-json-encode {:name "x" :owner "alice"})))
201)
(gitea-access-test
"web: org admin create 201"
(dream-status
(ga-post "/api/repos" "tok-a" (dream-json-encode {:name "tools" :owner "acme"})))
201)
(gitea-access-test
"web: org non-admin create 403"
(dream-status
(ga-post "/api/repos" "tok-e" (dream-json-encode {:name "z" :owner "acme"})))
403)
(gitea-access-test
"web: create unknown owner 400"
(dream-status
(ga-post "/api/repos" "tok-a" (dream-json-encode {:name "z" :owner "zeb"})))
400)
(gitea-access-test
"web: delete anon public 401"
(dream-status (ga-del "/api/repos/alice/x" nil))
401)
(gitea-access-test
"web: delete anon private hidden 404"
(dream-status (ga-del "/api/repos/alice/sec" nil))
404)
(gitea-access-test
"web: delete stranger private hidden 404"
(dream-status (ga-del "/api/repos/alice/sec" "tok-e"))
404)
(gitea-access-test
"web: delete non-admin 403"
(dream-status (ga-del "/api/repos/alice/x" "tok-b"))
403)
(gitea-access-test
"web: delete admin 200"
(dream-status (ga-del "/api/repos/alice/x" "tok-a"))
200)
(gitea-access-test
"web: deleted repo gone"
(dream-status (ga-del "/api/repos/alice/x" "tok-a"))
404)
(gitea-access-test
"web: collab put anon 401"
(dream-status (ga-put "/api/repos/alice/pub/collab/eve" nil "{}"))
401)
(gitea-access-test
"web: collab put non-admin 403"
(dream-status
(ga-put
"/api/repos/alice/pub/collab/eve"
"tok-b"
(dream-json-encode {:role "write"})))
403)
(gitea-access-test
"web: collab put on hidden repo 404"
(dream-status
(ga-put
"/api/repos/alice/sec/collab/eve"
"tok-e"
(dream-json-encode {:role "write"})))
404)
(gitea-access-test
"web: collab put admin 200"
(dream-status
(ga-put
"/api/repos/alice/pub/collab/eve"
"tok-a"
(dream-json-encode {:role "write"})))
200)
(gitea-access-test
"web: collab granted write"
(gitea/can? ga-forge "eve" "write" "alice" "pub")
true)
(gitea-access-test
"web: collab put bad role 400"
(dream-status
(ga-put
"/api/repos/alice/pub/collab/eve"
"tok-a"
(dream-json-encode {:role "boss"})))
400)
(gitea-access-test
"web: collab put unknown user 400"
(dream-status
(ga-put
"/api/repos/alice/pub/collab/zeb"
"tok-a"
(dream-json-encode {:role "read"})))
400)
(gitea-access-test
"web: collab delete admin 200"
(dream-status (ga-del "/api/repos/alice/pub/collab/eve" "tok-a"))
200)
(gitea-access-test
"web: collab revoked"
(gitea/can? ga-forge "eve" "write" "alice" "pub")
false)
(gitea-access-test
"web: collab delete missing 404"
(dream-status (ga-del "/api/repos/alice/pub/collab/eve" "tok-a"))
404)