mirror of https://github.com/go-gitea/gitea.git
318 lines
10 KiB
Go
318 lines
10 KiB
Go
// Copyright 2024 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package org
|
|
|
|
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 an organization
|
|
func GetActionsPermissions(ctx *context.APIContext) {
|
|
// swagger:operation GET /orgs/{org}/settings/actions/permissions organization orgGetActionsPermissions
|
|
// ---
|
|
// summary: Get organization Actions token permissions
|
|
// produces:
|
|
// - application/json
|
|
// parameters:
|
|
// - name: org
|
|
// in: path
|
|
// description: name of the organization
|
|
// type: string
|
|
// required: true
|
|
// responses:
|
|
// "200":
|
|
// "$ref": "#/responses/OrgActionsPermissionsResponse"
|
|
// "404":
|
|
// "$ref": "#/responses/notFound"
|
|
|
|
// Organization settings are more sensitive than repo settings because they
|
|
// affect ALL repositories in the org. We should be extra careful here.
|
|
// Only org owners should be able to modify these settings.
|
|
if !ctx.Org.IsOwner {
|
|
ctx.Error(http.StatusForbidden, "NoPermission", "You must be an organization owner")
|
|
return
|
|
}
|
|
|
|
perms, err := actions_model.GetOrgActionPermissions(ctx, ctx.Org.Organization.ID)
|
|
if err != nil {
|
|
ctx.Error(http.StatusInternalServerError, "GetOrgPermissions", err)
|
|
return
|
|
}
|
|
|
|
// Return default if no custom config exists
|
|
// Organizations default to restricted mode for maximum security
|
|
// Individual repos can be given more permissions if needed
|
|
if perms == nil {
|
|
perms = &actions_model.ActionOrgPermission{
|
|
OrgID: ctx.Org.Organization.ID,
|
|
PermissionMode: actions_model.PermissionModeRestricted,
|
|
AllowRepoOverride: true, // Allow repos to configure their own settings
|
|
ContentsRead: true,
|
|
MetadataRead: true,
|
|
}
|
|
}
|
|
|
|
ctx.JSON(http.StatusOK, convertToAPIOrgPermissions(perms))
|
|
}
|
|
|
|
// UpdateActionsPermissions updates the Actions token permissions for an organization
|
|
func UpdateActionsPermissions(ctx *context.APIContext) {
|
|
// swagger:operation PUT /orgs/{org}/settings/actions/permissions organization orgUpdateActionsPermissions
|
|
// ---
|
|
// summary: Update organization Actions token permissions
|
|
// consumes:
|
|
// - application/json
|
|
// produces:
|
|
// - application/json
|
|
// parameters:
|
|
// - name: org
|
|
// in: path
|
|
// description: name of the organization
|
|
// type: string
|
|
// required: true
|
|
// - name: body
|
|
// in: body
|
|
// schema:
|
|
// "$ref": "#/definitions/OrgActionsPermissions"
|
|
// responses:
|
|
// "200":
|
|
// "$ref": "#/responses/OrgActionsPermissionsResponse"
|
|
// "403":
|
|
// "$ref": "#/responses/forbidden"
|
|
|
|
if !ctx.Org.IsOwner {
|
|
ctx.Error(http.StatusForbidden, "NoPermission", "Organization owner access required")
|
|
return
|
|
}
|
|
|
|
form := web.GetForm(ctx).(*api.OrgActionsPermissions)
|
|
|
|
// 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
|
|
}
|
|
|
|
// Important security consideration:
|
|
// If AllowRepoOverride is false, ALL repos in this org MUST use org settings.
|
|
// This is useful for security-conscious organizations that want centralized control.
|
|
// However, it's a big change, so we should log this action for audit purposes.
|
|
// TODO: Add audit logging when this feature is used
|
|
|
|
perm := &actions_model.ActionOrgPermission{
|
|
OrgID: ctx.Org.Organization.ID,
|
|
PermissionMode: actions_model.PermissionMode(form.PermissionMode),
|
|
AllowRepoOverride: form.AllowRepoOverride,
|
|
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
|
|
}
|
|
|
|
if err := actions_model.CreateOrUpdateOrgPermissions(ctx, perm); err != nil {
|
|
ctx.Error(http.StatusInternalServerError, "UpdateOrgPermissions", err)
|
|
return
|
|
}
|
|
|
|
// If AllowRepoOverride is false, we might want to update all repo permissions
|
|
// to match org settings. But that's a big operation, so let's do it lazily
|
|
// when permissions are actually checked, rather than updating all repos here.
|
|
// This is more performant and avoids potential race conditions.
|
|
|
|
ctx.JSON(http.StatusOK, convertToAPIOrgPermissions(perm))
|
|
}
|
|
|
|
// ListCrossRepoAccess lists all cross-repository access rules for an organization
|
|
func ListCrossRepoAccess(ctx *context.APIContext) {
|
|
// swagger:operation GET /orgs/{org}/settings/actions/cross-repo-access organization orgListCrossRepoAccess
|
|
// ---
|
|
// summary: List cross-repository access rules
|
|
// produces:
|
|
// - application/json
|
|
// parameters:
|
|
// - name: org
|
|
// in: path
|
|
// description: name of the organization
|
|
// type: string
|
|
// required: true
|
|
// responses:
|
|
// "200":
|
|
// "$ref": "#/responses/CrossRepoAccessList"
|
|
|
|
if !ctx.Org.IsOwner {
|
|
ctx.Error(http.StatusForbidden, "NoPermission", "Organization owner access required")
|
|
return
|
|
}
|
|
|
|
// This is a critical security feature - cross-repo access allows one repo's
|
|
// Actions to access another repo's code/resources. We need to be very careful
|
|
// about how we implement this. See the discussion:
|
|
// https://github.com/go-gitea/gitea/issues/24635
|
|
|
|
rules, err := actions_model.ListCrossRepoAccessRules(ctx, ctx.Org.Organization.ID)
|
|
if err != nil {
|
|
ctx.Error(http.StatusInternalServerError, "ListCrossRepoAccess", err)
|
|
return
|
|
}
|
|
|
|
apiRules := make([]*api.CrossRepoAccessRule, len(rules))
|
|
for i, rule := range rules {
|
|
apiRules[i] = convertToCrossRepoAccessRule(rule)
|
|
}
|
|
|
|
ctx.JSON(http.StatusOK, apiRules)
|
|
}
|
|
|
|
// AddCrossRepoAccess adds a new cross-repository access rule
|
|
func AddCrossRepoAccess(ctx *context.APIContext) {
|
|
// swagger:operation POST /orgs/{org}/settings/actions/cross-repo-access organization orgAddCrossRepoAccess
|
|
// ---
|
|
// summary: Add cross-repository access rule
|
|
// consumes:
|
|
// - application/json
|
|
// produces:
|
|
// - application/json
|
|
// parameters:
|
|
// - name: org
|
|
// in: path
|
|
// description: name of the organization
|
|
// type: string
|
|
// required: true
|
|
// - name: body
|
|
// in: body
|
|
// schema:
|
|
// "$ref": "#/definitions/CrossRepoAccessRule"
|
|
// responses:
|
|
// "201":
|
|
// "$ref": "#/responses/CrossRepoAccessRule"
|
|
// "403":
|
|
// "$ref": "#/responses/forbidden"
|
|
|
|
if !ctx.Org.IsOwner {
|
|
ctx.Error(http.StatusForbidden, "NoPermission", "Organization owner access required")
|
|
return
|
|
}
|
|
|
|
form := web.GetForm(ctx).(*api.CrossRepoAccessRule)
|
|
|
|
// Validation: source and target repos must both belong to this org
|
|
// We don't want to allow cross-organization access - that would be a
|
|
// security nightmare and makes audit trails very complex.
|
|
// TODO: Verify both repos belong to this org
|
|
|
|
// Validation: Access level must be valid (0=none, 1=read, 2=write)
|
|
if form.AccessLevel < 0 || form.AccessLevel > 2 {
|
|
ctx.Error(http.StatusUnprocessableEntity, "InvalidAccessLevel",
|
|
"Access level must be 0 (none), 1 (read), or 2 (write)")
|
|
return
|
|
}
|
|
|
|
rule := &actions_model.ActionCrossRepoAccess{
|
|
OrgID: ctx.Org.Organization.ID,
|
|
SourceRepoID: form.SourceRepoID,
|
|
TargetRepoID: form.TargetRepoID,
|
|
AccessLevel: form.AccessLevel,
|
|
}
|
|
|
|
if err := actions_model.CreateCrossRepoAccess(ctx, rule); err != nil {
|
|
ctx.Error(http.StatusInternalServerError, "CreateCrossRepoAccess", err)
|
|
return
|
|
}
|
|
|
|
ctx.JSON(http.StatusCreated, convertToCrossRepoAccessRule(rule))
|
|
}
|
|
|
|
// DeleteCrossRepoAccess removes a cross-repository access rule
|
|
func DeleteCrossRepoAccess(ctx *context.APIContext) {
|
|
// swagger:operation DELETE /orgs/{org}/settings/actions/cross-repo-access/{id} organization orgDeleteCrossRepoAccess
|
|
// ---
|
|
// summary: Delete cross-repository access rule
|
|
// parameters:
|
|
// - name: org
|
|
// in: path
|
|
// description: name of the organization
|
|
// type: string
|
|
// required: true
|
|
// - name: id
|
|
// in: path
|
|
// description: ID of the rule to delete
|
|
// type: integer
|
|
// required: true
|
|
// responses:
|
|
// "204":
|
|
// "$ref": "#/responses/empty"
|
|
// "403":
|
|
// "$ref": "#/responses/forbidden"
|
|
|
|
if !ctx.Org.IsOwner {
|
|
ctx.Error(http.StatusForbidden, "NoPermission", "Organization owner access required")
|
|
return
|
|
}
|
|
|
|
ruleID := ctx.ParamsInt64("id")
|
|
|
|
// Security check: Verify the rule belongs to this org before deleting
|
|
// We don't want one org to be able to delete another org's rules
|
|
rule, err := actions_model.GetCrossRepoAccessByID(ctx, ruleID)
|
|
if err != nil {
|
|
ctx.Error(http.StatusNotFound, "RuleNotFound", "Cross-repo access rule not found")
|
|
return
|
|
}
|
|
|
|
if rule.OrgID != ctx.Org.Organization.ID {
|
|
ctx.Error(http.StatusForbidden, "WrongOrg", "This rule belongs to a different organization")
|
|
return
|
|
}
|
|
|
|
if err := actions_model.DeleteCrossRepoAccess(ctx, ruleID); err != nil {
|
|
ctx.Error(http.StatusInternalServerError, "DeleteCrossRepoAccess", err)
|
|
return
|
|
}
|
|
|
|
ctx.Status(http.StatusNoContent)
|
|
}
|
|
|
|
// Helper functions
|
|
|
|
func convertToAPIOrgPermissions(perm *actions_model.ActionOrgPermission) *api.OrgActionsPermissions {
|
|
return &api.OrgActionsPermissions{
|
|
PermissionMode: int(perm.PermissionMode),
|
|
AllowRepoOverride: perm.AllowRepoOverride,
|
|
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,
|
|
}
|
|
}
|
|
|
|
func convertToCrossRepoAccessRule(rule *actions_model.ActionCrossRepoAccess) *api.CrossRepoAccessRule {
|
|
return &api.CrossRepoAccessRule{
|
|
ID: rule.ID,
|
|
OrgID: rule.OrgID,
|
|
SourceRepoID: rule.SourceRepoID,
|
|
TargetRepoID: rule.TargetRepoID,
|
|
AccessLevel: rule.AccessLevel,
|
|
}
|
|
}
|