refactor: carry the API token on the doer (#39690)

The token a request authenticated with now lives on the doer, so
permission and visibility checks apply its scope from the doer alone.
Replaces https://github.com/go-gitea/gitea/pull/39167, following
https://github.com/go-gitea/gitea/pull/39167#issuecomment-6063839619.

1. Public-only tokens get 404 instead of 403 for hidden users, orgs and
repos, like GitHub.
2. Org ownership, site admin, repo and org creation, generation and
transfer endpoints reject public-only tokens.
3. API pull request responses set `head.repo` to `null` and
`head.repo_id` to `-1` when the caller cannot see the head repository.
4. Public-only tokens see org memberships like anonymous callers, so
concealed members stay hidden.
5. Site admin access in activity feeds, follower lists, org members,
Actions run lists and packages needs a session or an `all`-scoped token.

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
silverwindandwxiaoguang authored and GitHub committed 2026-10-19 00:34:01 +00:00
1 parent 7e62dda791
commit 5264304a34
96 files changed
+583 -686

No files matched your search

+8 -1
View File
@@ -424,6 +424,9 @@ func GetDoerRepoPermission(ctx context.Context, repo *repo_model.Repository, use
// In most request paths, callers should use GetDoerRepoPermission instead.
// Unlike GetDoerRepoPermission, this helper does not resolve Actions task users.
func GetIndividualUserRepoPermission(ctx context.Context, repo *repo_model.Repository, user *user_model.User) (perm Permission, err error) {
if !repo_model.DoerTokenAllowsRepo(ctx, user, repo) {
return PermissionNoAccess(), nil
}
defer func() {
if err == nil {
finalProcessRepoUnitPermission(user, &perm)
@@ -675,10 +678,14 @@ func PermissionNoAccess() Permission {
// permission on a repository. Producers and consumers must agree on it, so it lives here.
func RepoUserPermissionCacheKey(repoID int64, doer *user_model.User) string {
var doerID int64
var doerExtData string
if doer != nil {
doerID = doer.ID
if doer.ExtDoerData != nil {
doerExtData = doer.ExtDoerData.EncodeToString()
}
}
return fmt.Sprintf("%d-%d", repoID, doerID)
return fmt.Sprintf("%d-%d-%s", repoID, doerID, doerExtData)
}
// CanReadWorkflowCrossRepo checks whether the run can read workflow files from targetRepo.
@@ -272,4 +272,21 @@ func testGetDoerRepoPermission(t *testing.T) {
individualPerm, err := GetIndividualUserRepoPermission(ctx, repo1, regularUser)
require.NoError(t, err)
assert.Equal(t, individualPerm, doerPerm)
t.Run("PublicOnlyTokenReachesOnlyPublicReposOfPublicOwners", func(t *testing.T) {
publicOnlyUser := *regularUser
publicOnlyUser.ExtDoerData = user_model.NewTokenExtDoerData(user_model.CredentialAccessToken, 1, "public-only,write:repository")
publicOnlyAdmin := *unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
publicOnlyAdmin.ExtDoerData = publicOnlyUser.ExtDoerData
for _, repoID := range []int64{2, 38} {
perm, err := GetDoerRepoPermission(ctx, unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID}), &publicOnlyAdmin)
require.NoError(t, err)
assert.False(t, perm.HasAnyUnitAccessOrPublicAccess(), "repo %d", repoID)
}
perm, err := GetDoerRepoPermission(ctx, repo1, &publicOnlyUser)
require.NoError(t, err)
assert.True(t, perm.CanWrite(unit.TypeCode))
assert.NotEqual(t, RepoUserPermissionCacheKey(repo1.ID, regularUser), RepoUserPermissionCacheKey(repo1.ID, &publicOnlyUser))
})
}