0
0
mirror of https://github.com/go-gitea/gitea.git synced 2026-04-19 16:58:49 +02:00
gitea/tests/integration/actions_job_token_test.go
☙◦ The Tablet ❀ GamerGirlandCo ◦❧ 07d8bd7885
revert: fixture and test changes
give all existing fixture repos a group id of 0
2026-04-02 20:01:13 -04:00

477 lines
20 KiB
Go

// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"encoding/base64"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"strconv"
"testing"
actions_model "code.gitea.io/gitea/models/actions"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
org_model "code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/perm"
repo_model "code.gitea.io/gitea/models/repo"
unit_model "code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/lfs"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestActionsJobTokenPermissiveAccess(t *testing.T) {
cases := []struct {
name string
isFork bool
ownerPermMode repo_model.ActionsTokenPermissionMode
ownerMaxPerms map[unit_model.Type]perm.AccessMode
repoPermMode repo_model.ActionsTokenPermissionMode
repoMaxPerms map[unit_model.Type]perm.AccessMode
expectGitAccess perm.AccessMode
}{
{
name: "OwnerConfig-Permissive",
ownerPermMode: repo_model.ActionsTokenPermissionModePermissive,
expectGitAccess: perm.AccessModeWrite,
},
{
name: "OwnerConfig-Permissive-CodeNone",
ownerPermMode: repo_model.ActionsTokenPermissionModePermissive,
ownerMaxPerms: map[unit_model.Type]perm.AccessMode{unit_model.TypeCode: perm.AccessModeNone},
expectGitAccess: perm.AccessModeNone,
},
{
name: "OwnerConfig-Restricted",
ownerPermMode: repo_model.ActionsTokenPermissionModeRestricted,
expectGitAccess: perm.AccessModeRead,
},
// repo uses its own settings, so owner settings should not affect it
{
name: "SameRepo-Permissive",
ownerPermMode: repo_model.ActionsTokenPermissionModeRestricted,
ownerMaxPerms: map[unit_model.Type]perm.AccessMode{unit_model.TypeCode: perm.AccessModeNone},
repoPermMode: repo_model.ActionsTokenPermissionModePermissive,
expectGitAccess: perm.AccessModeWrite,
},
{
name: "SameRepo-Permissive-CodeNone",
ownerPermMode: repo_model.ActionsTokenPermissionModePermissive,
ownerMaxPerms: map[unit_model.Type]perm.AccessMode{unit_model.TypeCode: perm.AccessModeRead},
repoPermMode: repo_model.ActionsTokenPermissionModePermissive,
repoMaxPerms: map[unit_model.Type]perm.AccessMode{unit_model.TypeCode: perm.AccessModeNone},
expectGitAccess: perm.AccessModeNone,
},
{
name: "SameRepo-Restricted",
repoPermMode: repo_model.ActionsTokenPermissionModeRestricted,
expectGitAccess: perm.AccessModeRead,
},
// forks should be always restricted to max read access for code
{
name: "Fork-Permissive",
repoPermMode: repo_model.ActionsTokenPermissionModePermissive,
isFork: true,
expectGitAccess: perm.AccessModeRead,
},
{
name: "Fork-Restricted",
repoPermMode: repo_model.ActionsTokenPermissionModeRestricted,
isFork: true,
expectGitAccess: perm.AccessModeRead,
},
{
name: "Fork-Restricted-CodeNone",
repoPermMode: repo_model.ActionsTokenPermissionModeRestricted,
repoMaxPerms: map[unit_model.Type]perm.AccessMode{unit_model.TypeCode: perm.AccessModeNone},
isFork: true,
expectGitAccess: perm.AccessModeNone,
},
}
onGiteaRun(t, func(t *testing.T, u *url.URL) {
task := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 47})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: task.RepoID})
repoActionsUnit := repo.MustGetUnit(t.Context(), unit_model.TypeActions)
repoActionsCfg := repoActionsUnit.ActionsConfig()
ownerActionsCfg, err := actions_model.GetOwnerActionsConfig(t.Context(), repo.OwnerID)
require.NoError(t, err)
_, err = db.GetEngine(t.Context()).ID(task.RepoID).Cols("is_private").Update(&repo_model.Repository{IsPrivate: true})
require.NoError(t, err)
assertRespCodeForSuccess := func(t *testing.T, resp *httptest.ResponseRecorder, succeed bool) {
if succeed {
assert.True(t, 200 <= resp.Code && resp.Code < 300, "Expected success status code, got %d", resp.Code)
} else {
assert.True(t, 400 <= resp.Code && resp.Code < 500, "Expected client error status code, got %d", resp.Code)
}
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
// prepare owner's token permissions settings
ownerActionsCfg.TokenPermissionMode = tt.ownerPermMode
ownerActionsCfg.MaxTokenPermissions = util.Iif(tt.ownerMaxPerms == nil, nil, &repo_model.ActionsTokenPermissions{UnitAccessModes: tt.ownerMaxPerms})
require.NoError(t, actions_model.SetOwnerActionsConfig(t.Context(), repo.OwnerID, ownerActionsCfg))
// prepare repo's token permissions settings
repoActionsCfg.OverrideOwnerConfig = tt.repoPermMode != "" || tt.repoMaxPerms != nil
repoActionsCfg.TokenPermissionMode = tt.repoPermMode
repoActionsCfg.MaxTokenPermissions = util.Iif(tt.repoMaxPerms == nil, nil, &repo_model.ActionsTokenPermissions{UnitAccessModes: tt.repoMaxPerms})
require.NoError(t, repo_model.UpdateRepoUnitConfig(t.Context(), repoActionsUnit))
// prepare task and its token
require.NoError(t, task.GenerateToken())
task.Status = actions_model.StatusRunning
task.IsForkPullRequest = tt.isFork
err := actions_model.UpdateTask(t.Context(), task, "token_hash", "token_salt", "token_last_eight", "status", "is_fork_pull_request")
require.NoError(t, err)
require.NoError(t, task.LoadJob(t.Context()))
require.NoError(t, task.Job.LoadRun(t.Context()))
task.Job.Run.IsForkPullRequest = tt.isFork
require.NoError(t, actions_model.UpdateRun(t.Context(), task.Job.Run, "is_fork_pull_request"))
testURL := *u
testURL.User = url.UserPassword("gitea-actions", task.Token)
t.Run("ReadGitContent", func(t *testing.T) {
testURL.Path = "/user5/repo4.git/HEAD"
resp := MakeRequest(t, NewRequest(t, "GET", testURL.String()), NoExpectedStatus)
assertRespCodeForSuccess(t, resp, tt.expectGitAccess != perm.AccessModeNone)
testURL.Path = "/user5/repo4.git/info/lfs/locks"
req := NewRequest(t, "GET", testURL.String()).SetHeader("Accept", lfs.MediaType)
resp = MakeRequest(t, req, NoExpectedStatus)
assertRespCodeForSuccess(t, resp, tt.expectGitAccess != perm.AccessModeNone)
})
t.Run("WriteGitContent", func(t *testing.T) {
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/contents/test-filename", repo.FullName()), &structs.CreateFileOptions{
FileOptions: structs.FileOptions{NewBranchName: "new-branch" + t.Name()},
ContentBase64: base64.StdEncoding.EncodeToString([]byte(`dummy content`)),
}).AddTokenAuth(task.Token)
resp := MakeRequest(t, req, NoExpectedStatus)
assertRespCodeForSuccess(t, resp, tt.expectGitAccess == perm.AccessModeWrite)
testURL.Path = "/user5/repo4.git/info/lfs/objects/batch"
req = NewRequestWithJSON(t, "POST", testURL.String(), lfs.BatchRequest{Operation: "upload"}).SetHeader("Accept", lfs.MediaType)
resp = MakeRequest(t, req, NoExpectedStatus)
assertRespCodeForSuccess(t, resp, tt.expectGitAccess == perm.AccessModeWrite)
})
t.Run("NoOtherPermissions", func(t *testing.T) {
req := NewRequest(t, "DELETE", "/api/v1/repos/"+repo.FullName()).AddTokenAuth(task.Token)
resp := MakeRequest(t, req, NoExpectedStatus)
assertRespCodeForSuccess(t, resp, false)
})
})
}
})
}
func TestActionsCrossRepoAccess(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
session := loginUser(t, "user2")
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteUser, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteOrganization)
// 1. Create Organization
orgName := "org-cross-test"
req := NewRequestWithJSON(t, "POST", "/api/v1/orgs", &structs.CreateOrgOption{
UserName: orgName,
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusCreated)
owner, err := org_model.GetOrgByName(t.Context(), orgName)
require.NoError(t, err)
// 2. Create Two Repositories in owner
createRepoInOrg := func(name string) int64 {
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/orgs/%s/repos", orgName), &structs.CreateRepoOption{
Name: name,
AutoInit: true,
}).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusCreated)
var repo structs.Repository
DecodeJSON(t, resp, &repo)
return repo.ID
}
repoAID := createRepoInOrg("repo-A")
repoBID := createRepoInOrg("repo-B")
// 3. Enable Actions in Repo A (Source) and Repo B (Target)
enableActions := func(repoID int64) {
err := db.Insert(t.Context(), &repo_model.RepoUnit{
RepoID: repoID,
Type: unit_model.TypeActions,
Config: &repo_model.ActionsConfig{
TokenPermissionMode: repo_model.ActionsTokenPermissionModePermissive,
},
})
require.NoError(t, err)
}
enableActions(repoAID)
enableActions(repoBID)
// 4. Create Task in Repo A, and use A's token to access B
taskA := createActionTask(t, repoAID, false)
testCtxA := APITestContext{
Session: emptyTestSession(t),
Token: taskA.Token,
Username: orgName,
Reponame: "repo-B",
}
testCtxA.ExpectedCode = http.StatusOK
t.Run("PublicCrossRepoAccess", doAPIGetRepository(testCtxA, func(t *testing.T, r structs.Repository) {
assert.Equal(t, "repo-B", r.Name)
}))
// make repo-B be private
req = NewRequestWithJSON(t, "PATCH", "/api/v1/repos/org-cross-test/repo-B", &structs.EditRepoOption{Private: new(true)}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
testCtxA.ExpectedCode = http.StatusNotFound
t.Run("NoPrivateCrossRepoAccess", doAPIGetRepository(testCtxA, nil))
ownerActionsCfg := actions_model.OwnerActionsConfig{AllowedCrossRepoIDs: []int64{repoBID}}
require.NoError(t, actions_model.SetOwnerActionsConfig(t.Context(), owner.ID, ownerActionsCfg))
testCtxA.ExpectedCode = http.StatusOK
t.Run("AccessToSelectedPrivateRepo", doAPIGetRepository(testCtxA, func(t *testing.T, r structs.Repository) {
assert.Equal(t, "repo-B", r.Name)
}))
t.Run("RepoTransfer", func(t *testing.T) {
ownerActionsCfg, err := actions_model.GetOwnerActionsConfig(t.Context(), owner.ID)
require.NoError(t, err)
assert.Contains(t, ownerActionsCfg.AllowedCrossRepoIDs, repoBID)
// Transfer Repository to user4
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/repo-B/transfer", orgName), &structs.TransferRepoOption{
NewOwner: "user4",
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusCreated)
// Accept transfer as user4
session4 := loginUser(t, "user4")
token4 := getTokenForLoggedInUser(t, session4, auth_model.AccessTokenScopeWriteUser, auth_model.AccessTokenScopeWriteRepository)
req = NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/repo-B/transfer/accept", orgName)).AddTokenAuth(token4)
MakeRequest(t, req, http.StatusAccepted)
// Verify it is removed from the org's config
ownerActionsCfg, err = actions_model.GetOwnerActionsConfig(t.Context(), owner.ID)
require.NoError(t, err)
assert.NotContains(t, ownerActionsCfg.AllowedCrossRepoIDs, repoBID)
})
})
}
func TestActionsJobTokenPermissions(t *testing.T) {
defer tests.PrepareTestEnv(t)()
t.Run("WriteIssue", TestActionsJobTokenPermissionsWriteIssue)
}
func TestActionsJobTokenPermissionsWriteIssue(t *testing.T) {
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
task := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 53})
require.Equal(t, repo.ID, task.RepoID)
require.NoError(t, db.Insert(t.Context(), &repo_model.RepoUnit{
RepoID: repo.ID,
Type: unit_model.TypeActions,
Config: &repo_model.ActionsConfig{},
}))
repoActionsUnit := repo.MustGetUnit(t.Context(), unit_model.TypeActions)
repoActionsCfg := repoActionsUnit.ActionsConfig()
repoActionsCfg.OverrideOwnerConfig = true
repoActionsCfg.TokenPermissionMode = repo_model.ActionsTokenPermissionModePermissive
repoActionsCfg.MaxTokenPermissions = nil
require.NoError(t, repo_model.UpdateRepoUnitConfig(t.Context(), repoActionsUnit))
require.NoError(t, task.GenerateToken())
task.Status = actions_model.StatusRunning
require.NoError(t, actions_model.UpdateTask(t.Context(), task, "token_hash", "token_salt", "token_last_eight", "status"))
session := loginUser(t, user.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue, auth_model.AccessTokenScopeWriteRepository)
labelURL := fmt.Sprintf("/api/v1/repos/%s/%s/labels", user.Name, repo.Name)
req := NewRequestWithJSON(t, "POST", labelURL, &structs.CreateLabelOption{
Name: "task-label",
Color: "0e8a16",
}).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusCreated)
label := DecodeJSON(t, resp, &structs.Label{})
issueURL := fmt.Sprintf("/api/v1/repos/%s/%s/issues", user.Name, repo.Name)
req = NewRequestWithJSON(t, "POST", issueURL, &structs.CreateIssueOption{
Title: "issue for actions token label deletion",
}).AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusCreated)
issue := DecodeJSON(t, resp, &structs.Issue{})
taskToken := task.Token
require.NotEmpty(t, taskToken)
issueLabelsURL := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/labels", user.Name, repo.Name, issue.Index)
req = NewRequestWithJSON(t, "POST", issueLabelsURL, &structs.IssueLabelsOption{
Labels: []any{label.ID},
}).AddTokenAuth(taskToken)
MakeRequest(t, req, http.StatusOK)
req = NewRequest(t, "DELETE", fmt.Sprintf("%s/%d", issueLabelsURL, label.ID)).AddTokenAuth(taskToken)
MakeRequest(t, req, http.StatusNoContent)
}
func createActionTask(t *testing.T, repoID int64, isFork bool) *actions_model.ActionTask {
job := &actions_model.ActionRunJob{
RepoID: repoID,
Status: actions_model.StatusRunning,
IsForkPullRequest: isFork,
JobID: "test_job",
Name: "test_job",
}
require.NoError(t, db.Insert(t.Context(), job))
task := &actions_model.ActionTask{
JobID: job.ID,
RepoID: repoID,
Status: actions_model.StatusRunning,
IsForkPullRequest: isFork,
}
require.NoError(t, task.GenerateToken())
require.NoError(t, db.Insert(t.Context(), task))
return task
}
func TestActionsTokenPermissionsPersistenceWithWorkflow(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
session := loginUser(t, user2.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
// create repos
repo1 := createActionsTestRepo(t, token, "actions-permission-repo1", false)
repo2 := createActionsTestRepo(t, token, "actions-permission-repo2", true)
// add repo2 to owner-level cross-repo access list
req := NewRequestWithValues(t, "POST", "/user/settings/actions/general", map[string]string{
"cross_repo_add_target": "true",
"cross_repo_add_target_name": repo2.Name,
})
session.MakeRequest(t, req, http.StatusOK)
// create the runner for repo1
runner1 := newMockRunner()
runner1.registerAsRepoRunner(t, user2.Name, repo1.Name, "mock-runner", []string{"ubuntu-latest"}, false)
// set repo1 actions token permission mode to "permissive"
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/actions/general/token_permissions", user2.Name, repo1.Name), map[string]string{
"token_permission_mode": "permissive",
"override_owner_config": "true",
})
session.MakeRequest(t, req, http.StatusSeeOther)
// set repo2 actions token permission mode to "restricted", and set max permissions
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/actions/general/token_permissions", user2.Name, repo2.Name), map[string]string{
"token_permission_mode": "restricted",
"override_owner_config": "true",
"enable_max_permissions": "true",
"max_unit_access_mode_" + strconv.Itoa(int(unit_model.TypeReleases)): "read",
})
session.MakeRequest(t, req, http.StatusSeeOther)
// create a workflow file with "permission" keyword for repo1
wfTreePath := ".gitea/workflows/test_permissions.yml"
wfFileContent := `name: Test Permissions
on:
push:
paths:
- '.gitea/workflows/test_permissions.yml'
jobs:
job-override:
runs-on: ubuntu-latest
permissions:
code: write
steps:
- run: echo "test perms"
`
opts := getWorkflowCreateFileOptions(user2, repo1.DefaultBranch, "create "+wfTreePath, wfFileContent)
createWorkflowFile(t, token, user2.Name, repo1.Name, wfTreePath, opts)
task1 := runner1.fetchTask(t)
task1Token := task1.Secrets["GITEA_TOKEN"]
require.NotEmpty(t, task1Token)
// should fail: target repo does not allow code access
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s", user2.Name, repo2.Name)).AddTokenAuth(task1Token)
MakeRequest(t, req, http.StatusNotFound)
// set repo2 max permission to "read" so that the actions token can access code
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/actions/general/token_permissions", user2.Name, repo2.Name), map[string]string{
"token_permission_mode": "restricted",
"override_owner_config": "true",
"enable_max_permissions": "true",
"max_unit_access_mode_" + strconv.Itoa(int(unit_model.TypeCode)): "read",
"max_unit_access_mode_" + strconv.Itoa(int(unit_model.TypeReleases)): "read",
})
session.MakeRequest(t, req, http.StatusSeeOther)
// should succeed: target repo now allows code read access for this token
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s", user2.Name, repo2.Name)).AddTokenAuth(task1Token)
MakeRequest(t, req, http.StatusOK)
// but it should not have write access
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/%s/%s.git/info/lfs/objects/batch", user2.Name, repo2.Name), lfs.BatchRequest{Operation: "upload"}).
SetHeader("Accept", lfs.MediaType).
AddBasicAuth("gitea-actions", task1Token)
MakeRequest(t, req, http.StatusUnauthorized)
// set repo1&repo2 max permission to "write" so that the actions token can access code
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/actions/general/token_permissions", user2.Name, repo1.Name), map[string]string{
"token_permission_mode": "restricted",
"override_owner_config": "true",
"enable_max_permissions": "true",
"max_unit_access_mode_" + strconv.Itoa(int(unit_model.TypeCode)): "write",
})
session.MakeRequest(t, req, http.StatusSeeOther)
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/actions/general/token_permissions", user2.Name, repo2.Name), map[string]string{
"token_permission_mode": "restricted",
"override_owner_config": "true",
"enable_max_permissions": "true",
"max_unit_access_mode_" + strconv.Itoa(int(unit_model.TypeCode)): "write",
})
session.MakeRequest(t, req, http.StatusSeeOther)
// now task1 has write access to repo1, but still only read access to repo2 (different repo)
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/%s/%s.git/info/lfs/objects/batch", user2.Name, repo1.Name), lfs.BatchRequest{Operation: "upload"}).
SetHeader("Accept", lfs.MediaType).
AddBasicAuth("gitea-actions", task1Token)
MakeRequest(t, req, http.StatusOK)
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/%s/%s.git/info/lfs/objects/batch", user2.Name, repo2.Name), lfs.BatchRequest{Operation: "upload"}).
SetHeader("Accept", lfs.MediaType).
AddBasicAuth("gitea-actions", task1Token)
MakeRequest(t, req, http.StatusUnauthorized)
})
}