From 08b97769702bf426859e1a83625369b46b7e4ce1 Mon Sep 17 00:00:00 2001 From: ChristopherHX Date: Wed, 22 Oct 2025 13:12:31 +0200 Subject: [PATCH] Refactor Actions Token Access (#35688) * use a single function to do Action Tokens Permission checks * allows easier customization * add basic tests * lfs file locks should work now --------- Signed-off-by: ChristopherHX Co-authored-by: wxiaoguang --- models/git/lfs.go | 31 ------ models/git/lfs_lock.go | 30 ----- models/perm/access/repo_permission.go | 30 +++++ models/user/user.go | 7 +- models/user/user_system.go | 21 ++-- models/user/user_test.go | 39 ++++--- routers/api/v1/api.go | 19 +--- routers/api/v1/repo/repo.go | 3 + routers/web/repo/githttp.go | 25 +---- services/convert/convert.go | 2 +- services/lfs/locks.go | 14 --- services/lfs/server.go | 25 ++--- tests/integration/actions_job_token_test.go | 117 ++++++++++++++++++++ tests/integration/api_repo_file_get_test.go | 6 +- tests/integration/api_repo_lfs_test.go | 17 +-- 15 files changed, 218 insertions(+), 168 deletions(-) create mode 100644 tests/integration/actions_job_token_test.go diff --git a/models/git/lfs.go b/models/git/lfs.go index 8bba060ff9..a4ae3e7bee 100644 --- a/models/git/lfs.go +++ b/models/git/lfs.go @@ -8,7 +8,6 @@ import ( "fmt" "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/models/perm" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" @@ -42,30 +41,6 @@ func (err ErrLFSLockNotExist) Unwrap() error { return util.ErrNotExist } -// ErrLFSUnauthorizedAction represents a "LFSUnauthorizedAction" kind of error. -type ErrLFSUnauthorizedAction struct { - RepoID int64 - UserName string - Mode perm.AccessMode -} - -// IsErrLFSUnauthorizedAction checks if an error is a ErrLFSUnauthorizedAction. -func IsErrLFSUnauthorizedAction(err error) bool { - _, ok := err.(ErrLFSUnauthorizedAction) - return ok -} - -func (err ErrLFSUnauthorizedAction) Error() string { - if err.Mode == perm.AccessModeWrite { - return fmt.Sprintf("User %s doesn't have write access for lfs lock [rid: %d]", err.UserName, err.RepoID) - } - return fmt.Sprintf("User %s doesn't have read access for lfs lock [rid: %d]", err.UserName, err.RepoID) -} - -func (err ErrLFSUnauthorizedAction) Unwrap() error { - return util.ErrPermissionDenied -} - // ErrLFSLockAlreadyExist represents a "LFSLockAlreadyExist" kind of error. type ErrLFSLockAlreadyExist struct { RepoID int64 @@ -93,12 +68,6 @@ type ErrLFSFileLocked struct { UserName string } -// IsErrLFSFileLocked checks if an error is a ErrLFSFileLocked. -func IsErrLFSFileLocked(err error) bool { - _, ok := err.(ErrLFSFileLocked) - return ok -} - func (err ErrLFSFileLocked) Error() string { return fmt.Sprintf("File is lfs locked [repo: %d, locked by: %s, path: %s]", err.RepoID, err.UserName, err.Path) } diff --git a/models/git/lfs_lock.go b/models/git/lfs_lock.go index c5f9a4e6de..184e616915 100644 --- a/models/git/lfs_lock.go +++ b/models/git/lfs_lock.go @@ -11,10 +11,7 @@ import ( "time" "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/models/perm" - access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" - "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" @@ -71,10 +68,6 @@ func (l *LFSLock) LoadOwner(ctx context.Context) error { // CreateLFSLock creates a new lock. func CreateLFSLock(ctx context.Context, repo *repo_model.Repository, lock *LFSLock) (*LFSLock, error) { return db.WithTx2(ctx, func(ctx context.Context) (*LFSLock, error) { - if err := CheckLFSAccessForRepo(ctx, lock.OwnerID, repo, perm.AccessModeWrite); err != nil { - return nil, err - } - lock.Path = util.PathJoinRel(lock.Path) lock.RepoID = repo.ID @@ -165,10 +158,6 @@ func DeleteLFSLockByID(ctx context.Context, id int64, repo *repo_model.Repositor return nil, err } - if err := CheckLFSAccessForRepo(ctx, u.ID, repo, perm.AccessModeWrite); err != nil { - return nil, err - } - if !force && u.ID != lock.OwnerID { return nil, errors.New("user doesn't own lock and force flag is not set") } @@ -180,22 +169,3 @@ func DeleteLFSLockByID(ctx context.Context, id int64, repo *repo_model.Repositor return lock, nil }) } - -// CheckLFSAccessForRepo check needed access mode base on action -func CheckLFSAccessForRepo(ctx context.Context, ownerID int64, repo *repo_model.Repository, mode perm.AccessMode) error { - if ownerID == 0 { - return ErrLFSUnauthorizedAction{repo.ID, "undefined", mode} - } - u, err := user_model.GetUserByID(ctx, ownerID) - if err != nil { - return err - } - perm, err := access_model.GetUserRepoPermission(ctx, repo, u) - if err != nil { - return err - } - if !perm.CanAccess(mode, unit.TypeCode) { - return ErrLFSUnauthorizedAction{repo.ID, u.DisplayName(), mode} - } - return nil -} diff --git a/models/perm/access/repo_permission.go b/models/perm/access/repo_permission.go index 678b18442e..df96db8d5a 100644 --- a/models/perm/access/repo_permission.go +++ b/models/perm/access/repo_permission.go @@ -5,9 +5,11 @@ package access import ( "context" + "errors" "fmt" "slices" + actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/organization" perm_model "code.gitea.io/gitea/models/perm" @@ -253,6 +255,34 @@ func finalProcessRepoUnitPermission(user *user_model.User, perm *Permission) { } } +// GetActionsUserRepoPermission returns the actions user permissions to the repository +func GetActionsUserRepoPermission(ctx context.Context, repo *repo_model.Repository, actionsUser *user_model.User, taskID int64) (perm Permission, err error) { + if actionsUser.ID != user_model.ActionsUserID { + return perm, errors.New("api GetActionsUserRepoPermission can only be called by the actions user") + } + task, err := actions_model.GetTaskByID(ctx, taskID) + if err != nil { + return perm, err + } + if task.RepoID != repo.ID { + // FIXME allow public repo read access if tokenless pull is enabled + return perm, nil + } + + var accessMode perm_model.AccessMode + if task.IsForkPullRequest { + accessMode = perm_model.AccessModeRead + } else { + accessMode = perm_model.AccessModeWrite + } + + if err := repo.LoadUnits(ctx); err != nil { + return perm, err + } + perm.SetUnitsWithDefaultAccessMode(repo.Units, accessMode) + return perm, nil +} + // GetUserRepoPermission returns the user permissions to the repository func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, user *user_model.User) (perm Permission, err error) { defer func() { diff --git a/models/user/user.go b/models/user/user.go index 80d5eb5ec4..3583694cf9 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -249,8 +249,13 @@ func (u *User) MaxCreationLimit() int { } // CanCreateRepoIn checks whether the doer(u) can create a repository in the owner -// NOTE: functions calling this assume a failure due to repository count limit; it ONLY checks the repo number LIMIT, if new checks are added, those functions should be revised +// NOTE: functions calling this assume a failure due to repository count limit, or the owner is not a real user. +// It ONLY checks the repo number LIMIT or whether owner user is real. If new checks are added, those functions should be revised. +// TODO: the callers can only return ErrReachLimitOfRepo, need to fine tune to support other error types in the future. func (u *User) CanCreateRepoIn(owner *User) bool { + if u.ID <= 0 || owner.ID <= 0 { + return false // fake user like Ghost or Actions user + } if u.IsAdmin { return true } diff --git a/models/user/user_system.go b/models/user/user_system.go index e07274d291..11008c77d4 100644 --- a/models/user/user_system.go +++ b/models/user/user_system.go @@ -48,17 +48,16 @@ func IsGiteaActionsUserName(name string) bool { // NewActionsUser creates and returns a fake user for running the actions. func NewActionsUser() *User { return &User{ - ID: ActionsUserID, - Name: ActionsUserName, - LowerName: ActionsUserName, - IsActive: true, - FullName: "Gitea Actions", - Email: ActionsUserEmail, - KeepEmailPrivate: true, - LoginName: ActionsUserName, - Type: UserTypeBot, - AllowCreateOrganization: true, - Visibility: structs.VisibleTypePublic, + ID: ActionsUserID, + Name: ActionsUserName, + LowerName: ActionsUserName, + IsActive: true, + FullName: "Gitea Actions", + Email: ActionsUserEmail, + KeepEmailPrivate: true, + LoginName: ActionsUserName, + Type: UserTypeBot, + Visibility: structs.VisibleTypePublic, } } diff --git a/models/user/user_test.go b/models/user/user_test.go index 4201ec4816..6a530553d7 100644 --- a/models/user/user_test.go +++ b/models/user/user_test.go @@ -648,33 +648,36 @@ func TestGetInactiveUsers(t *testing.T) { func TestCanCreateRepo(t *testing.T) { defer test.MockVariableValue(&setting.Repository.MaxCreationLimit)() const noLimit = -1 - doerNormal := &user_model.User{} - doerAdmin := &user_model.User{IsAdmin: true} + doerActions := user_model.NewActionsUser() + doerNormal := &user_model.User{ID: 2} + doerAdmin := &user_model.User{ID: 1, IsAdmin: true} t.Run("NoGlobalLimit", func(t *testing.T) { setting.Repository.MaxCreationLimit = noLimit - assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: noLimit})) - assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 0})) - assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 100})) + assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 0})) + assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 100})) + assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: noLimit})) - assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: noLimit})) - assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 0})) - assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 100})) + assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 0})) + assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 100})) + assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: noLimit})) + assert.False(t, doerActions.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: noLimit})) + assert.False(t, doerAdmin.CanCreateRepoIn(doerActions)) }) t.Run("GlobalLimit50", func(t *testing.T) { setting.Repository.MaxCreationLimit = 50 - assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: noLimit})) - assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 60, MaxRepoCreation: noLimit})) // limited by global limit - assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 0})) - assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 100})) - assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 60, MaxRepoCreation: 100})) + assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: noLimit})) + assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 60, MaxRepoCreation: noLimit})) // limited by global limit + assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 0})) + assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 100})) + assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 60, MaxRepoCreation: 100})) - assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: noLimit})) - assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 60, MaxRepoCreation: noLimit})) - assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 0})) - assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 100})) - assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 60, MaxRepoCreation: 100})) + assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: noLimit})) + assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 60, MaxRepoCreation: noLimit})) + assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 0})) + assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 100})) + assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 60, MaxRepoCreation: 100})) }) } diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 5f52ee8a43..e6238acce0 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -70,7 +70,6 @@ import ( "net/http" "strings" - actions_model "code.gitea.io/gitea/models/actions" auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/perm" @@ -190,27 +189,11 @@ func repoAssignment() func(ctx *context.APIContext) { if ctx.Doer != nil && ctx.Doer.ID == user_model.ActionsUserID { taskID := ctx.Data["ActionsTaskID"].(int64) - task, err := actions_model.GetTaskByID(ctx, taskID) + ctx.Repo.Permission, err = access_model.GetActionsUserRepoPermission(ctx, repo, ctx.Doer, taskID) if err != nil { ctx.APIErrorInternal(err) return } - if task.RepoID != repo.ID { - ctx.APIErrorNotFound() - return - } - - if task.IsForkPullRequest { - ctx.Repo.Permission.AccessMode = perm.AccessModeRead - } else { - ctx.Repo.Permission.AccessMode = perm.AccessModeWrite - } - - if err := ctx.Repo.Repository.LoadUnits(ctx); err != nil { - ctx.APIErrorInternal(err) - return - } - ctx.Repo.Permission.SetUnitsWithDefaultAccessMode(ctx.Repo.Repository.Units, ctx.Repo.Permission.AccessMode) } else { needTwoFactor, err := doerNeedTwoFactorAuth(ctx, ctx.Doer) if err != nil { diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index 2f5a969b6b..bb6bda587d 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -28,6 +28,7 @@ import ( repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/validation" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/api/v1/utils" @@ -270,6 +271,8 @@ func CreateUserRepo(ctx *context.APIContext, owner *user_model.User, opt api.Cre db.IsErrNamePatternNotAllowed(err) || label.IsErrTemplateLoad(err) { ctx.APIError(http.StatusUnprocessableEntity, err) + } else if errors.Is(err, util.ErrPermissionDenied) { + ctx.APIError(http.StatusForbidden, err) } else { ctx.APIErrorInternal(err) } diff --git a/routers/web/repo/githttp.go b/routers/web/repo/githttp.go index 69b93dd060..1b1c272a8d 100644 --- a/routers/web/repo/githttp.go +++ b/routers/web/repo/githttp.go @@ -18,7 +18,6 @@ import ( "sync" "time" - actions_model "code.gitea.io/gitea/models/actions" auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/perm" access_model "code.gitea.io/gitea/models/perm/access" @@ -190,29 +189,17 @@ func httpBase(ctx *context.Context) *serviceHandler { if ctx.Data["IsActionsToken"] == true { taskID := ctx.Data["ActionsTaskID"].(int64) - task, err := actions_model.GetTaskByID(ctx, taskID) + p, err := access_model.GetActionsUserRepoPermission(ctx, repo, ctx.Doer, taskID) if err != nil { - ctx.ServerError("GetTaskByID", err) - return nil - } - if task.RepoID != repo.ID { - ctx.PlainText(http.StatusForbidden, "User permission denied") + ctx.ServerError("GetUserRepoPermission", err) return nil } - if task.IsForkPullRequest { - if accessMode > perm.AccessModeRead { - ctx.PlainText(http.StatusForbidden, "User permission denied") - return nil - } - environ = append(environ, fmt.Sprintf("%s=%d", repo_module.EnvActionPerm, perm.AccessModeRead)) - } else { - if accessMode > perm.AccessModeWrite { - ctx.PlainText(http.StatusForbidden, "User permission denied") - return nil - } - environ = append(environ, fmt.Sprintf("%s=%d", repo_module.EnvActionPerm, perm.AccessModeWrite)) + if !p.CanAccess(accessMode, unitType) { + ctx.PlainText(http.StatusNotFound, "Repository not found") + return nil } + environ = append(environ, fmt.Sprintf("%s=%d", repo_module.EnvActionPerm, p.UnitAccessMode(unitType))) } else { p, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer) if err != nil { diff --git a/services/convert/convert.go b/services/convert/convert.go index 0de3822140..9f8fff970c 100644 --- a/services/convert/convert.go +++ b/services/convert/convert.go @@ -771,7 +771,7 @@ func ToOAuth2Application(app *auth.OAuth2Application) *api.OAuth2Application { // ToLFSLock convert a LFSLock to api.LFSLock func ToLFSLock(ctx context.Context, l *git_model.LFSLock) *api.LFSLock { - u, err := user_model.GetUserByID(ctx, l.OwnerID) + u, err := user_model.GetPossibleUserByID(ctx, l.OwnerID) if err != nil { return nil } diff --git a/services/lfs/locks.go b/services/lfs/locks.go index 264001f0f9..5bc3f6b95a 100644 --- a/services/lfs/locks.go +++ b/services/lfs/locks.go @@ -187,13 +187,6 @@ func PostLockHandler(ctx *context.Context) { }) return } - if git_model.IsErrLFSUnauthorizedAction(err) { - ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="gitea-lfs"`) - ctx.JSON(http.StatusUnauthorized, api.LFSLockError{ - Message: "You must have push access to create locks : " + err.Error(), - }) - return - } log.Error("Unable to CreateLFSLock in repository %-v at %s for user %-v: Error: %v", repository, req.Path, ctx.Doer, err) ctx.JSON(http.StatusInternalServerError, api.LFSLockError{ Message: "internal server error : Internal Server Error", @@ -317,13 +310,6 @@ func UnLockHandler(ctx *context.Context) { lock, err := git_model.DeleteLFSLockByID(ctx, ctx.PathParamInt64("lid"), repository, ctx.Doer, req.Force) if err != nil { - if git_model.IsErrLFSUnauthorizedAction(err) { - ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="gitea-lfs"`) - ctx.JSON(http.StatusUnauthorized, api.LFSLockError{ - Message: "You must have push access to delete locks : " + err.Error(), - }) - return - } log.Error("Unable to DeleteLFSLockByID[%d] by user %-v with force %t: Error: %v", ctx.PathParamInt64("lid"), ctx.Doer, req.Force, err) ctx.JSON(http.StatusInternalServerError, api.LFSLockError{ Message: "unable to delete lock : Internal Server Error", diff --git a/services/lfs/server.go b/services/lfs/server.go index 4a1e0aaf7d..81991de434 100644 --- a/services/lfs/server.go +++ b/services/lfs/server.go @@ -20,7 +20,6 @@ import ( "strings" "time" - actions_model "code.gitea.io/gitea/models/actions" auth_model "code.gitea.io/gitea/models/auth" git_model "code.gitea.io/gitea/models/git" perm_model "code.gitea.io/gitea/models/perm" @@ -549,33 +548,31 @@ func authenticate(ctx *context.Context, repository *repo_model.Repository, autho if ctx.Data["IsActionsToken"] == true { taskID := ctx.Data["ActionsTaskID"].(int64) - task, err := actions_model.GetTaskByID(ctx, taskID) + perm, err := access_model.GetActionsUserRepoPermission(ctx, repository, ctx.Doer, taskID) if err != nil { - log.Error("Unable to GetTaskByID for task[%d] Error: %v", taskID, err) + log.Error("Unable to GetActionsUserRepoPermission for task[%d] Error: %v", taskID, err) return false } - if task.RepoID != repository.ID { - return false - } - - if task.IsForkPullRequest { - return accessMode <= perm_model.AccessModeRead - } - return accessMode <= perm_model.AccessModeWrite + return perm.CanAccess(accessMode, unit.TypeCode) } - // ctx.IsSigned is unnecessary here, this will be checked in perm.CanAccess + // it works for both anonymous request and signed-in user, then perm.CanAccess will do the permission check perm, err := access_model.GetUserRepoPermission(ctx, repository, ctx.Doer) if err != nil { log.Error("Unable to GetUserRepoPermission for user %-v in repo %-v Error: %v", ctx.Doer, repository, err) return false } - canRead := perm.CanAccess(accessMode, unit.TypeCode) - if canRead && (!requireSigned || ctx.IsSigned) { + canAccess := perm.CanAccess(accessMode, unit.TypeCode) + // if it doesn't require sign-in and anonymous user has access, return true + // if the user is already signed in (for example: by session auth method), and the doer can access, return true + if canAccess && (!requireSigned || ctx.IsSigned) { return true } + // now, either sign-in is required or the ctx.Doer cannot access, check the LFS token + // however, "ctx.Doer exists but cannot access then check LFS token" should not really happen: + // * why a request can be sent with both valid user session and valid LFS token then use LFS token to access? user, err := parseToken(ctx, authorization, repository, accessMode) if err != nil { // Most of these are Warn level - the true internal server errors are logged in parseToken already diff --git a/tests/integration/actions_job_token_test.go b/tests/integration/actions_job_token_test.go new file mode 100644 index 0000000000..c4e8e880eb --- /dev/null +++ b/tests/integration/actions_job_token_test.go @@ -0,0 +1,117 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "encoding/base64" + "net/http" + "net/url" + "testing" + + actions_model "code.gitea.io/gitea/models/actions" + auth_model "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" + + "github.com/stretchr/testify/assert" + "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 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() + + u.Path = context.GitPath() + u.User = url.UserPassword("gitea-actions", task.Token) + + t.Run("Git Clone", doGitClone(dstPath, u)) + + 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) + })) + + 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", + })) + } +} + +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", + } + + u.Path = httpContext.GitPath() + dstPath := t.TempDir() + + u.Path = httpContext.GitPath() + u.User = url.UserPassword("gitea-actions", task.Token) + + t.Run("Clone", doGitClone(dstPath, u)) + + dstPath2 := t.TempDir() + + t.Run("Partial Clone", doPartialGitClone(dstPath2, u)) + + lfs := lfsCommitAndPushTest(t, dstPath, testFileSizeSmall)[0] + + 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) + })) + }) +} diff --git a/tests/integration/api_repo_file_get_test.go b/tests/integration/api_repo_file_get_test.go index 379851b689..ec50cf52f4 100644 --- a/tests/integration/api_repo_file_get_test.go +++ b/tests/integration/api_repo_file_get_test.go @@ -9,7 +9,6 @@ import ( "testing" auth_model "code.gitea.io/gitea/models/auth" - api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" @@ -25,8 +24,9 @@ func TestAPIGetRawFileOrLFS(t *testing.T) { // Test with LFS onGiteaRun(t, func(t *testing.T, u *url.URL) { + createLFSTestRepository(t, "repo-lfs-test") httpContext := NewAPITestContext(t, "user2", "repo-lfs-test", auth_model.AccessTokenScopeWriteRepository) - doAPICreateRepository(httpContext, false, func(t *testing.T, repository api.Repository) { + t.Run("repo-lfs-test", func(t *testing.T) { u.Path = httpContext.GitPath() dstPath := t.TempDir() @@ -41,7 +41,7 @@ func TestAPIGetRawFileOrLFS(t *testing.T) { lfs := lfsCommitAndPushTest(t, dstPath, testFileSizeSmall)[0] - reqLFS := NewRequest(t, "GET", "/api/v1/repos/user2/repo1/media/"+lfs) + reqLFS := NewRequest(t, "GET", "/api/v1/repos/user2/repo-lfs-test/media/"+lfs).AddTokenAuth(httpContext.Token) respLFS := MakeRequestNilResponseRecorder(t, reqLFS, http.StatusOK) assert.Equal(t, testFileSizeSmall, respLFS.Length) }) diff --git a/tests/integration/api_repo_lfs_test.go b/tests/integration/api_repo_lfs_test.go index da4c89a1b7..fb55d311cc 100644 --- a/tests/integration/api_repo_lfs_test.go +++ b/tests/integration/api_repo_lfs_test.go @@ -23,6 +23,7 @@ import ( "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestAPILFSNotStarted(t *testing.T) { @@ -59,12 +60,12 @@ func TestAPILFSMediaType(t *testing.T) { MakeRequest(t, req, http.StatusUnsupportedMediaType) } -func createLFSTestRepository(t *testing.T, name string) *repo_model.Repository { - ctx := NewAPITestContext(t, "user2", "lfs-"+name+"-repo", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) +func createLFSTestRepository(t *testing.T, repoName string) *repo_model.Repository { + ctx := NewAPITestContext(t, "user2", repoName, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) t.Run("CreateRepo", doAPICreateRepository(ctx, false)) - repo, err := repo_model.GetRepositoryByOwnerAndName(t.Context(), "user2", "lfs-"+name+"-repo") - assert.NoError(t, err) + repo, err := repo_model.GetRepositoryByOwnerAndName(t.Context(), "user2", repoName) + require.NoError(t, err) return repo } @@ -74,7 +75,7 @@ func TestAPILFSBatch(t *testing.T) { setting.LFS.StartServer = true - repo := createLFSTestRepository(t, "batch") + repo := createLFSTestRepository(t, "lfs-batch-repo") content := []byte("dummy1") oid := storeObjectInRepo(t, repo.ID, &content) @@ -253,7 +254,7 @@ func TestAPILFSBatch(t *testing.T) { assert.NoError(t, err) assert.True(t, exist) - repo2 := createLFSTestRepository(t, "batch2") + repo2 := createLFSTestRepository(t, "lfs-batch2-repo") content := []byte("dummy0") storeObjectInRepo(t, repo2.ID, &content) @@ -329,7 +330,7 @@ func TestAPILFSUpload(t *testing.T) { setting.LFS.StartServer = true - repo := createLFSTestRepository(t, "upload") + repo := createLFSTestRepository(t, "lfs-upload-repo") content := []byte("dummy3") oid := storeObjectInRepo(t, repo.ID, &content) @@ -433,7 +434,7 @@ func TestAPILFSVerify(t *testing.T) { setting.LFS.StartServer = true - repo := createLFSTestRepository(t, "verify") + repo := createLFSTestRepository(t, "lfs-verify-repo") content := []byte("dummy3") oid := storeObjectInRepo(t, repo.ID, &content)