0
0
mirror of https://github.com/go-gitea/gitea.git synced 2026-05-14 17:27:39 +02:00

Merge 3a9e828714f6a61eecd88e27e9cc494e4efe8715 into a5d81d9ce230aaa6e1021b6236ca01cb6d2b56c3

This commit is contained in:
Norbert Szulc 2026-05-08 21:47:12 -04:00 committed by GitHub
commit fa2136036b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 113 additions and 10 deletions

View File

@ -24,6 +24,7 @@ const (
AccessTokenScopeCategoryIssue
AccessTokenScopeCategoryRepository
AccessTokenScopeCategoryUser
AccessTokenScopeCategoryCommitStatus
)
// AllAccessTokenScopeCategories contains all access token scope categories
@ -37,6 +38,7 @@ var AllAccessTokenScopeCategories = []AccessTokenScopeCategory{
AccessTokenScopeCategoryIssue,
AccessTokenScopeCategoryRepository,
AccessTokenScopeCategoryUser,
AccessTokenScopeCategoryCommitStatus,
}
// AccessTokenScopeLevel represents the access levels without a given scope category
@ -82,6 +84,9 @@ const (
AccessTokenScopeReadUser AccessTokenScope = "read:user"
AccessTokenScopeWriteUser AccessTokenScope = "write:user"
AccessTokenScopeReadCommitStatus AccessTokenScope = "read:commitstatus"
AccessTokenScopeWriteCommitStatus AccessTokenScope = "write:commitstatus"
)
// accessTokenScopeBitmap represents a bitmap of access token scopes.
@ -93,7 +98,7 @@ const (
accessTokenScopeAllBits accessTokenScopeBitmap = accessTokenScopeWriteActivityPubBits |
accessTokenScopeWriteAdminBits | accessTokenScopeWriteMiscBits | accessTokenScopeWriteNotificationBits |
accessTokenScopeWriteOrganizationBits | accessTokenScopeWritePackageBits | accessTokenScopeWriteIssueBits |
accessTokenScopeWriteRepositoryBits | accessTokenScopeWriteUserBits
accessTokenScopeWriteRepositoryBits | accessTokenScopeWriteUserBits | accessTokenScopeWriteCommitStatusBits
accessTokenScopePublicOnlyBits accessTokenScopeBitmap = 1 << iota
@ -118,12 +123,15 @@ const (
accessTokenScopeReadIssueBits accessTokenScopeBitmap = 1 << iota
accessTokenScopeWriteIssueBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadIssueBits
accessTokenScopeReadRepositoryBits accessTokenScopeBitmap = 1 << iota
accessTokenScopeWriteRepositoryBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadRepositoryBits
accessTokenScopeReadRepositoryBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadCommitStatusBits
accessTokenScopeWriteRepositoryBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadRepositoryBits | accessTokenScopeWriteCommitStatusBits
accessTokenScopeReadUserBits accessTokenScopeBitmap = 1 << iota
accessTokenScopeWriteUserBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadUserBits
accessTokenScopeReadCommitStatusBits accessTokenScopeBitmap = 1 << iota
accessTokenScopeWriteCommitStatusBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadCommitStatusBits
// The current implementation only supports up to 64 token scopes.
// If we need to support > 64 scopes,
// refactoring the whole implementation in this file (and only this file) is needed.
@ -142,6 +150,7 @@ var allAccessTokenScopes = []AccessTokenScope{
AccessTokenScopeWriteIssue, AccessTokenScopeReadIssue,
AccessTokenScopeWriteRepository, AccessTokenScopeReadRepository,
AccessTokenScopeWriteUser, AccessTokenScopeReadUser,
AccessTokenScopeWriteCommitStatus, AccessTokenScopeReadCommitStatus,
}
// allAccessTokenScopeBits contains all access token scopes.
@ -166,6 +175,8 @@ var allAccessTokenScopeBits = map[AccessTokenScope]accessTokenScopeBitmap{
AccessTokenScopeWriteRepository: accessTokenScopeWriteRepositoryBits,
AccessTokenScopeReadUser: accessTokenScopeReadUserBits,
AccessTokenScopeWriteUser: accessTokenScopeWriteUserBits,
AccessTokenScopeReadCommitStatus: accessTokenScopeReadCommitStatusBits,
AccessTokenScopeWriteCommitStatus: accessTokenScopeWriteCommitStatusBits,
}
// readAccessTokenScopes maps a scope category to the read permission scope
@ -180,6 +191,7 @@ var accessTokenScopes = map[AccessTokenScopeLevel]map[AccessTokenScopeCategory]A
AccessTokenScopeCategoryIssue: AccessTokenScopeReadIssue,
AccessTokenScopeCategoryRepository: AccessTokenScopeReadRepository,
AccessTokenScopeCategoryUser: AccessTokenScopeReadUser,
AccessTokenScopeCategoryCommitStatus: AccessTokenScopeReadCommitStatus,
},
Write: {
AccessTokenScopeCategoryActivityPub: AccessTokenScopeWriteActivityPub,
@ -191,6 +203,7 @@ var accessTokenScopes = map[AccessTokenScopeLevel]map[AccessTokenScopeCategory]A
AccessTokenScopeCategoryIssue: AccessTokenScopeWriteIssue,
AccessTokenScopeCategoryRepository: AccessTokenScopeWriteRepository,
AccessTokenScopeCategoryUser: AccessTokenScopeWriteUser,
AccessTokenScopeCategoryCommitStatus: AccessTokenScopeWriteCommitStatus,
},
}

View File

@ -17,13 +17,13 @@ type scopeTestNormalize struct {
}
func TestAccessTokenScope_Normalize(t *testing.T) {
assert.Equal(t, []string{"activitypub", "admin", "issue", "misc", "notification", "organization", "package", "repository", "user"}, GetAccessTokenCategories())
assert.Equal(t, []string{"activitypub", "admin", "commitstatus", "issue", "misc", "notification", "organization", "package", "repository", "user"}, GetAccessTokenCategories())
tests := []scopeTestNormalize{
{"", "", nil},
{"write:misc,write:notification,read:package,write:notification,public-only", "public-only,write:misc,write:notification,read:package", nil},
{"all", "all", nil},
{"write:activitypub,write:admin,write:misc,write:notification,write:organization,write:package,write:issue,write:repository,write:user", "all", nil},
{"write:activitypub,write:admin,write:misc,write:notification,write:organization,write:package,write:issue,write:repository,write:user,public-only", "public-only,all", nil},
{"write:activitypub,write:admin,write:misc,write:notification,write:organization,write:package,write:issue,write:repository,write:user,write:commitstatus", "all", nil},
{"write:activitypub,write:admin,write:misc,write:notification,write:organization,write:package,write:issue,write:repository,write:user,write:commitstatus,public-only", "public-only,all", nil},
}
for _, scope := range GetAccessTokenCategories() {

View File

@ -1373,10 +1373,6 @@ func Routes() *web.Router {
})
m.Get("/{base}/*", repo.GetPullRequestByBaseHead)
}, mustAllowPulls, reqRepoReader(unit.TypeCode), context.ReferencesGitRepo())
m.Group("/statuses", func() { // "/statuses/{sha}" only accepts commit ID
m.Combo("/{sha}").Get(repo.GetCommitStatuses).
Post(reqToken(), reqRepoWriter(unit.TypeCode), bind(api.CreateStatusOption{}), repo.NewCommitStatus)
}, reqRepoReader(unit.TypeCode))
m.Group("/commits", func() {
m.Get("", context.ReferencesGitRepo(), repo.GetAllCommits)
m.PathGroup("/*", func(g *web.RouterPathGroup) {
@ -1446,6 +1442,12 @@ func Routes() *web.Router {
}, repoAssignment(), checkTokenPublicOnly())
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository))
// Commit status can be created by write:commitstatus or write:repository
m.Group("/repos/{username}/{reponame}/statuses", func() { // "/statuses/{sha}" only accepts commit ID
m.Combo("/{sha}").Get(repo.GetCommitStatuses).
Post(reqToken(), bind(api.CreateStatusOption{}), repo.NewCommitStatus)
}, reqRepoReader(unit.TypeCode), repoAssignment(), checkTokenPublicOnly(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryCommitStatus))
// Artifacts direct download endpoint authenticates via signed url
// it is protected by the "sig" parameter (to help to access private repo), so no need to use other middlewares
m.Get("/repos/{username}/{reponame}/actions/artifacts/{artifact_id}/zip/raw", repo.DownloadArtifactRaw)

88
verify35383.py Executable file
View File

@ -0,0 +1,88 @@
# usage: uv run pytest -v verify35383.py
import os
import time
import requests
import pytest
GITEA_URL = os.environ["GITEA_URL"].rstrip("/")
OWNER = os.environ["OWNER"]
REPO = os.environ["REPO"]
COMMIT_SHA = os.environ["COMMIT_SHA"]
STATUS_URL = f"{GITEA_URL}/api/v1/repos/{OWNER}/{REPO}/statuses/{COMMIT_SHA}"
TOKENS = {
"repo-write": {
"token": os.environ["TOKEN_REPO_WRITE"],
"can_write": True,
"can_read": True,
},
"repo-read+commitstatus-write": {
"token": os.environ["TOKEN_STATUS_WRITE"],
"can_write": True,
"can_read": True,
},
"repo-read-only": {
"token": os.environ["TOKEN_REPO_READ"],
"can_write": False,
"can_read": True,
},
}
def auth_headers(token: str) -> dict:
return {
"Authorization": f"token {token}",
"Content-Type": "application/json",
"Accept": "application/json",
}
@pytest.mark.parametrize("name,cfg", TOKENS.items())
def test_commit_status_write_permission(name: str, cfg: dict) -> None:
context = f"perm-test-{name}-{int(time.time())}"
payload = {
"state": "success",
"context": context,
"description": "Permission verification test",
"target_url": "https://example.com",
}
response = requests.post(
STATUS_URL,
json=payload,
headers=auth_headers(cfg["token"]),
timeout=10,
)
if cfg["can_write"]:
assert response.status_code == 201, response.text
body = response.json()
assert body["status"] == "success"
assert body["context"] == context
else:
assert response.status_code == 403
@pytest.mark.parametrize("name,cfg", TOKENS.items())
def test_commit_status_read_permission(name: str, cfg: dict) -> None:
response = requests.get(
STATUS_URL,
headers=auth_headers(cfg["token"]),
timeout=10,
)
if cfg["can_read"]:
assert response.status_code == 200, response.text
body = response.json()
assert isinstance(body, list)
if body:
status = body[0]
assert "status" in status
assert "context" in status
assert "created_at" in status
else:
assert response.status_code in (401, 403)