mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 19:06:18 +01:00 
			
		
		
		
	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 <christopher.homberger@web.de> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
		| @@ -8,7 +8,6 @@ import ( | |||||||
| 	"fmt" | 	"fmt" | ||||||
|  |  | ||||||
| 	"code.gitea.io/gitea/models/db" | 	"code.gitea.io/gitea/models/db" | ||||||
| 	"code.gitea.io/gitea/models/perm" |  | ||||||
| 	repo_model "code.gitea.io/gitea/models/repo" | 	repo_model "code.gitea.io/gitea/models/repo" | ||||||
| 	"code.gitea.io/gitea/models/unit" | 	"code.gitea.io/gitea/models/unit" | ||||||
| 	user_model "code.gitea.io/gitea/models/user" | 	user_model "code.gitea.io/gitea/models/user" | ||||||
| @@ -42,30 +41,6 @@ func (err ErrLFSLockNotExist) Unwrap() error { | |||||||
| 	return util.ErrNotExist | 	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. | // ErrLFSLockAlreadyExist represents a "LFSLockAlreadyExist" kind of error. | ||||||
| type ErrLFSLockAlreadyExist struct { | type ErrLFSLockAlreadyExist struct { | ||||||
| 	RepoID int64 | 	RepoID int64 | ||||||
| @@ -93,12 +68,6 @@ type ErrLFSFileLocked struct { | |||||||
| 	UserName string | 	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 { | 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) | 	return fmt.Sprintf("File is lfs locked [repo: %d, locked by: %s, path: %s]", err.RepoID, err.UserName, err.Path) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -11,10 +11,7 @@ import ( | |||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| 	"code.gitea.io/gitea/models/db" | 	"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" | 	repo_model "code.gitea.io/gitea/models/repo" | ||||||
| 	"code.gitea.io/gitea/models/unit" |  | ||||||
| 	user_model "code.gitea.io/gitea/models/user" | 	user_model "code.gitea.io/gitea/models/user" | ||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
| 	"code.gitea.io/gitea/modules/util" | 	"code.gitea.io/gitea/modules/util" | ||||||
| @@ -71,10 +68,6 @@ func (l *LFSLock) LoadOwner(ctx context.Context) error { | |||||||
| // CreateLFSLock creates a new lock. | // CreateLFSLock creates a new lock. | ||||||
| func CreateLFSLock(ctx context.Context, repo *repo_model.Repository, lock *LFSLock) (*LFSLock, error) { | func CreateLFSLock(ctx context.Context, repo *repo_model.Repository, lock *LFSLock) (*LFSLock, error) { | ||||||
| 	return db.WithTx2(ctx, func(ctx context.Context) (*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.Path = util.PathJoinRel(lock.Path) | ||||||
| 		lock.RepoID = repo.ID | 		lock.RepoID = repo.ID | ||||||
|  |  | ||||||
| @@ -165,10 +158,6 @@ func DeleteLFSLockByID(ctx context.Context, id int64, repo *repo_model.Repositor | |||||||
| 			return nil, err | 			return nil, err | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if err := CheckLFSAccessForRepo(ctx, u.ID, repo, perm.AccessModeWrite); err != nil { |  | ||||||
| 			return nil, err |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		if !force && u.ID != lock.OwnerID { | 		if !force && u.ID != lock.OwnerID { | ||||||
| 			return nil, errors.New("user doesn't own lock and force flag is not set") | 			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 | 		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 |  | ||||||
| } |  | ||||||
|   | |||||||
| @@ -5,9 +5,11 @@ package access | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
|  | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"slices" | 	"slices" | ||||||
|  |  | ||||||
|  | 	actions_model "code.gitea.io/gitea/models/actions" | ||||||
| 	"code.gitea.io/gitea/models/db" | 	"code.gitea.io/gitea/models/db" | ||||||
| 	"code.gitea.io/gitea/models/organization" | 	"code.gitea.io/gitea/models/organization" | ||||||
| 	perm_model "code.gitea.io/gitea/models/perm" | 	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 | // 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) { | func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, user *user_model.User) (perm Permission, err error) { | ||||||
| 	defer func() { | 	defer func() { | ||||||
|   | |||||||
| @@ -249,8 +249,13 @@ func (u *User) MaxCreationLimit() int { | |||||||
| } | } | ||||||
|  |  | ||||||
| // CanCreateRepoIn checks whether the doer(u) can create a repository in the owner | // 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 { | 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 { | 	if u.IsAdmin { | ||||||
| 		return true | 		return true | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -48,17 +48,16 @@ func IsGiteaActionsUserName(name string) bool { | |||||||
| // NewActionsUser creates and returns a fake user for running the actions. | // NewActionsUser creates and returns a fake user for running the actions. | ||||||
| func NewActionsUser() *User { | func NewActionsUser() *User { | ||||||
| 	return &User{ | 	return &User{ | ||||||
| 		ID:                      ActionsUserID, | 		ID:               ActionsUserID, | ||||||
| 		Name:                    ActionsUserName, | 		Name:             ActionsUserName, | ||||||
| 		LowerName:               ActionsUserName, | 		LowerName:        ActionsUserName, | ||||||
| 		IsActive:                true, | 		IsActive:         true, | ||||||
| 		FullName:                "Gitea Actions", | 		FullName:         "Gitea Actions", | ||||||
| 		Email:                   ActionsUserEmail, | 		Email:            ActionsUserEmail, | ||||||
| 		KeepEmailPrivate:        true, | 		KeepEmailPrivate: true, | ||||||
| 		LoginName:               ActionsUserName, | 		LoginName:        ActionsUserName, | ||||||
| 		Type:                    UserTypeBot, | 		Type:             UserTypeBot, | ||||||
| 		AllowCreateOrganization: true, | 		Visibility:       structs.VisibleTypePublic, | ||||||
| 		Visibility:              structs.VisibleTypePublic, |  | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -648,33 +648,36 @@ func TestGetInactiveUsers(t *testing.T) { | |||||||
| func TestCanCreateRepo(t *testing.T) { | func TestCanCreateRepo(t *testing.T) { | ||||||
| 	defer test.MockVariableValue(&setting.Repository.MaxCreationLimit)() | 	defer test.MockVariableValue(&setting.Repository.MaxCreationLimit)() | ||||||
| 	const noLimit = -1 | 	const noLimit = -1 | ||||||
| 	doerNormal := &user_model.User{} | 	doerActions := user_model.NewActionsUser() | ||||||
| 	doerAdmin := &user_model.User{IsAdmin: true} | 	doerNormal := &user_model.User{ID: 2} | ||||||
|  | 	doerAdmin := &user_model.User{ID: 1, IsAdmin: true} | ||||||
| 	t.Run("NoGlobalLimit", func(t *testing.T) { | 	t.Run("NoGlobalLimit", func(t *testing.T) { | ||||||
| 		setting.Repository.MaxCreationLimit = noLimit | 		setting.Repository.MaxCreationLimit = noLimit | ||||||
|  |  | ||||||
| 		assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: noLimit})) | 		assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 0})) | ||||||
| 		assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{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{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{ID: 2, NumRepos: 10, MaxRepoCreation: 0})) | ||||||
| 		assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{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{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) { | 	t.Run("GlobalLimit50", func(t *testing.T) { | ||||||
| 		setting.Repository.MaxCreationLimit = 50 | 		setting.Repository.MaxCreationLimit = 50 | ||||||
|  |  | ||||||
| 		assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: noLimit})) | 		assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, 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{ID: 2, NumRepos: 60, MaxRepoCreation: noLimit})) // limited by global limit | ||||||
| 		assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 0})) | 		assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 0})) | ||||||
| 		assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 100})) | 		assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, 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: 60, MaxRepoCreation: 100})) | ||||||
|  |  | ||||||
| 		assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: noLimit})) | 		assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: noLimit})) | ||||||
| 		assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 60, MaxRepoCreation: noLimit})) | 		assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 60, MaxRepoCreation: noLimit})) | ||||||
| 		assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 0})) | 		assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, 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: 100})) | ||||||
| 		assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 60, MaxRepoCreation: 100})) | 		assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 60, MaxRepoCreation: 100})) | ||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -70,7 +70,6 @@ import ( | |||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
| 	actions_model "code.gitea.io/gitea/models/actions" |  | ||||||
| 	auth_model "code.gitea.io/gitea/models/auth" | 	auth_model "code.gitea.io/gitea/models/auth" | ||||||
| 	"code.gitea.io/gitea/models/organization" | 	"code.gitea.io/gitea/models/organization" | ||||||
| 	"code.gitea.io/gitea/models/perm" | 	"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 { | 		if ctx.Doer != nil && ctx.Doer.ID == user_model.ActionsUserID { | ||||||
| 			taskID := ctx.Data["ActionsTaskID"].(int64) | 			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 { | 			if err != nil { | ||||||
| 				ctx.APIErrorInternal(err) | 				ctx.APIErrorInternal(err) | ||||||
| 				return | 				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 { | 		} else { | ||||||
| 			needTwoFactor, err := doerNeedTwoFactorAuth(ctx, ctx.Doer) | 			needTwoFactor, err := doerNeedTwoFactorAuth(ctx, ctx.Doer) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
|   | |||||||
| @@ -28,6 +28,7 @@ import ( | |||||||
| 	repo_module "code.gitea.io/gitea/modules/repository" | 	repo_module "code.gitea.io/gitea/modules/repository" | ||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
| 	api "code.gitea.io/gitea/modules/structs" | 	api "code.gitea.io/gitea/modules/structs" | ||||||
|  | 	"code.gitea.io/gitea/modules/util" | ||||||
| 	"code.gitea.io/gitea/modules/validation" | 	"code.gitea.io/gitea/modules/validation" | ||||||
| 	"code.gitea.io/gitea/modules/web" | 	"code.gitea.io/gitea/modules/web" | ||||||
| 	"code.gitea.io/gitea/routers/api/v1/utils" | 	"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) || | 			db.IsErrNamePatternNotAllowed(err) || | ||||||
| 			label.IsErrTemplateLoad(err) { | 			label.IsErrTemplateLoad(err) { | ||||||
| 			ctx.APIError(http.StatusUnprocessableEntity, err) | 			ctx.APIError(http.StatusUnprocessableEntity, err) | ||||||
|  | 		} else if errors.Is(err, util.ErrPermissionDenied) { | ||||||
|  | 			ctx.APIError(http.StatusForbidden, err) | ||||||
| 		} else { | 		} else { | ||||||
| 			ctx.APIErrorInternal(err) | 			ctx.APIErrorInternal(err) | ||||||
| 		} | 		} | ||||||
|   | |||||||
| @@ -18,7 +18,6 @@ import ( | |||||||
| 	"sync" | 	"sync" | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| 	actions_model "code.gitea.io/gitea/models/actions" |  | ||||||
| 	auth_model "code.gitea.io/gitea/models/auth" | 	auth_model "code.gitea.io/gitea/models/auth" | ||||||
| 	"code.gitea.io/gitea/models/perm" | 	"code.gitea.io/gitea/models/perm" | ||||||
| 	access_model "code.gitea.io/gitea/models/perm/access" | 	access_model "code.gitea.io/gitea/models/perm/access" | ||||||
| @@ -190,29 +189,17 @@ func httpBase(ctx *context.Context) *serviceHandler { | |||||||
|  |  | ||||||
| 			if ctx.Data["IsActionsToken"] == true { | 			if ctx.Data["IsActionsToken"] == true { | ||||||
| 				taskID := ctx.Data["ActionsTaskID"].(int64) | 				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 { | 				if err != nil { | ||||||
| 					ctx.ServerError("GetTaskByID", err) | 					ctx.ServerError("GetUserRepoPermission", err) | ||||||
| 					return nil |  | ||||||
| 				} |  | ||||||
| 				if task.RepoID != repo.ID { |  | ||||||
| 					ctx.PlainText(http.StatusForbidden, "User permission denied") |  | ||||||
| 					return nil | 					return nil | ||||||
| 				} | 				} | ||||||
|  |  | ||||||
| 				if task.IsForkPullRequest { | 				if !p.CanAccess(accessMode, unitType) { | ||||||
| 					if accessMode > perm.AccessModeRead { | 					ctx.PlainText(http.StatusNotFound, "Repository not found") | ||||||
| 						ctx.PlainText(http.StatusForbidden, "User permission denied") | 					return nil | ||||||
| 						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)) |  | ||||||
| 				} | 				} | ||||||
|  | 				environ = append(environ, fmt.Sprintf("%s=%d", repo_module.EnvActionPerm, p.UnitAccessMode(unitType))) | ||||||
| 			} else { | 			} else { | ||||||
| 				p, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer) | 				p, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer) | ||||||
| 				if err != nil { | 				if err != nil { | ||||||
|   | |||||||
| @@ -771,7 +771,7 @@ func ToOAuth2Application(app *auth.OAuth2Application) *api.OAuth2Application { | |||||||
|  |  | ||||||
| // ToLFSLock convert a LFSLock to api.LFSLock | // ToLFSLock convert a LFSLock to api.LFSLock | ||||||
| func ToLFSLock(ctx context.Context, l *git_model.LFSLock) *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 { | 	if err != nil { | ||||||
| 		return nil | 		return nil | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -187,13 +187,6 @@ func PostLockHandler(ctx *context.Context) { | |||||||
| 			}) | 			}) | ||||||
| 			return | 			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) | 		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{ | 		ctx.JSON(http.StatusInternalServerError, api.LFSLockError{ | ||||||
| 			Message: "internal server error : Internal Server Error", | 			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) | 	lock, err := git_model.DeleteLFSLockByID(ctx, ctx.PathParamInt64("lid"), repository, ctx.Doer, req.Force) | ||||||
| 	if err != nil { | 	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) | 		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{ | 		ctx.JSON(http.StatusInternalServerError, api.LFSLockError{ | ||||||
| 			Message: "unable to delete lock : Internal Server Error", | 			Message: "unable to delete lock : Internal Server Error", | ||||||
|   | |||||||
| @@ -20,7 +20,6 @@ import ( | |||||||
| 	"strings" | 	"strings" | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| 	actions_model "code.gitea.io/gitea/models/actions" |  | ||||||
| 	auth_model "code.gitea.io/gitea/models/auth" | 	auth_model "code.gitea.io/gitea/models/auth" | ||||||
| 	git_model "code.gitea.io/gitea/models/git" | 	git_model "code.gitea.io/gitea/models/git" | ||||||
| 	perm_model "code.gitea.io/gitea/models/perm" | 	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 { | 	if ctx.Data["IsActionsToken"] == true { | ||||||
| 		taskID := ctx.Data["ActionsTaskID"].(int64) | 		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 { | 		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 | 			return false | ||||||
| 		} | 		} | ||||||
| 		if task.RepoID != repository.ID { | 		return perm.CanAccess(accessMode, unit.TypeCode) | ||||||
| 			return false |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		if task.IsForkPullRequest { |  | ||||||
| 			return accessMode <= perm_model.AccessModeRead |  | ||||||
| 		} |  | ||||||
| 		return accessMode <= perm_model.AccessModeWrite |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// 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) | 	perm, err := access_model.GetUserRepoPermission(ctx, repository, ctx.Doer) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Error("Unable to GetUserRepoPermission for user %-v in repo %-v Error: %v", ctx.Doer, repository, err) | 		log.Error("Unable to GetUserRepoPermission for user %-v in repo %-v Error: %v", ctx.Doer, repository, err) | ||||||
| 		return false | 		return false | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	canRead := perm.CanAccess(accessMode, unit.TypeCode) | 	canAccess := perm.CanAccess(accessMode, unit.TypeCode) | ||||||
| 	if canRead && (!requireSigned || ctx.IsSigned) { | 	// 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 | 		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) | 	user, err := parseToken(ctx, authorization, repository, accessMode) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		// Most of these are Warn level - the true internal server errors are logged in parseToken already | 		// Most of these are Warn level - the true internal server errors are logged in parseToken already | ||||||
|   | |||||||
							
								
								
									
										117
									
								
								tests/integration/actions_job_token_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								tests/integration/actions_job_token_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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) | ||||||
|  | 		})) | ||||||
|  | 	}) | ||||||
|  | } | ||||||
| @@ -9,7 +9,6 @@ import ( | |||||||
| 	"testing" | 	"testing" | ||||||
|  |  | ||||||
| 	auth_model "code.gitea.io/gitea/models/auth" | 	auth_model "code.gitea.io/gitea/models/auth" | ||||||
| 	api "code.gitea.io/gitea/modules/structs" |  | ||||||
| 	"code.gitea.io/gitea/tests" | 	"code.gitea.io/gitea/tests" | ||||||
|  |  | ||||||
| 	"github.com/stretchr/testify/assert" | 	"github.com/stretchr/testify/assert" | ||||||
| @@ -25,8 +24,9 @@ func TestAPIGetRawFileOrLFS(t *testing.T) { | |||||||
|  |  | ||||||
| 	// Test with LFS | 	// Test with LFS | ||||||
| 	onGiteaRun(t, func(t *testing.T, u *url.URL) { | 	onGiteaRun(t, func(t *testing.T, u *url.URL) { | ||||||
|  | 		createLFSTestRepository(t, "repo-lfs-test") | ||||||
| 		httpContext := NewAPITestContext(t, "user2", "repo-lfs-test", auth_model.AccessTokenScopeWriteRepository) | 		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() | 			u.Path = httpContext.GitPath() | ||||||
| 			dstPath := t.TempDir() | 			dstPath := t.TempDir() | ||||||
|  |  | ||||||
| @@ -41,7 +41,7 @@ func TestAPIGetRawFileOrLFS(t *testing.T) { | |||||||
|  |  | ||||||
| 			lfs := lfsCommitAndPushTest(t, dstPath, testFileSizeSmall)[0] | 			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) | 			respLFS := MakeRequestNilResponseRecorder(t, reqLFS, http.StatusOK) | ||||||
| 			assert.Equal(t, testFileSizeSmall, respLFS.Length) | 			assert.Equal(t, testFileSizeSmall, respLFS.Length) | ||||||
| 		}) | 		}) | ||||||
|   | |||||||
| @@ -23,6 +23,7 @@ import ( | |||||||
| 	"code.gitea.io/gitea/tests" | 	"code.gitea.io/gitea/tests" | ||||||
|  |  | ||||||
| 	"github.com/stretchr/testify/assert" | 	"github.com/stretchr/testify/assert" | ||||||
|  | 	"github.com/stretchr/testify/require" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func TestAPILFSNotStarted(t *testing.T) { | func TestAPILFSNotStarted(t *testing.T) { | ||||||
| @@ -59,12 +60,12 @@ func TestAPILFSMediaType(t *testing.T) { | |||||||
| 	MakeRequest(t, req, http.StatusUnsupportedMediaType) | 	MakeRequest(t, req, http.StatusUnsupportedMediaType) | ||||||
| } | } | ||||||
|  |  | ||||||
| func createLFSTestRepository(t *testing.T, name string) *repo_model.Repository { | func createLFSTestRepository(t *testing.T, repoName string) *repo_model.Repository { | ||||||
| 	ctx := NewAPITestContext(t, "user2", "lfs-"+name+"-repo", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) | 	ctx := NewAPITestContext(t, "user2", repoName, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) | ||||||
| 	t.Run("CreateRepo", doAPICreateRepository(ctx, false)) | 	t.Run("CreateRepo", doAPICreateRepository(ctx, false)) | ||||||
|  |  | ||||||
| 	repo, err := repo_model.GetRepositoryByOwnerAndName(t.Context(), "user2", "lfs-"+name+"-repo") | 	repo, err := repo_model.GetRepositoryByOwnerAndName(t.Context(), "user2", repoName) | ||||||
| 	assert.NoError(t, err) | 	require.NoError(t, err) | ||||||
|  |  | ||||||
| 	return repo | 	return repo | ||||||
| } | } | ||||||
| @@ -74,7 +75,7 @@ func TestAPILFSBatch(t *testing.T) { | |||||||
|  |  | ||||||
| 	setting.LFS.StartServer = true | 	setting.LFS.StartServer = true | ||||||
|  |  | ||||||
| 	repo := createLFSTestRepository(t, "batch") | 	repo := createLFSTestRepository(t, "lfs-batch-repo") | ||||||
|  |  | ||||||
| 	content := []byte("dummy1") | 	content := []byte("dummy1") | ||||||
| 	oid := storeObjectInRepo(t, repo.ID, &content) | 	oid := storeObjectInRepo(t, repo.ID, &content) | ||||||
| @@ -253,7 +254,7 @@ func TestAPILFSBatch(t *testing.T) { | |||||||
| 			assert.NoError(t, err) | 			assert.NoError(t, err) | ||||||
| 			assert.True(t, exist) | 			assert.True(t, exist) | ||||||
|  |  | ||||||
| 			repo2 := createLFSTestRepository(t, "batch2") | 			repo2 := createLFSTestRepository(t, "lfs-batch2-repo") | ||||||
| 			content := []byte("dummy0") | 			content := []byte("dummy0") | ||||||
| 			storeObjectInRepo(t, repo2.ID, &content) | 			storeObjectInRepo(t, repo2.ID, &content) | ||||||
|  |  | ||||||
| @@ -329,7 +330,7 @@ func TestAPILFSUpload(t *testing.T) { | |||||||
|  |  | ||||||
| 	setting.LFS.StartServer = true | 	setting.LFS.StartServer = true | ||||||
|  |  | ||||||
| 	repo := createLFSTestRepository(t, "upload") | 	repo := createLFSTestRepository(t, "lfs-upload-repo") | ||||||
|  |  | ||||||
| 	content := []byte("dummy3") | 	content := []byte("dummy3") | ||||||
| 	oid := storeObjectInRepo(t, repo.ID, &content) | 	oid := storeObjectInRepo(t, repo.ID, &content) | ||||||
| @@ -433,7 +434,7 @@ func TestAPILFSVerify(t *testing.T) { | |||||||
|  |  | ||||||
| 	setting.LFS.StartServer = true | 	setting.LFS.StartServer = true | ||||||
|  |  | ||||||
| 	repo := createLFSTestRepository(t, "verify") | 	repo := createLFSTestRepository(t, "lfs-verify-repo") | ||||||
|  |  | ||||||
| 	content := []byte("dummy3") | 	content := []byte("dummy3") | ||||||
| 	oid := storeObjectInRepo(t, repo.ID, &content) | 	oid := storeObjectInRepo(t, repo.ID, &content) | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user