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>
547 lines
16 KiB
Plaintext
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)
|