mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-26 06:16:56 +09:00
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:
1 parent
7e62dda791
commit
5264304a34
96 files changed
+583
-686
No files matched your search
@@ -281,10 +281,9 @@ func TestPackageGenericPublicOnlyTokenLimitedOwner(t *testing.T) {
|
||||
// a public-only read:package token (even the owner's own) must be refused
|
||||
publicOnlyToken := getUserToken(t, owner.Name, auth_model.AccessTokenScopeReadPackage, auth_model.AccessTokenScopePublicOnly)
|
||||
req = NewRequest(t, "GET", base+"/file.bin").AddTokenAuth(publicOnlyToken)
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
// same via the v1 package API surface (checkTokenPublicOnly)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/packages/%s/generic/pkg/1.0.0", owner.Name)).AddTokenAuth(publicOnlyToken)
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
|
||||
// a normal read:package token still works, proving only public-only is restricted
|
||||
token := getUserToken(t, owner.Name, auth_model.AccessTokenScopeReadPackage)
|
||||
|
||||
@@ -30,6 +30,7 @@ type projectScope struct {
|
||||
issueID int64
|
||||
// a second such issue, deliberately left off the board
|
||||
foreignIssueID int64
|
||||
publicReader string
|
||||
}
|
||||
|
||||
func TestAPIProjects(t *testing.T) {
|
||||
@@ -43,10 +44,12 @@ func TestAPIProjects(t *testing.T) {
|
||||
outsider := getUserToken(t, "user8", auth_model.AccessTokenScopeWriteIssue,
|
||||
auth_model.AccessTokenScopeWriteOrganization, auth_model.AccessTokenScopeReadUser)
|
||||
|
||||
publicOnlyOutsider := getUserToken(t, "user8", auth_model.AccessTokenScopeReadIssue,
|
||||
auth_model.AccessTokenScopeReadOrganization, auth_model.AccessTokenScopePublicOnly)
|
||||
for _, scope := range []projectScope{
|
||||
{"Repository", "/api/v1/repos/user2/repo1/projects", 1, 11},
|
||||
{"Organization", "/api/v1/orgs/org3/projects", 16, 17},
|
||||
{"User", "/api/v1/user/projects", 1, 11},
|
||||
{"Repository", "/api/v1/repos/user2/repo1/projects", 1, 11, ""},
|
||||
{"Organization", "/api/v1/orgs/org3/projects", 16, 17, publicOnlyOutsider},
|
||||
{"User", "/api/v1/user/projects", 1, 11, ""},
|
||||
} {
|
||||
t.Run(scope.name, func(t *testing.T) {
|
||||
testProjectLifecycle(t, scope, token)
|
||||
@@ -215,6 +218,10 @@ func testProjectLifecycle(t *testing.T, scope projectScope, token string) {
|
||||
issues := *DecodeJSON(t, MakeRequest(t, req, http.StatusOK), &[]api.Issue{})
|
||||
require.Len(t, issues, 1)
|
||||
assert.Equal(t, scope.issueID, issues[0].ID)
|
||||
if scope.publicReader != "" {
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("%s/columns/%d/issues", projectURL, columnIDs[1])).AddTokenAuth(scope.publicReader)
|
||||
assert.Len(t, *DecodeJSON(t, MakeRequest(t, req, http.StatusOK), &[]api.Issue{}), 1)
|
||||
}
|
||||
|
||||
// the sibling column must not report the same issue
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("%s/columns/%d/issues", projectURL, columnIDs[0])).AddTokenAuth(token)
|
||||
@@ -288,7 +295,7 @@ func testAPIProjectVisibility(t *testing.T, outsider string) {
|
||||
auth_model.AccessTokenScopeReadOrganization, auth_model.AccessTokenScopeReadIssue,
|
||||
auth_model.AccessTokenScopePublicOnly)
|
||||
for _, url := range []string{"/api/v1/orgs/private_org35/projects", "/api/v1/users/private_org35/projects"} {
|
||||
MakeRequest(t, NewRequest(t, "GET", url).AddTokenAuth(publicOnly), http.StatusForbidden)
|
||||
MakeRequest(t, NewRequest(t, "GET", url).AddTokenAuth(publicOnly), http.StatusNotFound)
|
||||
}
|
||||
|
||||
// a public org's board is readable by an outsider but not writable
|
||||
|
||||
@@ -44,6 +44,7 @@ func TestAPIUserReposPublicOnly(t *testing.T) {
|
||||
assert.False(t, repo.Private)
|
||||
}
|
||||
assert.NotContains(t, repoNames(repos), "user2/repo2")
|
||||
assert.Equal(t, strconv.Itoa(len(repos)), resp.Header().Get("X-Total-Count"))
|
||||
}
|
||||
|
||||
func repoNames(repos []api.Repository) []string {
|
||||
@@ -78,12 +79,14 @@ func TestAPIActivityFeedsPublicOnly(t *testing.T) {
|
||||
DecodeJSON(t, resp, &activities)
|
||||
assert.NotEmpty(t, activities)
|
||||
|
||||
publicToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopePublicOnly)
|
||||
req = NewRequest(t, "GET", "/api/v1/users/user2/activities/feeds").
|
||||
AddTokenAuth(publicToken)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
DecodeJSON(t, resp, &activities)
|
||||
assertPublicActivitiesOnly(t, activities)
|
||||
for _, tokenOwner := range []string{"user2", "user1"} {
|
||||
publicToken := getUserToken(t, tokenOwner, auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopePublicOnly)
|
||||
req = NewRequest(t, "GET", "/api/v1/users/user2/activities/feeds").
|
||||
AddTokenAuth(publicToken)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
DecodeJSON(t, resp, &activities)
|
||||
assertPublicActivitiesOnly(t, activities)
|
||||
}
|
||||
|
||||
orgToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadOrganization)
|
||||
req = NewRequest(t, "GET", "/api/v1/orgs/org3/activities/feeds").
|
||||
@@ -100,7 +103,7 @@ func TestAPIActivityFeedsPublicOnly(t *testing.T) {
|
||||
assertPublicActivitiesOnly(t, activities)
|
||||
}
|
||||
|
||||
func TestAPIOrgPermissionsPublicOnly(t *testing.T) {
|
||||
func TestAPIOrgMembershipPublicOnly(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
// user2 is a member of the private org private_org35
|
||||
@@ -115,6 +118,14 @@ func TestAPIOrgPermissionsPublicOnly(t *testing.T) {
|
||||
publicToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopeReadOrganization, auth_model.AccessTokenScopePublicOnly)
|
||||
req = NewRequestf(t, "GET", "/api/v1/users/user2/orgs/%s/permissions", org.Name).AddTokenAuth(publicToken)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
|
||||
var members, publicMembers []*api.User
|
||||
DecodeJSON(t, MakeRequest(t, NewRequest(t, "GET", "/api/v1/orgs/org3/members").AddTokenAuth(token), http.StatusOK), &members)
|
||||
DecodeJSON(t, MakeRequest(t, NewRequest(t, "GET", "/api/v1/orgs/org3/members").AddTokenAuth(publicToken), http.StatusOK), &publicMembers)
|
||||
assert.Len(t, members, 3)
|
||||
assert.Len(t, publicMembers, 2)
|
||||
MakeRequest(t, NewRequest(t, "GET", "/api/v1/orgs/org3/members/user4").AddTokenAuth(token), http.StatusNoContent)
|
||||
MakeRequest(t, NewRequest(t, "GET", "/api/v1/orgs/org3/members/user4").AddTokenAuth(publicToken), http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func TestAPITeamReposPublicOnly(t *testing.T) {
|
||||
|
||||
@@ -142,6 +142,34 @@ func TestAPIViewPulls(t *testing.T) {
|
||||
assert.Len(t, files, 1)
|
||||
}))
|
||||
}
|
||||
|
||||
t.Run("HeadRepoHiddenWhenInaccessible", func(t *testing.T) {
|
||||
session := loginUser(t, "user31")
|
||||
forkToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||
MakeRequest(t, NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/forks", &api.CreateForkOption{}).AddTokenAuth(forkToken), http.StatusAccepted)
|
||||
visible := DecodeJSON(t, MakeRequest(t, NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/pulls", &api.CreatePullRequestOption{
|
||||
Head: "user31:branch2", Base: "master", Title: "pull from a private user's fork",
|
||||
}).AddTokenAuth(forkToken), http.StatusCreated), &api.PullRequest{})
|
||||
require.NotNil(t, visible.Head.Repository)
|
||||
require.NotEmpty(t, visible.Head.Sha)
|
||||
|
||||
prURL := fmt.Sprintf("/api/v1/repos/user2/repo1/pulls/%d", visible.Index)
|
||||
var listed []*api.PullRequest
|
||||
DecodeJSON(t, MakeRequest(t, NewRequest(t, "GET", "/api/v1/repos/user2/repo1/pulls?poster=user31"), http.StatusOK), &listed)
|
||||
require.Len(t, listed, 1)
|
||||
for _, hidden := range []*api.PullRequest{
|
||||
DecodeJSON(t, MakeRequest(t, NewRequest(t, "GET", prURL), http.StatusOK), &api.PullRequest{}),
|
||||
listed[0],
|
||||
} {
|
||||
assert.Nil(t, hidden.Head.Repository)
|
||||
assert.EqualValues(t, -1, hidden.Head.RepoID)
|
||||
assert.Equal(t, visible.Head.Name, hidden.Head.Name)
|
||||
assert.Equal(t, visible.Head.Sha, hidden.Head.Sha)
|
||||
}
|
||||
|
||||
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: 1, Index: visible.Index})
|
||||
assert.NotNil(t, convert.ToPayloadPullRequest(t.Context(), pr, nil).Head.Repository)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAPIViewPullsByBaseHead(t *testing.T) {
|
||||
|
||||
@@ -21,7 +21,7 @@ func TestUserHeatmap(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
adminUsername := "user1"
|
||||
normalUsername := "user2"
|
||||
token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeReadUser)
|
||||
token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeAll)
|
||||
|
||||
fakeNow := time.Date(2011, 10, 20, 0, 0, 0, 0, time.Local)
|
||||
timeutil.MockSet(fakeNow)
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"testing"
|
||||
|
||||
auth_model "gitea.dev/models/auth"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
api "gitea.dev/modules/structs"
|
||||
@@ -32,10 +33,12 @@ func TestAPIPublicOnlySelfUserRoutes(t *testing.T) {
|
||||
auth_model.AccessTokenScopeWriteUser,
|
||||
)
|
||||
|
||||
t.Run("PrivateProfileForbidden", func(t *testing.T) {
|
||||
t.Run("PrivateProfileUnreachable", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
MakeRequest(t, NewRequest(t, "GET", "/api/v1/users/user31").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
|
||||
MakeRequest(t, NewRequest(t, "GET", "/api/v1/users/user31").AddTokenAuth(privateReadUserToken), http.StatusNotFound)
|
||||
require.NoError(t, db.Insert(t.Context(), &user_model.Redirect{LowerName: "old-user31", RedirectUserID: privateUser.ID}))
|
||||
MakeRequest(t, NewRequest(t, "GET", "/api/v1/users/old-user31").AddTokenAuth(privateReadUserToken), http.StatusNotFound)
|
||||
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
|
||||
})
|
||||
|
||||
|
||||
@@ -277,16 +277,16 @@ func TestAttachmentTokenScopes(t *testing.T) {
|
||||
url: "/attachments/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12",
|
||||
readIssueStatus: http.StatusOK,
|
||||
readRepoStatus: http.StatusForbidden,
|
||||
publicOnlyIssueStatus: http.StatusForbidden,
|
||||
publicOnlyRepoStatus: http.StatusForbidden,
|
||||
publicOnlyIssueStatus: http.StatusNotFound,
|
||||
publicOnlyRepoStatus: http.StatusNotFound,
|
||||
},
|
||||
{
|
||||
name: "RepoPrivateIssueAttachment",
|
||||
url: "/user2/repo2/attachments/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12",
|
||||
readIssueStatus: http.StatusOK,
|
||||
readRepoStatus: http.StatusForbidden,
|
||||
publicOnlyIssueStatus: http.StatusForbidden,
|
||||
publicOnlyRepoStatus: http.StatusForbidden,
|
||||
publicOnlyIssueStatus: http.StatusNotFound,
|
||||
publicOnlyRepoStatus: http.StatusNotFound,
|
||||
},
|
||||
{
|
||||
name: "GlobalPublicReleaseAttachment",
|
||||
@@ -309,16 +309,16 @@ func TestAttachmentTokenScopes(t *testing.T) {
|
||||
url: "/attachments/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a22",
|
||||
readIssueStatus: http.StatusForbidden,
|
||||
readRepoStatus: http.StatusOK,
|
||||
publicOnlyIssueStatus: http.StatusForbidden,
|
||||
publicOnlyRepoStatus: http.StatusForbidden,
|
||||
publicOnlyIssueStatus: http.StatusNotFound,
|
||||
publicOnlyRepoStatus: http.StatusNotFound,
|
||||
},
|
||||
{
|
||||
name: "RepoPrivateReleaseAttachment",
|
||||
url: "/user2/repo2/releases/attachments/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a22",
|
||||
readIssueStatus: http.StatusForbidden,
|
||||
readRepoStatus: http.StatusOK,
|
||||
publicOnlyIssueStatus: http.StatusForbidden,
|
||||
publicOnlyRepoStatus: http.StatusForbidden,
|
||||
publicOnlyIssueStatus: http.StatusNotFound,
|
||||
publicOnlyRepoStatus: http.StatusNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -201,7 +201,7 @@ func TestDownloadRepoContentTokenScopes(t *testing.T) {
|
||||
MakeRequest(t, NewRequest(t, tc.method, tc.url).AddTokenAuth(miscToken), http.StatusForbidden)
|
||||
MakeRequest(t, NewRequest(t, tc.method, tc.url).AddTokenAuth(ownerReadToken), tc.withScope)
|
||||
|
||||
publicOnlyStatus := http.StatusForbidden
|
||||
publicOnlyStatus := http.StatusNotFound
|
||||
if tc.publicOnlyOK {
|
||||
publicOnlyStatus = tc.withScope
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ func testGitSmartHTTPTokenScopes(t *testing.T) {
|
||||
|
||||
t.Run("public-only scope rejects private repo", func(t *testing.T) {
|
||||
path := "/user2/repo2/info/refs?service=git-upload-pack"
|
||||
MakeRequest(t, NewRequest(t, "GET", path).AddTokenAuth(publicOnlyToken), http.StatusForbidden)
|
||||
MakeRequest(t, NewRequest(t, "GET", path).AddTokenAuth(publicOnlyToken), http.StatusNotFound)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -123,7 +123,7 @@ func testPrivateActivityHelperHasHeatmapContentFromPublic(t *testing.T) bool {
|
||||
}
|
||||
|
||||
func testPrivateActivityHelperHasHeatmapContentFromSession(t *testing.T, session *TestSession) bool {
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadUser)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeAll)
|
||||
|
||||
req := NewRequestf(t, "GET", "/api/v1/users/%s/heatmap", privateActivityTestUser).
|
||||
AddTokenAuth(token)
|
||||
|
||||
@@ -61,41 +61,6 @@ func TestAPIPullUpdate(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// TestAPIPullUpdatePublicOnlyToken verifies that a public-only API token cannot
|
||||
// update (push into) a PR whose head repo is private, even when the base repo
|
||||
// named in the route is public.
|
||||
func TestAPIPullUpdatePublicOnlyToken(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
org26 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 26})
|
||||
pr := createOutdatedPR(t, user, org26)
|
||||
require.NoError(t, pr.LoadBaseRepo(t.Context()))
|
||||
require.NoError(t, pr.LoadHeadRepo(t.Context()))
|
||||
require.NoError(t, pr.LoadIssue(t.Context()))
|
||||
|
||||
// make the head repo private while the base repo stays public
|
||||
require.NoError(t, repo_model.UpdateRepositoryColsNoAutoTime(t.Context(),
|
||||
&repo_model.Repository{ID: pr.HeadRepo.ID, IsPrivate: true}, "is_private"))
|
||||
|
||||
// a public-only write token must be refused (404), not perform the push
|
||||
publicOnlyToken := getUserToken(t, user.Name, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopePublicOnly)
|
||||
req := NewRequestf(t, "POST", "/api/v1/repos/%s/%s/pulls/%d/update", pr.BaseRepo.OwnerName, pr.BaseRepo.Name, pr.Issue.Index).
|
||||
AddTokenAuth(publicOnlyToken)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
|
||||
// the head branch must still be outdated (no push happened)
|
||||
diffCount, err := git.GetDivergingCommits(t.Context(), pr.BaseRepo, pr.BaseBranch, pr.GetGitHeadRefName())
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 1, diffCount.Behind)
|
||||
|
||||
// a normal write token still works, proving the guard is scope-specific
|
||||
token := getUserToken(t, user.Name, auth_model.AccessTokenScopeWriteRepository)
|
||||
req = NewRequestf(t, "POST", "/api/v1/repos/%s/%s/pulls/%d/update", pr.BaseRepo.OwnerName, pr.BaseRepo.Name, pr.Issue.Index).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
})
|
||||
}
|
||||
|
||||
func updateRepoPullRequestConfig(t *testing.T, repoID int64, update func(*repo_model.PullRequestsConfig)) {
|
||||
t.Helper()
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ func TestRepoWebTokenScopes(t *testing.T) {
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assertBasicAuthStatus(t, test.url, miscToken, http.StatusForbidden)
|
||||
assertBasicAuthStatus(t, test.url, publicOnlyToken, http.StatusForbidden)
|
||||
assertBasicAuthStatus(t, test.url, publicOnlyToken, http.StatusNotFound)
|
||||
assertBasicAuthStatus(t, test.url, readToken, http.StatusOK)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ func TestAPIWorkflowRun(t *testing.T) {
|
||||
testAPIWorkflowRunBasic(t, "/api/v1/user/actions", "User2", 803, auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopeReadRepository)
|
||||
})
|
||||
t.Run("OrgRuns", func(t *testing.T) {
|
||||
testAPIWorkflowRunBasic(t, "/api/v1/orgs/org3/actions", "User1", 802, auth_model.AccessTokenScopeReadOrganization, auth_model.AccessTokenScopeReadRepository)
|
||||
testAPIWorkflowRunBasic(t, "/api/v1/orgs/org3/actions", "User1", 802, auth_model.AccessTokenScopeAll)
|
||||
})
|
||||
t.Run("RepoRuns", func(t *testing.T) {
|
||||
testAPIWorkflowRunBasic(t, "/api/v1/repos/org3/repo5/actions", "User2", 802, auth_model.AccessTokenScopeReadRepository)
|
||||
|
||||
Reference in new issue
Block a user