{{ctx.Locale.Tr "actions.general.remove_collaborative_owner_desc"}}
+diff --git a/models/actions/task.go b/models/actions/task.go index af74faf937e5f..4d8741d18ab7b 100644 --- a/models/actions/task.go +++ b/models/actions/task.go @@ -11,6 +11,7 @@ import ( auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/log" @@ -37,9 +38,10 @@ type ActionTask struct { Started timeutil.TimeStamp `xorm:"index"` Stopped timeutil.TimeStamp `xorm:"index(stopped_log_expired)"` - RepoID int64 `xorm:"index"` - OwnerID int64 `xorm:"index"` - CommitSHA string `xorm:"index"` + RepoID int64 `xorm:"index"` + Repo *repo_model.Repository `xorm:"-"` + OwnerID int64 `xorm:"index"` + CommitSHA string `xorm:"index"` IsForkPullRequest bool Token string `xorm:"-"` @@ -143,7 +145,7 @@ func (task *ActionTask) LoadAttributes(ctx context.Context) error { task.Steps = steps } - return nil + return task.LoadRepository(ctx) } func (task *ActionTask) GenerateToken() (err error) { @@ -151,6 +153,14 @@ func (task *ActionTask) GenerateToken() (err error) { return err } +func (task *ActionTask) LoadRepository(ctx context.Context) (err error) { + if task.Repo != nil { + return nil + } + task.Repo, err = repo_model.GetRepositoryByID(ctx, task.RepoID) + return err +} + func GetTaskByID(ctx context.Context, id int64) (*ActionTask, error) { var task ActionTask has, err := db.GetEngine(ctx).Where("id=?", id).Get(&task) diff --git a/models/fixtures/action_run.yml b/models/fixtures/action_run.yml index a42ab77ca5b09..ed971776b880c 100644 --- a/models/fixtures/action_run.yml +++ b/models/fixtures/action_run.yml @@ -36,3 +36,22 @@ updated: 1683636626 need_approval: 0 approved_by: 0 +- + id: 793 + title: "use a private action" + repo_id: 6 + owner_id: 10 + workflow_id: "run.yaml" + index: 189 + trigger_user_id: 10 + ref: "refs/heads/master" + commit_sha: "6e64b26de7ba966d01d90ecfaf5c7f14ef203e86" + 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 fd90f4fd5d2ea..c88c067564ab1 100644 --- a/models/fixtures/action_run_job.yml +++ b/models/fixtures/action_run_job.yml @@ -26,3 +26,17 @@ status: 1 started: 1683636528 stopped: 1683636626 +- + id: 194 + run_id: 793 + 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 d88a8ed8a9189..05afcb58ca2bf 100644 --- a/models/fixtures/action_task.yml +++ b/models/fixtures/action_task.yml @@ -57,3 +57,23 @@ log_length: 707 log_size: 90179 log_expired: 0 +- + id: 49 + job_id: 194 + 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 f6b6252da1f88..f8bb8ef0d3282 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/repo/repo_unit.go b/models/repo/repo_unit.go index cb52c2c9e2058..6835257eee1a0 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) { @@ -194,6 +197,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 6af33892373ad..f55ac07775dbd 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 @@ -45,15 +46,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 { - if opts.Type == UserTypeIndividual { + if slices.Contains(opts.Types, UserTypeIndividual) { cond = cond.Or(builder.Eq{"type": UserTypeUserReserved}).Or( builder.Eq{"type": UserTypeBot}, ).Or( builder.Eq{"type": UserTypeRemoteUser}, ) - } else if opts.Type == UserTypeOrganization { + } + if 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 bd92693b6e0ff..37b0ea0cb1260 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -1317,3 +1317,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 6701be39a5536..1fcd8aad1f564 100644 --- a/models/user/user_test.go +++ b/models/user/user_test.go @@ -76,7 +76,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) } @@ -100,7 +100,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 ffce4b7e2f301..7566a96fcf120 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -3755,6 +3755,15 @@ variables.creation.success = The variable "%s" has been added. variables.update.failed = Failed to edit variable. variables.update.success = The variable has been edited. +general = General +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? +general.collaborative_owner_not_required = The actions and workflows of a public repository are always accessible to other repositories. You do not need to specify collaborative owners. + [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 a5c299bbf01c8..30577700670cd 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 b0f40084da38b..a476aad562793 100644 --- a/routers/api/v1/admin/user.go +++ b/routers/api/v1/admin/user.go @@ -423,7 +423,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 3fb653bcb6d0c..65695dc40ad5d 100644 --- a/routers/api/v1/org/org.go +++ b/routers/api/v1/org/org.go @@ -204,7 +204,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 e668326861e1a..8d0b357c6f5c3 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 cea28f82203b2..31b1e08e9f66b 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 a6b0b5c78bb13..ac5945d10e82f 100644 --- a/routers/web/admin/users.go +++ b/routers/web/admin/users.go @@ -71,7 +71,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 7bb71acfd78e0..8748f4a019d06 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 c009982d420ce..0c1e5b2fcfee9 100644 --- a/routers/web/explore/user.go +++ b/routers/web/explore/user.go @@ -156,7 +156,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 d4be0931e850d..358749a3bf8f8 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 58a2bdbab1c34..2a696ad659306 100644 --- a/routers/web/repo/githttp.go +++ b/routers/web/repo/githttp.go @@ -195,8 +195,17 @@ func httpBase(ctx *context.Context) *serviceHandler { return nil } if task.RepoID != repo.ID { - ctx.PlainText(http.StatusForbidden, "User permission denied") - return nil + if err := task.LoadRepository(ctx); err != nil { + ctx.ServerError("LoadRepository", err) + return nil + } + actionsCfg := repo.MustGetUnit(ctx, unit.TypeActions).ActionsConfig() + if !actionsCfg.IsCollaborativeOwner(task.Repo.OwnerID) || !task.Repo.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. + ctx.PlainText(http.StatusForbidden, "User permission denied") + return nil + } } if task.IsForkPullRequest { diff --git a/routers/web/repo/setting/actions.go b/routers/web/repo/setting/actions.go new file mode 100644 index 0000000000000..d17fbd140953f --- /dev/null +++ b/routers/web/repo/setting/actions.go @@ -0,0 +1,97 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package setting + +import ( + "errors" + "fmt" + "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/base" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/services/context" +) + +const tplRepoActionsGeneralSettings base.TplName = "repo/settings/actions" + +func ActionsGeneralSettings(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("actions.general") + ctx.Data["PageType"] = "general" + ctx.Data["PageIsActionsSettingsGeneral"] = true + + if ctx.Repo.Repository.IsPrivate { + actionsUnit, err := ctx.Repo.Repository.GetUnit(ctx, unit_model.TypeActions) + if err != nil { + ctx.ServerError("GetUnit", err) + return + } + 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 AddCollaborativeOwner(ctx *context.Context) { + redirectURL := fmt.Sprintf("%s/settings/actions/general", ctx.Repo.RepoLink) + 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.Redirect(redirectURL) + } 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.Redirect(redirectURL) +} + +func DeleteCollaborativeOwner(ctx *context.Context) { + redirectURL := fmt.Sprintf("%s/settings/actions/general", ctx.Repo.RepoLink) + 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.Redirect(redirectURL) + return + } + actionsCfg.RemoveCollaborativeOwner(ownerID) + if err := repo_model.UpdateRepoUnit(ctx, actionsUnit); err != nil { + ctx.ServerError("UpdateRepoUnit", err) + return + } + + ctx.JSONRedirect(redirectURL) +} diff --git a/routers/web/user/search.go b/routers/web/user/search.go index be5eee90a971a..6f0526c161005 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 5ed046a9838b1..e27c53090c5f8 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1134,6 +1134,13 @@ func registerRoutes(m *web.Router) { addSettingsRunnersRoutes() addSettingsSecretsRoutes() addSettingsVariablesRoutes() + m.Group("/general", func() { + m.Get("", repo_setting.ActionsGeneralSettings) + 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 f38ab5b658412..5388de35af35e 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 0000000000000..0019236d3a493 --- /dev/null +++ b/templates/repo/settings/actions_general.tmpl @@ -0,0 +1,56 @@ +
{{ctx.Locale.Tr "actions.general.remove_collaborative_owner_desc"}}
+