mirror of
				https://github.com/go-gitea/gitea
				synced 2025-10-31 11:28:24 +00: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" | ||||
|  | ||||
| 	"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) | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
|   | ||||
| @@ -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() { | ||||
|   | ||||
| @@ -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 | ||||
| 	} | ||||
|   | ||||
| @@ -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, | ||||
| 	} | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -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})) | ||||
| 	}) | ||||
| } | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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) | ||||
| 		} | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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 | ||||
| 	} | ||||
|   | ||||
| @@ -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", | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
							
								
								
									
										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" | ||||
|  | ||||
| 	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) | ||||
| 		}) | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user