|
|
|
|
@@ -5,14 +5,23 @@ 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"
|
|
|
|
|
|
|
|
|
|
@@ -20,98 +29,388 @@ import (
|
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func TestActionsJobTokenAccess(t *testing.T) {
|
|
|
|
|
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
|
|
|
|
t.Run("Write Access", testActionsJobTokenAccess(u, false))
|
|
|
|
|
t.Run("Read Access", testActionsJobTokenAccess(u, true))
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
func TestActionsJobTokenPermissiveAccess(t *testing.T) {
|
|
|
|
|
cases := []struct {
|
|
|
|
|
name string
|
|
|
|
|
isFork bool
|
|
|
|
|
|
|
|
|
|
func testActionsJobTokenAccess(u *url.URL, isFork bool) func(t *testing.T) {
|
|
|
|
|
return func(t *testing.T) {
|
|
|
|
|
task := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 47})
|
|
|
|
|
require.NoError(t, task.GenerateToken())
|
|
|
|
|
task.Status = actions_model.StatusRunning
|
|
|
|
|
task.IsForkPullRequest = isFork
|
|
|
|
|
err := actions_model.UpdateTask(t.Context(), task, "token_hash", "token_salt", "token_last_eight", "status", "is_fork_pull_request")
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
session := emptyTestSession(t)
|
|
|
|
|
context := APITestContext{
|
|
|
|
|
Session: session,
|
|
|
|
|
Token: task.Token,
|
|
|
|
|
Username: "user5",
|
|
|
|
|
Reponame: "repo4",
|
|
|
|
|
}
|
|
|
|
|
dstPath := t.TempDir()
|
|
|
|
|
ownerPermMode repo_model.ActionsTokenPermissionMode
|
|
|
|
|
ownerMaxPerms map[unit_model.Type]perm.AccessMode
|
|
|
|
|
|
|
|
|
|
u.Path = context.GitPath()
|
|
|
|
|
u.User = url.UserPassword("gitea-actions", task.Token)
|
|
|
|
|
repoPermMode repo_model.ActionsTokenPermissionMode
|
|
|
|
|
repoMaxPerms map[unit_model.Type]perm.AccessMode
|
|
|
|
|
|
|
|
|
|
t.Run("Git Clone", doGitClone(dstPath, u))
|
|
|
|
|
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,
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
t.Run("API Get Repository", doAPIGetRepository(context, func(t *testing.T, r structs.Repository) {
|
|
|
|
|
require.Equal(t, "repo4", r.Name)
|
|
|
|
|
require.Equal(t, "user5", r.Owner.UserName)
|
|
|
|
|
}))
|
|
|
|
|
// 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,
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
context.ExpectedCode = util.Iif(isFork, http.StatusForbidden, http.StatusCreated)
|
|
|
|
|
t.Run("API Create File", doAPICreateFile(context, "test.txt", &structs.CreateFileOptions{
|
|
|
|
|
FileOptions: structs.FileOptions{
|
|
|
|
|
NewBranchName: "new-branch",
|
|
|
|
|
Message: "Create File",
|
|
|
|
|
},
|
|
|
|
|
ContentBase64: base64.StdEncoding.EncodeToString([]byte(`This is a test file created using job token.`)),
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
context.ExpectedCode = http.StatusForbidden
|
|
|
|
|
t.Run("Fail to Create Repository", doAPICreateRepository(context, true))
|
|
|
|
|
|
|
|
|
|
context.ExpectedCode = http.StatusForbidden
|
|
|
|
|
t.Run("Fail to Delete Repository", doAPIDeleteRepository(context))
|
|
|
|
|
|
|
|
|
|
t.Run("Fail to Create Organization", doAPICreateOrganization(context, &structs.CreateOrgOption{
|
|
|
|
|
UserName: "actions",
|
|
|
|
|
FullName: "Gitea Actions",
|
|
|
|
|
}))
|
|
|
|
|
// 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,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestActionsJobTokenAccessLFS(t *testing.T) {
|
|
|
|
|
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
|
|
|
|
httpContext := NewAPITestContext(t, "user2", "repo-lfs-test", auth_model.AccessTokenScopeWriteUser, auth_model.AccessTokenScopeWriteRepository)
|
|
|
|
|
t.Run("Create Repository", doAPICreateRepository(httpContext, false, func(t *testing.T, repository structs.Repository) {
|
|
|
|
|
task := &actions_model.ActionTask{}
|
|
|
|
|
require.NoError(t, task.GenerateToken())
|
|
|
|
|
task.Status = actions_model.StatusRunning
|
|
|
|
|
task.IsForkPullRequest = false
|
|
|
|
|
task.RepoID = repository.ID
|
|
|
|
|
err := db.Insert(t.Context(), task)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
session := emptyTestSession(t)
|
|
|
|
|
httpContext := APITestContext{
|
|
|
|
|
Session: session,
|
|
|
|
|
Token: task.Token,
|
|
|
|
|
Username: "user2",
|
|
|
|
|
Reponame: "repo-lfs-test",
|
|
|
|
|
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))
|
|
|
|
|
|
|
|
|
|
u.Path = httpContext.GitPath()
|
|
|
|
|
dstPath := t.TempDir()
|
|
|
|
|
// 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))
|
|
|
|
|
|
|
|
|
|
u.Path = httpContext.GitPath()
|
|
|
|
|
u.User = url.UserPassword("gitea-actions", task.Token)
|
|
|
|
|
// 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)
|
|
|
|
|
|
|
|
|
|
t.Run("Clone", doGitClone(dstPath, u))
|
|
|
|
|
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"))
|
|
|
|
|
|
|
|
|
|
dstPath2 := t.TempDir()
|
|
|
|
|
testURL := *u
|
|
|
|
|
testURL.User = url.UserPassword("gitea-actions", task.Token)
|
|
|
|
|
|
|
|
|
|
t.Run("Partial Clone", doPartialGitClone(dstPath2, u))
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
lfs := lfsCommitAndPushTest(t, dstPath, testFileSizeSmall)[0]
|
|
|
|
|
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)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
reqLFS := NewRequest(t, "GET", "/api/v1/repos/user2/repo-lfs-test/media/"+lfs).AddTokenAuth(task.Token)
|
|
|
|
|
respLFS := MakeRequestNilResponseRecorder(t, reqLFS, http.StatusOK)
|
|
|
|
|
assert.Equal(t, testFileSizeSmall, respLFS.Length)
|
|
|
|
|
}))
|
|
|
|
|
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 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)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|