diff --git a/models/fixtures/action_run.yml b/models/fixtures/action_run.yml index 09dfa6cccb..b9688dd5f5 100644 --- a/models/fixtures/action_run.yml +++ b/models/fixtures/action_run.yml @@ -139,3 +139,23 @@ updated: 1683636626 need_approval: 0 approved_by: 0 +- + id: 804 + title: "use a private action" + repo_id: 60 + owner_id: 40 + workflow_id: "run.yaml" + index: 189 + trigger_user_id: 40 + ref: "refs/heads/master" + commit_sha: "6e64b26de7ba966d01d90ecfaf5c7f14ef203e86" + event: "push" + trigger_event: "push" + is_fork_pull_request: 0 + status: 1 + started: 1683636528 + stopped: 1683636626 + created: 1683636108 + updated: 1683636626 + need_approval: 0 + approved_by: 0 diff --git a/models/fixtures/action_run_job.yml b/models/fixtures/action_run_job.yml index 6c06d94aa4..337e83605a 100644 --- a/models/fixtures/action_run_job.yml +++ b/models/fixtures/action_run_job.yml @@ -129,3 +129,17 @@ status: 5 started: 1683636528 stopped: 1683636626 +- + id: 205 + run_id: 804 + repo_id: 6 + owner_id: 10 + commit_sha: 6e64b26de7ba966d01d90ecfaf5c7f14ef203e86 + is_fork_pull_request: 0 + name: job_2 + attempt: 1 + job_id: job_2 + task_id: 48 + status: 1 + started: 1683636528 + stopped: 1683636626 diff --git a/models/fixtures/action_task.yml b/models/fixtures/action_task.yml index c79fb07050..e09fd6f2ec 100644 --- a/models/fixtures/action_task.yml +++ b/models/fixtures/action_task.yml @@ -177,3 +177,23 @@ log_length: 0 log_size: 0 log_expired: 0 +- + id: 55 + job_id: 205 + attempt: 1 + runner_id: 1 + status: 6 # 6 is the status code for "running" + started: 1683636528 + stopped: 1683636626 + repo_id: 6 + owner_id: 10 + commit_sha: 6e64b26de7ba966d01d90ecfaf5c7f14ef203e86 + is_fork_pull_request: 0 + token_hash: b8d3962425466b6709b9ac51446f93260c54afe8e7b6d3686e34f991fb8a8953822b0deed86fe41a103f34bc48dbc478422b + token_salt: ERxJGHvg3I + token_last_eight: 182199eb + log_filename: collaborative-owner-test/1a/49.log + log_in_storage: 1 + log_length: 707 + log_size: 90179 + log_expired: 0 diff --git a/models/fixtures/repo_unit.yml b/models/fixtures/repo_unit.yml index f6b6252da1..f8bb8ef0d3 100644 --- a/models/fixtures/repo_unit.yml +++ b/models/fixtures/repo_unit.yml @@ -733,3 +733,10 @@ type: 3 config: "{\"IgnoreWhitespaceConflicts\":false,\"AllowMerge\":true,\"AllowRebase\":true,\"AllowRebaseMerge\":true,\"AllowSquash\":true}" created_unix: 946684810 + +- + id: 111 + repo_id: 3 + type: 10 + config: "{}" + created_unix: 946684810 diff --git a/models/perm/access/repo_permission.go b/models/perm/access/repo_permission.go index df96db8d5a..ba7544f343 100644 --- a/models/perm/access/repo_permission.go +++ b/models/perm/access/repo_permission.go @@ -264,13 +264,22 @@ func GetActionsUserRepoPermission(ctx context.Context, repo *repo_model.Reposito 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 { + if task.RepoID != repo.ID { + taskRepo, exist, err := db.GetByID[repo_model.Repository](ctx, task.RepoID) + if err != nil || !exist { + return perm, err + } + actionsCfg := repo.MustGetUnit(ctx, unit.TypeActions).ActionsConfig() + if !actionsCfg.IsCollaborativeOwner(taskRepo.OwnerID) || !taskRepo.IsPrivate { + // The task repo can access the current repo only if the task repo is private and + // the owner of the task repo is a collaborative owner of the current repo. + // FIXME allow public repo read access if tokenless pull is enabled + return perm, nil + } + accessMode = perm_model.AccessModeRead + } else if task.IsForkPullRequest { accessMode = perm_model.AccessModeRead } else { accessMode = perm_model.AccessModeWrite diff --git a/models/repo/repo_unit.go b/models/repo/repo_unit.go index a5207bc22a..ad0bb9d3f8 100644 --- a/models/repo/repo_unit.go +++ b/models/repo/repo_unit.go @@ -170,6 +170,9 @@ func (cfg *PullRequestsConfig) GetDefaultMergeStyle() MergeStyle { type ActionsConfig struct { DisabledWorkflows []string + // CollaborativeOwnerIDs is a list of owner IDs used to share actions from private repos. + // Only workflows from the private repos whose owners are in CollaborativeOwnerIDs can access the current repo's actions. + CollaborativeOwnerIDs []int64 } func (cfg *ActionsConfig) EnableWorkflow(file string) { @@ -192,6 +195,20 @@ func (cfg *ActionsConfig) DisableWorkflow(file string) { cfg.DisabledWorkflows = append(cfg.DisabledWorkflows, file) } +func (cfg *ActionsConfig) AddCollaborativeOwner(ownerID int64) { + if !slices.Contains(cfg.CollaborativeOwnerIDs, ownerID) { + cfg.CollaborativeOwnerIDs = append(cfg.CollaborativeOwnerIDs, ownerID) + } +} + +func (cfg *ActionsConfig) RemoveCollaborativeOwner(ownerID int64) { + cfg.CollaborativeOwnerIDs = util.SliceRemoveAll(cfg.CollaborativeOwnerIDs, ownerID) +} + +func (cfg *ActionsConfig) IsCollaborativeOwner(ownerID int64) bool { + return slices.Contains(cfg.CollaborativeOwnerIDs, ownerID) +} + // FromDB fills up a ActionsConfig from serialized format. func (cfg *ActionsConfig) FromDB(bs []byte) error { return json.UnmarshalHandleDoubleEncode(bs, &cfg) diff --git a/models/user/search.go b/models/user/search.go index cfd0d011bc..db4b07f64a 100644 --- a/models/user/search.go +++ b/models/user/search.go @@ -6,6 +6,7 @@ package user import ( "context" "fmt" + "slices" "strings" "code.gitea.io/gitea/models/db" @@ -22,7 +23,7 @@ type SearchUserOptions struct { db.ListOptions Keyword string - Type UserType + Types []UserType UID int64 LoginName string // this option should be used only for admin user SourceID int64 // this option should be used only for admin user @@ -43,16 +44,16 @@ type SearchUserOptions struct { func (opts *SearchUserOptions) toSearchQueryBase(ctx context.Context) *xorm.Session { var cond builder.Cond - cond = builder.Eq{"type": opts.Type} + cond = builder.In("type", opts.Types) if opts.IncludeReserved { - switch opts.Type { - case UserTypeIndividual: + switch { + case slices.Contains(opts.Types, UserTypeIndividual): cond = cond.Or(builder.Eq{"type": UserTypeUserReserved}).Or( builder.Eq{"type": UserTypeBot}, ).Or( builder.Eq{"type": UserTypeRemoteUser}, ) - case UserTypeOrganization: + case slices.Contains(opts.Types, UserTypeOrganization): cond = cond.Or(builder.Eq{"type": UserTypeOrganizationReserved}) } } diff --git a/models/user/user.go b/models/user/user.go index 3583694cf9..d6e1eec276 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -1449,3 +1449,15 @@ func DisabledFeaturesWithLoginType(user *User) *container.Set[string] { } return &setting.Admin.UserDisabledFeatures } + +// GetUserOrOrgIDByName returns the id for a user or an org by name +func GetUserOrOrgIDByName(ctx context.Context, name string) (int64, error) { + var id int64 + has, err := db.GetEngine(ctx).Table("user").Where("name = ?", name).Cols("id").Get(&id) + if err != nil { + return 0, err + } else if !has { + return 0, fmt.Errorf("user or org with name %s: %w", name, util.ErrNotExist) + } + return id, nil +} diff --git a/models/user/user_test.go b/models/user/user_test.go index 6a530553d7..923f2cd40e 100644 --- a/models/user/user_test.go +++ b/models/user/user_test.go @@ -126,7 +126,7 @@ func TestSearchUsers(t *testing.T) { // test orgs testOrgSuccess := func(opts user_model.SearchUserOptions, expectedOrgIDs []int64) { - opts.Type = user_model.UserTypeOrganization + opts.Types = []user_model.UserType{user_model.UserTypeOrganization} testSuccess(opts, expectedOrgIDs) } @@ -150,7 +150,7 @@ func TestSearchUsers(t *testing.T) { // test users testUserSuccess := func(opts user_model.SearchUserOptions, expectedUserIDs []int64) { - opts.Type = user_model.UserTypeIndividual + opts.Types = []user_model.UserType{user_model.UserTypeIndividual} testSuccess(opts, expectedUserIDs) } diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 46fdf06022..ddc12aefaa 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -3914,6 +3914,15 @@ variables.update.success = The variable has been edited. logs.always_auto_scroll = Always auto scroll logs logs.always_expand_running = Always expand running logs +general = General +general.enable_actions = Enable Actions +general.collaborative_owners_management = Collaborative Owners Management +general.collaborative_owners_management_help = A collaborative owner is a user or an organization whose private repository has access to the actions and workflows of this repository. +general.add_collaborative_owner = Add Collaborative Owner +general.collaborative_owner_not_exist = The collaborative owner does not exist. +general.remove_collaborative_owner = Remove Collaborative Owner +general.remove_collaborative_owner_desc = Removing a collaborative owner will prevent the repositories of the owner from accessing the actions in this repository. Continue? + [projects] deleted.display_name = Deleted Project type-1.display_name = Individual Project diff --git a/routers/api/v1/admin/org.go b/routers/api/v1/admin/org.go index c3473372f2..62afcb00d9 100644 --- a/routers/api/v1/admin/org.go +++ b/routers/api/v1/admin/org.go @@ -103,7 +103,7 @@ func GetAllOrgs(ctx *context.APIContext) { users, maxResults, err := user_model.SearchUsers(ctx, user_model.SearchUserOptions{ Actor: ctx.Doer, - Type: user_model.UserTypeOrganization, + Types: []user_model.UserType{user_model.UserTypeOrganization}, OrderBy: db.SearchOrderByAlphabetically, ListOptions: listOptions, Visible: []api.VisibleType{api.VisibleTypePublic, api.VisibleTypeLimited, api.VisibleTypePrivate}, diff --git a/routers/api/v1/admin/user.go b/routers/api/v1/admin/user.go index 494bace585..6afa651448 100644 --- a/routers/api/v1/admin/user.go +++ b/routers/api/v1/admin/user.go @@ -425,7 +425,7 @@ func SearchUsers(ctx *context.APIContext) { users, maxResults, err := user_model.SearchUsers(ctx, user_model.SearchUserOptions{ Actor: ctx.Doer, - Type: user_model.UserTypeIndividual, + Types: []user_model.UserType{user_model.UserTypeIndividual}, LoginName: ctx.FormTrim("login_name"), SourceID: ctx.FormInt64("source_id"), OrderBy: db.SearchOrderByAlphabetically, diff --git a/routers/api/v1/org/org.go b/routers/api/v1/org/org.go index cd67686065..08e37e8df4 100644 --- a/routers/api/v1/org/org.go +++ b/routers/api/v1/org/org.go @@ -202,7 +202,7 @@ func GetAll(ctx *context.APIContext) { publicOrgs, maxResults, err := user_model.SearchUsers(ctx, user_model.SearchUserOptions{ Actor: ctx.Doer, ListOptions: listOptions, - Type: user_model.UserTypeOrganization, + Types: []user_model.UserType{user_model.UserTypeOrganization}, OrderBy: db.SearchOrderByAlphabetically, Visible: vMode, }) diff --git a/routers/api/v1/user/user.go b/routers/api/v1/user/user.go index 6de1125c40..f7b9301795 100644 --- a/routers/api/v1/user/user.go +++ b/routers/api/v1/user/user.go @@ -77,7 +77,7 @@ func Search(ctx *context.APIContext) { Actor: ctx.Doer, Keyword: ctx.FormTrim("q"), UID: uid, - Type: user_model.UserTypeIndividual, + Types: []user_model.UserType{user_model.UserTypeIndividual}, SearchByEmail: true, Visible: visible, ListOptions: listOptions, diff --git a/routers/web/admin/orgs.go b/routers/web/admin/orgs.go index e34f203aaf..62a8b30b13 100644 --- a/routers/web/admin/orgs.go +++ b/routers/web/admin/orgs.go @@ -29,7 +29,7 @@ func Organizations(ctx *context.Context) { explore.RenderUserSearch(ctx, user_model.SearchUserOptions{ Actor: ctx.Doer, - Type: user_model.UserTypeOrganization, + Types: []user_model.UserType{user_model.UserTypeOrganization}, IncludeReserved: true, // administrator needs to list all accounts include reserved ListOptions: db.ListOptions{ PageSize: setting.UI.Admin.OrgPagingNum, diff --git a/routers/web/admin/users.go b/routers/web/admin/users.go index 27577cd35b..1f22d800a9 100644 --- a/routers/web/admin/users.go +++ b/routers/web/admin/users.go @@ -67,7 +67,7 @@ func Users(ctx *context.Context) { explore.RenderUserSearch(ctx, user_model.SearchUserOptions{ Actor: ctx.Doer, - Type: user_model.UserTypeIndividual, + Types: []user_model.UserType{user_model.UserTypeIndividual}, ListOptions: db.ListOptions{ PageSize: setting.UI.Admin.UserPagingNum, }, diff --git a/routers/web/explore/org.go b/routers/web/explore/org.go index f8f7f5c18c..4d25f4ec2d 100644 --- a/routers/web/explore/org.go +++ b/routers/web/explore/org.go @@ -46,7 +46,7 @@ func Organizations(ctx *context.Context) { RenderUserSearch(ctx, user_model.SearchUserOptions{ Actor: ctx.Doer, - Type: user_model.UserTypeOrganization, + Types: []user_model.UserType{user_model.UserTypeOrganization}, ListOptions: db.ListOptions{PageSize: setting.UI.ExplorePagingNum}, Visible: visibleTypes, diff --git a/routers/web/explore/user.go b/routers/web/explore/user.go index 40d3e2a060..4b3c269410 100644 --- a/routers/web/explore/user.go +++ b/routers/web/explore/user.go @@ -153,7 +153,7 @@ func Users(ctx *context.Context) { RenderUserSearch(ctx, user_model.SearchUserOptions{ Actor: ctx.Doer, - Type: user_model.UserTypeIndividual, + Types: []user_model.UserType{user_model.UserTypeIndividual}, ListOptions: db.ListOptions{PageSize: setting.UI.ExplorePagingNum}, IsActive: optional.Some(true), Visible: []structs.VisibleType{structs.VisibleTypePublic, structs.VisibleTypeLimited, structs.VisibleTypePrivate}, diff --git a/routers/web/home.go b/routers/web/home.go index 4b15ee83c2..7efa5f344e 100644 --- a/routers/web/home.go +++ b/routers/web/home.go @@ -69,7 +69,7 @@ func HomeSitemap(ctx *context.Context) { m := sitemap.NewSitemapIndex() if !setting.Service.Explore.DisableUsersPage { _, cnt, err := user_model.SearchUsers(ctx, user_model.SearchUserOptions{ - Type: user_model.UserTypeIndividual, + Types: []user_model.UserType{user_model.UserTypeIndividual}, ListOptions: db.ListOptions{PageSize: 1}, IsActive: optional.Some(true), Visible: []structs.VisibleType{structs.VisibleTypePublic}, diff --git a/routers/web/repo/githttp.go b/routers/web/repo/githttp.go index 1b1c272a8d..c6f5f74e4b 100644 --- a/routers/web/repo/githttp.go +++ b/routers/web/repo/githttp.go @@ -191,7 +191,7 @@ func httpBase(ctx *context.Context) *serviceHandler { taskID := ctx.Data["ActionsTaskID"].(int64) p, err := access_model.GetActionsUserRepoPermission(ctx, repo, ctx.Doer, taskID) if err != nil { - ctx.ServerError("GetUserRepoPermission", err) + ctx.ServerError("GetActionsUserRepoPermission", err) return nil } diff --git a/routers/web/repo/setting/actions.go b/routers/web/repo/setting/actions.go new file mode 100644 index 0000000000..9c2c9242d3 --- /dev/null +++ b/routers/web/repo/setting/actions.go @@ -0,0 +1,121 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package setting + +import ( + "errors" + "net/http" + "strings" + + repo_model "code.gitea.io/gitea/models/repo" + unit_model "code.gitea.io/gitea/models/unit" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/services/context" + repo_service "code.gitea.io/gitea/services/repository" +) + +const tplRepoActionsGeneralSettings templates.TplName = "repo/settings/actions" + +func ActionsGeneralSettings(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("actions.general") + ctx.Data["PageType"] = "general" + ctx.Data["PageIsActionsSettingsGeneral"] = true + + actionsUnit, err := ctx.Repo.Repository.GetUnit(ctx, unit_model.TypeActions) + if err != nil && !repo_model.IsErrUnitTypeNotExist(err) { + ctx.ServerError("GetUnit", err) + return + } + if actionsUnit == nil { // no actions unit + ctx.HTML(http.StatusOK, tplRepoActionsGeneralSettings) + return + } + + if ctx.Repo.Repository.IsPrivate { + collaborativeOwnerIDs := actionsUnit.ActionsConfig().CollaborativeOwnerIDs + collaborativeOwners, err := user_model.GetUsersByIDs(ctx, collaborativeOwnerIDs) + if err != nil { + ctx.ServerError("GetUsersByIDs", err) + return + } + ctx.Data["CollaborativeOwners"] = collaborativeOwners + } + + ctx.HTML(http.StatusOK, tplRepoActionsGeneralSettings) +} + +func ActionsUnitPost(ctx *context.Context) { + redirectURL := ctx.Repo.RepoLink + "/settings/actions/general" + enableActionsUnit := ctx.FormBool("enable_actions") + repo := ctx.Repo.Repository + + var err error + if enableActionsUnit && !unit_model.TypeActions.UnitGlobalDisabled() { + err = repo_service.UpdateRepositoryUnits(ctx, repo, []repo_model.RepoUnit{newRepoUnit(repo, unit_model.TypeActions, nil)}, nil) + } else if !unit_model.TypeActions.UnitGlobalDisabled() { + err = repo_service.UpdateRepositoryUnits(ctx, repo, nil, []unit_model.Type{unit_model.TypeActions}) + } + + if err != nil { + ctx.ServerError("UpdateRepositoryUnits", err) + return + } + + ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success")) + ctx.Redirect(redirectURL) +} + +func AddCollaborativeOwner(ctx *context.Context) { + name := strings.ToLower(ctx.FormString("collaborative_owner")) + + ownerID, err := user_model.GetUserOrOrgIDByName(ctx, name) + if err != nil { + if errors.Is(err, util.ErrNotExist) { + ctx.Flash.Error(ctx.Tr("form.user_not_exist")) + ctx.JSONErrorNotFound() + } else { + ctx.ServerError("GetUserOrOrgIDByName", err) + } + return + } + + actionsUnit, err := ctx.Repo.Repository.GetUnit(ctx, unit_model.TypeActions) + if err != nil { + ctx.ServerError("GetUnit", err) + return + } + actionsCfg := actionsUnit.ActionsConfig() + actionsCfg.AddCollaborativeOwner(ownerID) + if err := repo_model.UpdateRepoUnit(ctx, actionsUnit); err != nil { + ctx.ServerError("UpdateRepoUnit", err) + return + } + + ctx.JSONOK() +} + +func DeleteCollaborativeOwner(ctx *context.Context) { + ownerID := ctx.FormInt64("id") + + actionsUnit, err := ctx.Repo.Repository.GetUnit(ctx, unit_model.TypeActions) + if err != nil { + ctx.ServerError("GetUnit", err) + return + } + actionsCfg := actionsUnit.ActionsConfig() + if !actionsCfg.IsCollaborativeOwner(ownerID) { + ctx.Flash.Error(ctx.Tr("actions.general.collaborative_owner_not_exist")) + ctx.JSONErrorNotFound() + return + } + actionsCfg.RemoveCollaborativeOwner(ownerID) + if err := repo_model.UpdateRepoUnit(ctx, actionsUnit); err != nil { + ctx.ServerError("UpdateRepoUnit", err) + return + } + + ctx.JSONOK() +} diff --git a/routers/web/repo/setting/setting.go b/routers/web/repo/setting/setting.go index dd887d6edf..0b0c990ae0 100644 --- a/routers/web/repo/setting/setting.go +++ b/routers/web/repo/setting/setting.go @@ -613,12 +613,6 @@ func handleSettingsPostAdvanced(ctx *context.Context) { deleteUnitTypes = append(deleteUnitTypes, unit_model.TypePackages) } - if form.EnableActions && !unit_model.TypeActions.UnitGlobalDisabled() { - units = append(units, newRepoUnit(repo, unit_model.TypeActions, nil)) - } else if !unit_model.TypeActions.UnitGlobalDisabled() { - deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeActions) - } - if form.EnablePulls && !unit_model.TypePullRequests.UnitGlobalDisabled() { units = append(units, newRepoUnit(repo, unit_model.TypePullRequests, &repo_model.PullRequestsConfig{ IgnoreWhitespaceConflicts: form.PullsIgnoreWhitespace, diff --git a/routers/web/user/search.go b/routers/web/user/search.go index 9acb9694d7..b2a15bf90e 100644 --- a/routers/web/user/search.go +++ b/routers/web/user/search.go @@ -16,10 +16,14 @@ import ( // SearchCandidates searches candidate users for dropdown list func SearchCandidates(ctx *context.Context) { + searchUserTypes := []user_model.UserType{user_model.UserTypeIndividual} + if ctx.FormBool("orgs") { + searchUserTypes = append(searchUserTypes, user_model.UserTypeOrganization) + } users, _, err := user_model.SearchUsers(ctx, user_model.SearchUserOptions{ Actor: ctx.Doer, Keyword: ctx.FormTrim("q"), - Type: user_model.UserTypeIndividual, + Types: searchUserTypes, IsActive: optional.Some(true), ListOptions: db.ListOptions{PageSize: setting.UI.MembersPagingNum}, }) diff --git a/routers/web/web.go b/routers/web/web.go index 9b3cfb6d16..43f104a73e 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1159,11 +1159,21 @@ func registerWebRoutes(m *web.Router) { m.Post("/{lid}/unlock", repo_setting.LFSUnlock) }) }) + m.Group("/actions/general", func() { + m.Get("", repo_setting.ActionsGeneralSettings) + m.Post("/actions_unit", repo_setting.ActionsUnitPost) + }) m.Group("/actions", func() { m.Get("", shared_actions.RedirectToDefaultSetting) addSettingsRunnersRoutes() addSettingsSecretsRoutes() addSettingsVariablesRoutes() + m.Group("/general", func() { + m.Group("/collaborative_owner", func() { + m.Post("/add", repo_setting.AddCollaborativeOwner) + m.Post("/delete", repo_setting.DeleteCollaborativeOwner) + }) + }) }, actions.MustEnableActions) // the follow handler must be under "settings", otherwise this incomplete repo can't be accessed m.Group("/migrate", func() { diff --git a/templates/repo/settings/actions.tmpl b/templates/repo/settings/actions.tmpl index f38ab5b658..5388de35af 100644 --- a/templates/repo/settings/actions.tmpl +++ b/templates/repo/settings/actions.tmpl @@ -6,6 +6,8 @@ {{template "shared/secrets/add_list" .}} {{else if eq .PageType "variables"}} {{template "shared/variables/variable_list" .}} + {{else if eq .PageType "general"}} + {{template "repo/settings/actions_general" .}} {{end}} {{template "repo/settings/layout_footer" .}} diff --git a/templates/repo/settings/actions_general.tmpl b/templates/repo/settings/actions_general.tmpl new file mode 100644 index 0000000000..96d854ca8d --- /dev/null +++ b/templates/repo/settings/actions_general.tmpl @@ -0,0 +1,69 @@ +
+

+ {{ctx.Locale.Tr "actions.general.enable_actions"}} +

+
+
+ {{.CsrfTokenHtml}} + {{$isActionsEnabled := .Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeActions}} + {{$isActionsGlobalDisabled := ctx.Consts.RepoUnitTypeActions.UnitGlobalDisabled}} +
+ +
+ + +
+
+ {{if not $isActionsGlobalDisabled}} +
+
+ +
+ {{end}} +
+
+ + {{if and .EnableActions (.Permission.CanRead ctx.Consts.RepoUnitTypeActions)}} + {{if .Repository.IsPrivate}} +

+ {{ctx.Locale.Tr "actions.general.collaborative_owners_management"}} +

+ {{if len .CollaborativeOwners}} +
+
+ {{range .CollaborativeOwners}} +
+ +
+
+ {{template "shared/user/name" .}} +
+
+
+ +
+
+ {{end}} +
+
+ {{end}} +
+
+ {{.CsrfTokenHtml}} + + +
+
+ {{ctx.Locale.Tr "actions.general.collaborative_owners_management_help"}} +
+ {{end}} + {{end}} +
diff --git a/templates/repo/settings/navbar.tmpl b/templates/repo/settings/navbar.tmpl index 3dd86d1f6a..ba25e34ba4 100644 --- a/templates/repo/settings/navbar.tmpl +++ b/templates/repo/settings/navbar.tmpl @@ -38,10 +38,13 @@ {{end}} {{end}} - {{if and .EnableActions (.Permission.CanRead ctx.Consts.RepoUnitTypeActions)}} -
+
{{ctx.Locale.Tr "actions.actions"}}
- {{end}} diff --git a/templates/repo/settings/options.tmpl b/templates/repo/settings/options.tmpl index fc42056e0a..b4680431b8 100644 --- a/templates/repo/settings/options.tmpl +++ b/templates/repo/settings/options.tmpl @@ -509,18 +509,6 @@ - {{if .EnableActions}} - {{$isActionsEnabled := .Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeActions}} - {{$isActionsGlobalDisabled := ctx.Consts.RepoUnitTypeActions.UnitGlobalDisabled}} -
- -
- - -
-
- {{end}} - {{if not .IsMirror}}
{{$pullRequestEnabled := .Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypePullRequests}} diff --git a/tests/integration/actions_settings_test.go b/tests/integration/actions_settings_test.go new file mode 100644 index 0000000000..935d8bbceb --- /dev/null +++ b/tests/integration/actions_settings_test.go @@ -0,0 +1,62 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "fmt" + "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/unittest" + user_model "code.gitea.io/gitea/models/user" + + "github.com/stretchr/testify/assert" +) + +func TestActionsCollaborativeOwner(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + // user2 is the owner of "reusable_workflow" repo + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + user2Session := loginUser(t, user2.Name) + user2Token := getTokenForLoggedInUser(t, user2Session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + repo := createActionsTestRepo(t, user2Token, "reusable_workflow", true) + + // a private repo(id=6) of user10 will try to clone "reusable_workflow" repo + user10 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 10}) + // task id is 55 and its repo_id=6 + task := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 55, RepoID: 6}) + taskToken := "674f727a81ed2f195bccab036cccf86a182199eb" + tokenHash := auth_model.HashToken(taskToken, task.TokenSalt) + assert.Equal(t, task.TokenHash, tokenHash) + + dstPath := t.TempDir() + u.Path = fmt.Sprintf("%s/%s.git", repo.Owner.UserName, repo.Name) + u.User = url.UserPassword("gitea-actions", taskToken) + + // the git clone will fail + doGitCloneFail(u)(t) + + // add user10 to the list of collaborative owners + req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/actions/general/collaborative_owner/add", repo.Owner.UserName, repo.Name), map[string]string{ + "_csrf": GetUserCSRFToken(t, user2Session), + "collaborative_owner": user10.Name, + }) + user2Session.MakeRequest(t, req, http.StatusOK) + + // the git clone will be successful + doGitClone(dstPath, u)(t) + + // remove user10 from the list of collaborative owners + req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/actions/general/collaborative_owner/delete?id=%d", repo.Owner.UserName, repo.Name, user10.ID), map[string]string{ + "_csrf": GetUserCSRFToken(t, user2Session), + }) + user2Session.MakeRequest(t, req, http.StatusOK) + + // the git clone will fail + doGitCloneFail(u)(t) + }) +} diff --git a/web_src/js/features/comp/SearchUserBox.ts b/web_src/js/features/comp/SearchUserBox.ts index 4b13a2141f..9cba18b356 100644 --- a/web_src/js/features/comp/SearchUserBox.ts +++ b/web_src/js/features/comp/SearchUserBox.ts @@ -10,10 +10,11 @@ export function initCompSearchUserBox() { const allowEmailInput = searchUserBox.getAttribute('data-allow-email') === 'true'; const allowEmailDescription = searchUserBox.getAttribute('data-allow-email-description') ?? undefined; + const includeOrgs = searchUserBox.getAttribute('data-include-orgs') === 'true'; fomanticQuery(searchUserBox).search({ minCharacters: 2, apiSettings: { - url: `${appSubUrl}/user/search_candidates?q={query}`, + url: `${appSubUrl}/user/search_candidates?q={query}&orgs=${includeOrgs}`, onResponse(response: any) { const resultItems = []; const searchQuery = searchUserBox.querySelector('input').value;