diff --git a/routers/api/v1/repo/repo_actions_permissions.go b/routers/api/v1/repo/repo_actions_permissions.go new file mode 100644 index 0000000000..03c1f7b5cf --- /dev/null +++ b/routers/api/v1/repo/repo_actions_permissions.go @@ -0,0 +1,208 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "net/http" + + actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/web" + api "code.gitea.io/gitea/modules/structs" +) + +// GetActionsPermissions returns the Actions token permissions for a repository +func GetActionsPermissions(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/settings/actions/permissions repository repoGetActionsPermissions + // --- + // summary: Get repository Actions token permissions + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/responses/ActionsPermissionsResponse" + // "404": + // "$ref": "#/responses/notFound" + + // Check if user has admin access to this repo + // NOTE: Only repo admins should be able to view/modify permission settings + // This is important for security - we don't want regular contributors + // to be able to grant themselves elevated permissions via Actions + if !ctx.Repo.IsAdmin() { + ctx.Error(http.StatusForbidden, "NoPermission", "You must be a repository admin to access this") + return + } + + perms, err := actions_model.GetRepoActionPermissions(ctx, ctx.Repo.Repository.ID) + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetPermissions", err) + return + } + + // If no custom permissions are set, return the default (restricted mode) + // This is intentional - we want a secure default that requires explicit opt-in + // to more permissive settings. See: https://github.com/go-gitea/gitea/issues/24635 + if perms == nil { + perms = &actions_model.ActionTokenPermission{ + RepoID: ctx.Repo.Repository.ID, + PermissionMode: actions_model.PermissionModeRestricted, + // Default restricted permissions - only read contents and metadata + ContentsRead: true, + MetadataRead: true, + } + } + + ctx.JSON(http.StatusOK, convertToAPIPermissions(perms)) +} + +// UpdateActionsPermissions updates the Actions token permissions for a repository +func UpdateActionsPermissions(ctx *context.APIContext) { + // swagger:operation PUT /repos/{owner}/{repo}/settings/actions/permissions repository repoUpdateActionsPermissions + // --- + // summary: Update repository Actions token permissions + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/ActionsPermissions" + // responses: + // "200": + // "$ref": "#/responses/ActionsPermissionsResponse" + // "403": + // "$ref": "#/responses/forbidden" + // "422": + // "$ref": "#/responses/validationError" + + if !ctx.Repo.IsAdmin() { + ctx.Error(http.StatusForbidden, "NoPermission", "You must be a repository admin to modify this") + return + } + + form := web.GetForm(ctx).(*api.ActionsPermissions) + + // Validate permission mode + if form.PermissionMode < 0 || form.PermissionMode > 2 { + ctx.Error(http.StatusUnprocessableEntity, "InvalidMode", "Permission mode must be 0 (restricted), 1 (permissive), or 2 (custom)") + return + } + + // TODO: Check if org-level permissions exist and validate against them + // For now, we'll implement basic validation, but we should enhance this + // to ensure repo settings don't exceed org caps. This is important for + // multi-repository organizations where admins want centralized control. + // See wolfogre's comment: https://github.com/go-gitea/gitea/pull/24554#issuecomment-1537040811 + + perm := &actions_model.ActionTokenPermission{ + RepoID: ctx.Repo.Repository.ID, + PermissionMode: actions_model.PermissionMode(form.PermissionMode), + ActionsRead: form.ActionsRead, + ActionsWrite: form.ActionsWrite, + ContentsRead: form.ContentsRead, + ContentsWrite: form.ContentsWrite, + IssuesRead: form.IssuesRead, + IssuesWrite: form.IssuesWrite, + PackagesRead: form.PackagesRead, + PackagesWrite: form.PackagesWrite, + PullRequestsRead: form.PullRequestsRead, + PullRequestsWrite: form.PullRequestsWrite, + MetadataRead: true, // Always true - needed for basic operations + } + + if err := actions_model.CreateOrUpdateRepoPermissions(ctx, perm); err != nil { + ctx.Error(http.StatusInternalServerError, "UpdatePermissions", err) + return + } + + ctx.JSON(http.StatusOK, convertToAPIPermissions(perm)) +} + +// ResetActionsPermissions resets permissions to default (restricted mode) +func ResetActionsPermissions(ctx *context.APIContext) { + // swagger:operation DELETE /repos/{owner}/{repo}/settings/actions/permissions repository repoResetActionsPermissions + // --- + // summary: Reset repository Actions permissions to default + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + // "403": + // "$ref": "#/responses/forbidden" + + if !ctx.Repo.IsAdmin() { + ctx.Error(http.StatusForbidden, "NoPermission", "You must be a repository admin") + return + } + + // Create default restricted permissions + // This is a "safe reset" - puts the repo back to secure defaults + defaultPerm := &actions_model.ActionTokenPermission{ + RepoID: ctx.Repo.Repository.ID, + PermissionMode: actions_model.PermissionModeRestricted, + ContentsRead: true, + MetadataRead: true, + } + + if err := actions_model.CreateOrUpdateRepoPermissions(ctx, defaultPerm); err != nil { + ctx.Error(http.StatusInternalServerError, "ResetPermissions", err) + return + } + + ctx.Status(http.StatusNoContent) +} + +// convertToAPIPermissions converts model to API response format +// This helper keeps our internal model separate from the API contract +func convertToAPIPermissions(perm *actions_model.ActionTokenPermission) *api.ActionsPermissions { + return &api.ActionsPermissions{ + PermissionMode: int(perm.PermissionMode), + ActionsRead: perm.ActionsRead, + ActionsWrite: perm.ActionsWrite, + ContentsRead: perm.ContentsRead, + ContentsWrite: perm.ContentsWrite, + IssuesRead: perm.IssuesRead, + IssuesWrite: perm.IssuesWrite, + PackagesRead: perm.PackagesRead, + PackagesWrite: perm.PackagesWrite, + PullRequestsRead: perm.PullRequestsRead, + PullRequestsWrite: perm.PullRequestsWrite, + MetadataRead: perm.MetadataRead, + } +}