0
0
mirror of https://github.com/go-gitea/gitea.git synced 2026-02-20 22:14:50 +01:00

Implement all requested changes

This commit is contained in:
Excellencedev 2026-01-06 15:49:34 +01:00
parent d26294839e
commit 9610d7f27a
9 changed files with 239 additions and 5 deletions

View File

@ -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),
}
}

View File

@ -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",

View File

@ -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")
}

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -16,24 +16,53 @@
<div class="grouped fields">
<div class="field">
<div class="ui radio checkbox">
<input type="radio" name="cross_repo_mode" value="none" {{if not .AllowCrossRepoAccess}}checked{{end}}>
<input type="radio" name="cross_repo_mode" value="none" {{if not .AllowCrossRepoAccess}}checked{{end}} class="js-cross-repo-mode">
<label>{{ctx.Locale.Tr "settings.disabled"}}</label>
</div>
</div>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" name="cross_repo_mode" value="all" {{if and .AllowCrossRepoAccess (not .HasSelectedRepos)}}checked{{end}}>
<input type="radio" name="cross_repo_mode" value="all" {{if and .AllowCrossRepoAccess (not .HasSelectedRepos)}}checked{{end}} class="js-cross-repo-mode">
<label>{{ctx.Locale.Tr "actions.general.token_permissions.cross_repo_all"}}</label>
</div>
</div>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" name="cross_repo_mode" value="selected" {{if and .AllowCrossRepoAccess .HasSelectedRepos}}checked{{end}}>
<input type="radio" name="cross_repo_mode" value="selected" {{if and .AllowCrossRepoAccess .HasSelectedRepos}}checked{{end}} class="js-cross-repo-mode">
<label>{{ctx.Locale.Tr "actions.general.token_permissions.cross_repo_selected"}}</label>
</div>
</div>
</div>
<!-- Allowed Repositories List -->
<div id="allowed-repos-section" class="field">
<h5 class="ui header">
{{ctx.Locale.Tr "actions.general.token_permissions.allowed_repos"}}
</h5>
<div class="ui middle aligned divided list">
{{range .AllowedRepos}}
<div class="item">
<div class="right floated content">
<button class="ui red tiny button" formmethod="post" formaction="{{$.Link}}/allowed_repos/remove?repo_id={{.ID}}">{{ctx.Locale.Tr "remove"}}</button>
</div>
<div class="content">
<a href="{{.Link}}">{{.Name}}</a>
</div>
</div>
{{else}}
<div class="item text empty">{{ctx.Locale.Tr "org.repos.none"}}</div>
{{end}}
</div>
<h5 class="ui header">
{{ctx.Locale.Tr "actions.general.token_permissions.add_repo"}}
</h5>
<div class="ui action input">
<input name="repo_name" placeholder="{{ctx.Locale.Tr "search.search_repo"}}">
<button class="ui green button" formmethod="post" formaction="{{$.Link}}/allowed_repos/add">{{ctx.Locale.Tr "add"}}</button>
</div>
</div>
<div class="divider"></div>
<!-- Default Permission Mode -->

View File

@ -196,6 +196,56 @@
</div>
</td>
</tr>
<!-- Releases -->
<tr>
<td>
<strong>{{ctx.Locale.Tr "actions.general.token_permissions.releases"}}</strong>
<p class="text small grey">{{ctx.Locale.Tr "actions.general.token_permissions.releases.description"}}</p>
</td>
<td style="text-align: center">
<div class="ui radio checkbox">
<input type="radio" name="max_releases" value="none" {{if not ($.MaxTokenPermissions.HasRead "releases")}}checked{{end}}>
<label></label>
</div>
</td>
<td style="text-align: center">
<div class="ui radio checkbox">
<input type="radio" name="max_releases" value="read" {{if and ($.MaxTokenPermissions.HasRead "releases") (not ($.MaxTokenPermissions.HasWrite "releases"))}}checked{{end}}>
<label></label>
</div>
</td>
<td style="text-align: center">
<div class="ui radio checkbox">
<input type="radio" name="max_releases" value="write" {{if $.MaxTokenPermissions.HasWrite "releases"}}checked{{end}}>
<label></label>
</div>
</td>
</tr>
<!-- Projects -->
<tr>
<td>
<strong>{{ctx.Locale.Tr "actions.general.token_permissions.projects"}}</strong>
<p class="text small grey">{{ctx.Locale.Tr "actions.general.token_permissions.projects.description"}}</p>
</td>
<td style="text-align: center">
<div class="ui radio checkbox">
<input type="radio" name="max_projects" value="none" {{if not ($.MaxTokenPermissions.HasRead "projects")}}checked{{end}}>
<label></label>
</div>
</td>
<td style="text-align: center">
<div class="ui radio checkbox">
<input type="radio" name="max_projects" value="read" {{if and ($.MaxTokenPermissions.HasRead "projects") (not ($.MaxTokenPermissions.HasWrite "projects"))}}checked{{end}}>
<label></label>
</div>
</td>
<td style="text-align: center">
<div class="ui radio checkbox">
<input type="radio" name="max_projects" value="write" {{if $.MaxTokenPermissions.HasWrite "projects"}}checked{{end}}>
<label></label>
</div>
</td>
</tr>
<!-- Packages -->
<tr>
<td>

View File

@ -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<HTMLInputElement>('.js-cross-repo-mode');
const allowedReposSection = document.querySelector<HTMLElement>('#allowed-repos-section');
if (crossRepoRadios.length && allowedReposSection) {
function updateCrossRepoState(): void {
const selectedMode = document.querySelector<HTMLInputElement>('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();
}
}