diff --git a/models/actions/config.go b/models/actions/config.go new file mode 100644 index 0000000000..bb04f8a3ca --- /dev/null +++ b/models/actions/config.go @@ -0,0 +1,43 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "context" + + repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/json" +) + +// GetOrgActionsConfig loads the ActionsConfig for an organization from user settings +// It returns a default config if no setting is found +func GetOrgActionsConfig(ctx context.Context, orgID int64) (*repo_model.ActionsConfig, error) { + val, err := user_model.GetUserSetting(ctx, orgID, "actions.config") + if err != nil { + return nil, err + } + + cfg := &repo_model.ActionsConfig{} + if val == "" { + // Return defaults if no config exists + return cfg, nil + } + + if err := json.Unmarshal([]byte(val), cfg); err != nil { + return nil, err + } + + return cfg, nil +} + +// SetOrgActionsConfig saves the ActionsConfig for an organization to user settings +func SetOrgActionsConfig(ctx context.Context, orgID int64, cfg *repo_model.ActionsConfig) error { + bs, err := json.Marshal(cfg) + if err != nil { + return err + } + + return user_model.SetUserSetting(ctx, orgID, "actions.config", string(bs)) +} diff --git a/models/perm/access/repo_permission.go b/models/perm/access/repo_permission.go index 2ecdef6a48..3e655d2672 100644 --- a/models/perm/access/repo_permission.go +++ b/models/perm/access/repo_permission.go @@ -280,6 +280,19 @@ func GetActionsUserRepoPermission(ctx context.Context, repo *repo_model.Reposito if err != nil || !exist { return perm, err } + + // Check Organization Cross-Repo Access Policy + if repo.OwnerID == taskRepo.OwnerID && repo.Owner.IsOrganization() { + orgCfg, err := actions_model.GetOrgActionsConfig(ctx, repo.OwnerID) + if err != nil { + return perm, err + } + if !orgCfg.AllowCrossRepoAccess { + // Deny access if cross-repo is disabled in Org + return perm, nil + } + } + 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. diff --git a/models/repo/repo_unit.go b/models/repo/repo_unit.go index 6e420373da..7ae9ad287c 100644 --- a/models/repo/repo_unit.go +++ b/models/repo/repo_unit.go @@ -240,6 +240,8 @@ type ActionsConfig struct { DefaultTokenPermissions *ActionsTokenPermissions `json:"default_token_permissions,omitempty"` // MaxTokenPermissions defines the maximum permissions (cannot be exceeded by workflow permissions keyword) MaxTokenPermissions *ActionsTokenPermissions `json:"max_token_permissions,omitempty"` + // AllowCrossRepoAccess indicates if actions in this repo/org can access other repos in the same org + AllowCrossRepoAccess bool `json:"allow_cross_repo_access,omitempty"` } func (cfg *ActionsConfig) EnableWorkflow(file string) { diff --git a/models/user/setting.go b/models/user/setting.go index c65afae76c..7b60e4ee83 100644 --- a/models/user/setting.go +++ b/models/user/setting.go @@ -10,6 +10,7 @@ import ( "strings" "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/cache" setting_module "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 756aa35b1f..50609582ae 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -3955,6 +3955,8 @@ general.token_permissions.access_read = Read general.token_permissions.access_write = Write general.token_permissions.access_none = No access general.token_permissions.update_success = Token permissions updated successfully. +general.token_permissions.cross_repo = Cross-Repository Access +general.token_permissions.cross_repo_desc = Allow workflows in this organization to access other repositories within the same organization. [projects] deleted.display_name = Deleted Project diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go index f6ee5958b5..822510dce7 100644 --- a/routers/api/packages/api.go +++ b/routers/api/packages/api.go @@ -6,7 +6,9 @@ package packages import ( "net/http" + actions_model "code.gitea.io/gitea/models/actions" auth_model "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" @@ -80,6 +82,55 @@ func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.Context) { } } + if ctx.Data["IsActionsToken"] == true { + if ctx.Package != nil && ctx.Package.Owner.Visibility.IsPrivate() { + // Actions rules: + // 1. If the package key matches the task repo, allow. + // 2. If not, check cross-repo policy. + + taskID, ok := ctx.Data["ActionsTaskID"].(int64) + if ok { + task, err := actions_model.GetTaskByID(ctx, taskID) + if err != nil { + log.Error("GetTaskByID: %v", err) + ctx.HTTPError(http.StatusInternalServerError, "GetTaskByID", err.Error()) + return + } + + var packageRepoID int64 + if ctx.Package.Descriptor != nil && ctx.Package.Descriptor.Package != nil { + packageRepoID = ctx.Package.Descriptor.Package.RepoID + } + + if task.RepoID != packageRepoID { + // Not linked to the running repo. + // Check Org Policy + if ctx.Package.Owner.IsOrganization() { + cfg, err := actions_model.GetOrgActionsConfig(ctx, ctx.Package.Owner.ID) + if err != nil { + log.Error("GetOrgActionsConfig: %v", err) + ctx.HTTPError(http.StatusInternalServerError, "GetOrgActionsConfig", err.Error()) + return + } + if !cfg.AllowCrossRepoAccess { + ctx.HTTPError(http.StatusForbidden, "reqPackageAccess", "cross-repository package access is disabled") + return + } + } else { + // For user-owned packages, maybe stricter? Or same? + // Issue says "only when they have been linked". + // If Owner is User, Cross-Repo setting is not available (it's Org setting). + // Default to Strict for Users? + if task.RepoID != ctx.Package.RepoID { + ctx.HTTPError(http.StatusForbidden, "reqPackageAccess", "package must be linked to the repository") + return + } + } + } + } + } + } + if ctx.Package.AccessMode < accessMode && !ctx.IsUserSiteAdmin() { ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="Gitea Package API"`) ctx.HTTPError(http.StatusUnauthorized, "reqPackageAccess", "user should have specific permission or be a site admin") diff --git a/routers/web/org/setting/actions.go b/routers/web/org/setting/actions.go new file mode 100644 index 0000000000..44d014b20c --- /dev/null +++ b/routers/web/org/setting/actions.go @@ -0,0 +1,69 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package setting + +import ( + "net/http" + + actions_model "code.gitea.io/gitea/models/actions" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/services/context" +) + +const ( + tplSettingsActionsGeneral base.TplName = "org/settings/actions_general" +) + +// ActionsGeneral renders the actions general settings page +func ActionsGeneral(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("actions.actions") + ctx.Data["PageIsOrgSettings"] = true + ctx.Data["PageIsOrgSettingsActions"] = true + + // Load Org Actions Config + actionsCfg, err := actions_model.GetOrgActionsConfig(ctx, ctx.Org.Organization.ID) + if err != nil { + ctx.ServerError("GetOrgActionsConfig", err) + return + } + + ctx.Data["TokenPermissionMode"] = actionsCfg.GetTokenPermissionMode() + ctx.Data["TokenPermissionModePermissive"] = repo_model.ActionsTokenPermissionModePermissive + ctx.Data["TokenPermissionModeRestricted"] = repo_model.ActionsTokenPermissionModeRestricted + + ctx.Data["AllowCrossRepoAccess"] = actionsCfg.AllowCrossRepoAccess + + ctx.HTML(http.StatusOK, tplSettingsActionsGeneral) +} + +// ActionsGeneralPost responses for actions general settings page +func ActionsGeneralPost(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("actions.actions") + ctx.Data["PageIsOrgSettings"] = true + ctx.Data["PageIsOrgSettingsActions"] = true + + actionsCfg, err := actions_model.GetOrgActionsConfig(ctx, ctx.Org.Organization.ID) + if err != nil { + ctx.ServerError("GetOrgActionsConfig", err) + return + } + + // Update Token Permission Mode + permissionMode := repo_model.ActionsTokenPermissionMode(ctx.FormString("token_permission_mode")) + if permissionMode == repo_model.ActionsTokenPermissionModeRestricted || permissionMode == repo_model.ActionsTokenPermissionModePermissive { + actionsCfg.TokenPermissionMode = permissionMode + } + + // Update Cross-Repo Access + actionsCfg.AllowCrossRepoAccess = ctx.FormBool("allow_cross_repo_access") + + if err := actions_model.SetOrgActionsConfig(ctx, ctx.Org.Organization.ID, actionsCfg); err != nil { + ctx.ServerError("SetOrgActionsConfig", err) + return + } + + ctx.Flash.Success(ctx.Tr("org.settings.update_setting_success")) + ctx.Redirect(ctx.Org.OrgLink + "/settings/actions") +} diff --git a/routers/web/web.go b/routers/web/web.go index ea70d8bb62..73b9a5dd0b 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -961,7 +961,8 @@ func registerWebRoutes(m *web.Router) { }) m.Group("/actions", func() { - m.Get("", org_setting.RedirectToDefaultSetting) + m.Get("", org_setting.ActionsGeneral) + m.Post("", org_setting.ActionsGeneralPost) addSettingsRunnersRoutes() addSettingsSecretsRoutes() addSettingsVariablesRoutes() diff --git a/templates/org/settings/actions_general.tmpl b/templates/org/settings/actions_general.tmpl new file mode 100644 index 0000000000..1057d8d8e1 --- /dev/null +++ b/templates/org/settings/actions_general.tmpl @@ -0,0 +1,47 @@ +{{template "org/settings/layout_head" .}} +