From 9610d7f27a245720dcde23322581ba90873eaeb6 Mon Sep 17 00:00:00 2001 From: Excellencedev Date: Tue, 6 Jan 2026 15:49:34 +0100 Subject: [PATCH] Implement all requested changes --- models/repo/repo_unit.go | 18 ++++ options/locale/locale_en-US.json | 4 + routers/web/org/setting/actions.go | 99 ++++++++++++++++++++ routers/web/repo/setting/actions.go | 3 + routers/web/web.go | 4 + services/actions/permission_parser.go | 8 ++ templates/org/settings/actions_general.tmpl | 35 ++++++- templates/repo/settings/actions_general.tmpl | 50 ++++++++++ web_src/js/features/repo-settings-actions.ts | 23 ++++- 9 files changed, 239 insertions(+), 5 deletions(-) diff --git a/models/repo/repo_unit.go b/models/repo/repo_unit.go index cbb5cca38d..e5c1ec65bf 100644 --- a/models/repo/repo_unit.go +++ b/models/repo/repo_unit.go @@ -194,6 +194,10 @@ type ActionsTokenPermissions struct { Actions perm.AccessMode `json:"actions"` // Wiki - read/write/none Wiki perm.AccessMode `json:"wiki"` + // Releases - read/write/none + Releases perm.AccessMode `json:"releases"` + // Projects - read/write/none + Projects perm.AccessMode `json:"projects"` } // HasAccess checks if the permission meets the required access level for the given scope @@ -212,6 +216,10 @@ func (p ActionsTokenPermissions) HasAccess(scope string, required perm.AccessMod mode = p.PullRequests case "wiki": mode = p.Wiki + case "releases": + mode = p.Releases + case "projects": + mode = p.Projects } return mode >= required } @@ -236,6 +244,8 @@ func DefaultActionsTokenPermissions(mode ActionsTokenPermissionMode) ActionsToke Packages: perm.AccessModeRead, Actions: perm.AccessModeRead, Wiki: perm.AccessModeRead, + Releases: perm.AccessModeRead, + Projects: perm.AccessModeRead, } } // Permissive mode (default) @@ -246,6 +256,8 @@ func DefaultActionsTokenPermissions(mode ActionsTokenPermissionMode) ActionsToke Packages: perm.AccessModeRead, // Packages read by default for security Actions: perm.AccessModeWrite, Wiki: perm.AccessModeWrite, + Releases: perm.AccessModeWrite, + Projects: perm.AccessModeWrite, } } @@ -258,6 +270,8 @@ func ForkPullRequestPermissions() ActionsTokenPermissions { Packages: perm.AccessModeRead, Actions: perm.AccessModeRead, Wiki: perm.AccessModeRead, + Releases: perm.AccessModeRead, + Projects: perm.AccessModeRead, } } @@ -370,6 +384,8 @@ func (cfg *ActionsConfig) GetMaxTokenPermissions() ActionsTokenPermissions { Packages: perm.AccessModeWrite, Actions: perm.AccessModeWrite, Wiki: perm.AccessModeWrite, + Releases: perm.AccessModeWrite, + Projects: perm.AccessModeWrite, } } @@ -383,6 +399,8 @@ func (cfg *ActionsConfig) ClampPermissions(perms ActionsTokenPermissions) Action Packages: min(perms.Packages, maxPerms.Packages), Actions: min(perms.Actions, maxPerms.Actions), Wiki: min(perms.Wiki, maxPerms.Wiki), + Releases: min(perms.Releases, maxPerms.Releases), + Projects: min(perms.Projects, maxPerms.Projects), } } diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index 79177e6061..98b2227a6a 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -3749,6 +3749,10 @@ "actions.general.token_permissions.pull_requests.description": "Pull requests and related comments, assignees, labels, and milestones.", "actions.general.token_permissions.wiki": "Wiki", "actions.general.token_permissions.wiki.description": "Wiki pages and files.", + "actions.general.token_permissions.releases": "Releases", + "actions.general.token_permissions.releases.description": "Repository releases and tags.", + "actions.general.token_permissions.projects": "Projects", + "actions.general.token_permissions.projects.description": "Repository projects and boards.", "actions.general.token_permissions.packages": "Packages", "actions.general.token_permissions.packages.description": "Packages and container images.", "actions.general.token_permissions.actions_scope": "Actions", diff --git a/routers/web/org/setting/actions.go b/routers/web/org/setting/actions.go index fd5cd723b3..fa62cbde9e 100644 --- a/routers/web/org/setting/actions.go +++ b/routers/web/org/setting/actions.go @@ -39,6 +39,25 @@ func ActionsGeneral(ctx *context.Context) { ctx.Data["AllowCrossRepoAccess"] = actionsCfg.AllowCrossRepoAccess ctx.Data["HasSelectedRepos"] = len(actionsCfg.AllowedCrossRepoIDs) > 0 + // Load Allowed Repositories + var allowedRepos []*repo_model.Repository + if len(actionsCfg.AllowedCrossRepoIDs) > 0 { + // Since the list shouldn't be too long, we can loop. + // Ideally use GetRepositoriesByIDs but simple loop is fine for now. + for _, id := range actionsCfg.AllowedCrossRepoIDs { + repo, err := repo_model.GetRepositoryByID(ctx, id) + if err != nil { + if repo_model.IsErrRepoNotExist(err) { + continue + } + ctx.ServerError("GetRepositoryByID", err) + return + } + allowedRepos = append(allowedRepos, repo) + } + } + ctx.Data["AllowedRepos"] = allowedRepos + ctx.HTML(http.StatusOK, tplSettingsActionsGeneral) } @@ -105,3 +124,83 @@ func ActionsGeneralPost(ctx *context.Context) { ctx.Flash.Success(ctx.Tr("org.settings.update_setting_success")) ctx.Redirect(ctx.Org.OrgLink + "/settings/actions") } + +// ActionsAllowedReposAdd adds a repository to the allowed list for cross-repo access +func ActionsAllowedReposAdd(ctx *context.Context) { + repoName := ctx.FormString("repo_name") + if repoName == "" { + ctx.Redirect(ctx.Org.OrgLink + "/settings/actions") + return + } + + repo, err := repo_model.GetRepositoryByName(ctx, ctx.Org.Organization.ID, repoName) + if err != nil { + if repo_model.IsErrRepoNotExist(err) { + ctx.Flash.Error(ctx.Tr("repo.not_exist")) + ctx.Redirect(ctx.Org.OrgLink + "/settings/actions") + return + } + ctx.ServerError("GetRepositoryByName", err) + return + } + + actionsCfg, err := actions_model.GetOrgActionsConfig(ctx, ctx.Org.Organization.AsUser().ID) + if err != nil { + ctx.ServerError("GetOrgActionsConfig", err) + return + } + + // Check if already exists + for _, id := range actionsCfg.AllowedCrossRepoIDs { + if id == repo.ID { + ctx.Redirect(ctx.Org.OrgLink + "/settings/actions") + return + } + } + + actionsCfg.AllowedCrossRepoIDs = append(actionsCfg.AllowedCrossRepoIDs, repo.ID) + // Ensure mode is set to selected if we are adding specific repos? + // Logic: If user adds a repo, they probably want it enabled. + // But let's respect the current mode toggle. If "all" or "none" is set, adding a repo updates the list but might not activate "selected" mode unless user explicitly chose "selected". + // However, if "selected" is active, this adds to it. + + if err := actions_model.SetOrgActionsConfig(ctx, ctx.Org.Organization.AsUser().ID, actionsCfg); err != nil { + ctx.ServerError("SetOrgActionsConfig", err) + return + } + + ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success")) + ctx.Redirect(ctx.Org.OrgLink + "/settings/actions") +} + +// ActionsAllowedReposRemove removes a repository from the allowed list +func ActionsAllowedReposRemove(ctx *context.Context) { + repoID := ctx.FormInt64("repo_id") + if repoID == 0 { + ctx.Redirect(ctx.Org.OrgLink + "/settings/actions") + return + } + + actionsCfg, err := actions_model.GetOrgActionsConfig(ctx, ctx.Org.Organization.AsUser().ID) + if err != nil { + ctx.ServerError("GetOrgActionsConfig", err) + return + } + + // Filter out the ID + newIDs := make([]int64, 0, len(actionsCfg.AllowedCrossRepoIDs)) + for _, id := range actionsCfg.AllowedCrossRepoIDs { + if id != repoID { + newIDs = append(newIDs, id) + } + } + actionsCfg.AllowedCrossRepoIDs = newIDs + + if err := actions_model.SetOrgActionsConfig(ctx, ctx.Org.Organization.AsUser().ID, actionsCfg); err != nil { + ctx.ServerError("SetOrgActionsConfig", err) + return + } + + ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success")) + ctx.Redirect(ctx.Org.OrgLink + "/settings/actions") +} diff --git a/routers/web/repo/setting/actions.go b/routers/web/repo/setting/actions.go index a717191255..cd974c2371 100644 --- a/routers/web/repo/setting/actions.go +++ b/routers/web/repo/setting/actions.go @@ -183,6 +183,9 @@ func UpdateTokenPermissions(ctx *context.Context) { Packages: parseMaxPerm("packages"), PullRequests: parseMaxPerm("pull_requests"), Wiki: parseMaxPerm("wiki"), + Actions: parseMaxPerm("actions"), + Releases: parseMaxPerm("releases"), + Projects: parseMaxPerm("projects"), } } else { actionsCfg.MaxTokenPermissions = nil diff --git a/routers/web/web.go b/routers/web/web.go index 0e93a1476a..9a704a16ea 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -961,6 +961,10 @@ func registerWebRoutes(m *web.Router) { m.Group("/actions", func() { m.Get("", org_setting.ActionsGeneral) m.Post("", org_setting.ActionsGeneralPost) + m.Group("/allowed_repos", func() { + m.Post("/add", org_setting.ActionsAllowedReposAdd) + m.Post("/remove", org_setting.ActionsAllowedReposRemove) + }) addSettingsRunnersRoutes() addSettingsSecretsRoutes() addSettingsVariablesRoutes() diff --git a/services/actions/permission_parser.go b/services/actions/permission_parser.go index 830fb84c9e..47edec689a 100644 --- a/services/actions/permission_parser.go +++ b/services/actions/permission_parser.go @@ -70,6 +70,8 @@ func parseRawPermissions(rawPerms *yaml.Node, defaultPerms repo_model.ActionsTok Packages: perm.AccessModeRead, Actions: perm.AccessModeRead, Wiki: perm.AccessModeRead, + Releases: perm.AccessModeRead, + Projects: perm.AccessModeRead, } case "write-all": return repo_model.ActionsTokenPermissions{ @@ -79,6 +81,8 @@ func parseRawPermissions(rawPerms *yaml.Node, defaultPerms repo_model.ActionsTok Packages: perm.AccessModeWrite, Actions: perm.AccessModeWrite, Wiki: perm.AccessModeWrite, + Releases: perm.AccessModeWrite, + Projects: perm.AccessModeWrite, } } return defaultPerms @@ -117,6 +121,10 @@ func parseRawPermissions(rawPerms *yaml.Node, defaultPerms repo_model.ActionsTok result.Actions = accessMode case "wiki": result.Wiki = accessMode + case "releases": + result.Releases = accessMode + case "projects": + result.Projects = accessMode // Additional GitHub scopes we don't explicitly handle yet: // These fall through to defaults // - deployments, environments, id-token, pages, repository-projects, security-events, statuses diff --git a/templates/org/settings/actions_general.tmpl b/templates/org/settings/actions_general.tmpl index 871ddcc4b2..ef4bfff980 100644 --- a/templates/org/settings/actions_general.tmpl +++ b/templates/org/settings/actions_general.tmpl @@ -16,24 +16,53 @@
- +
- +
- +
+ +
+
+ {{ctx.Locale.Tr "actions.general.token_permissions.allowed_repos"}} +
+
+ {{range .AllowedRepos}} +
+
+ +
+
+ {{.Name}} +
+
+ {{else}} +
{{ctx.Locale.Tr "org.repos.none"}}
+ {{end}} +
+ +
+ {{ctx.Locale.Tr "actions.general.token_permissions.add_repo"}} +
+
+ + +
+
+
diff --git a/templates/repo/settings/actions_general.tmpl b/templates/repo/settings/actions_general.tmpl index 962fc8a8b0..9c31fb02c7 100644 --- a/templates/repo/settings/actions_general.tmpl +++ b/templates/repo/settings/actions_general.tmpl @@ -196,6 +196,56 @@ + + + + {{ctx.Locale.Tr "actions.general.token_permissions.releases"}} +

{{ctx.Locale.Tr "actions.general.token_permissions.releases.description"}}

+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + + + + + {{ctx.Locale.Tr "actions.general.token_permissions.projects"}} +

{{ctx.Locale.Tr "actions.general.token_permissions.projects.description"}}

+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + diff --git a/web_src/js/features/repo-settings-actions.ts b/web_src/js/features/repo-settings-actions.ts index 6dc549d329..66136841b1 100644 --- a/web_src/js/features/repo-settings-actions.ts +++ b/web_src/js/features/repo-settings-actions.ts @@ -28,11 +28,11 @@ export function initActionsPermissionsTable(): void { for (const input of inputs) { input.disabled = tableDisabled; } - permTable.style.opacity = tableDisabled ? '0.5' : '1'; + permTable.style.display = tableDisabled ? 'none' : ''; } if (tableSection) { - tableSection.style.opacity = tableDisabled ? '0.5' : '1'; + tableSection.style.display = tableDisabled ? 'none' : ''; } } @@ -43,4 +43,23 @@ export function initActionsPermissionsTable(): void { followOrgCheckbox?.addEventListener('change', updateTableState); updateTableState(); + + // Cross-Repo Access Table Toggle + const crossRepoRadios = document.querySelectorAll('.js-cross-repo-mode'); + const allowedReposSection = document.querySelector('#allowed-repos-section'); + + if (crossRepoRadios.length && allowedReposSection) { + function updateCrossRepoState(): void { + const selectedMode = document.querySelector('input[name="cross_repo_mode"]:checked'); + const isSelected = selectedMode?.value === 'selected'; + if (allowedReposSection) { + allowedReposSection.style.display = isSelected ? '' : 'none'; + } + } + + for (const radio of crossRepoRadios) { + radio.addEventListener('change', updateCrossRepoState); + } + updateCrossRepoState(); + } }