0
0
mirror of https://github.com/go-gitea/gitea.git synced 2026-02-12 15:26:58 +01:00

Re-implement changes for feedback

This commit is contained in:
Excellencedev 2025-12-26 09:18:59 +01:00
parent 7df7f72a71
commit 4ccb766bf2
10 changed files with 205 additions and 52 deletions

View File

@ -176,6 +176,8 @@ const (
ActionsTokenPermissionModePermissive ActionsTokenPermissionMode = "permissive"
// ActionsTokenPermissionModeRestricted - read access by default
ActionsTokenPermissionModeRestricted ActionsTokenPermissionMode = "restricted"
// ActionsTokenPermissionModeCustom - custom permissions defined by MaxTokenPermissions
ActionsTokenPermissionModeCustom ActionsTokenPermissionMode = "custom"
)
// ActionsTokenPermissions defines the permissions for different repository units
@ -272,6 +274,10 @@ type ActionsConfig struct {
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"`
// AllowedCrossRepoIDs is a list of specific repo IDs that can be accessed cross-repo (empty means all if AllowCrossRepoAccess is true)
AllowedCrossRepoIDs []int64 `json:"allowed_cross_repo_ids,omitempty"`
// FollowOrgConfig indicates if this repository should follow the organization-level configuration
FollowOrgConfig bool `json:"follow_org_config,omitempty"`
}
func (cfg *ActionsConfig) EnableWorkflow(file string) {
@ -361,6 +367,20 @@ func (cfg *ActionsConfig) ClampPermissions(perms ActionsTokenPermissions) Action
}
}
// IsRepoAllowedCrossAccess checks if a specific repo is allowed for cross-repo access
// Returns true if AllowCrossRepoAccess is enabled AND (AllowedCrossRepoIDs is empty OR repoID is in the list)
func (cfg *ActionsConfig) IsRepoAllowedCrossAccess(repoID int64) bool {
if !cfg.AllowCrossRepoAccess {
return false
}
// If no specific repos are configured, allow all
if len(cfg.AllowedCrossRepoIDs) == 0 {
return true
}
// Check if repo is in the allowed list
return slices.Contains(cfg.AllowedCrossRepoIDs, repoID)
}
// FromDB fills up a ActionsConfig from serialized format.
func (cfg *ActionsConfig) FromDB(bs []byte) error {
return json.UnmarshalHandleDoubleEncode(bs, &cfg)

View File

@ -3740,8 +3740,12 @@
"actions.general.token_permissions.none": "None",
"actions.general.token_permissions.cross_repo": "Allow Cross-Repository Access",
"actions.general.token_permissions.cross_repo_desc": "Control whether the token can access other repositories and packages within this organization.",
"actions.general.token_permissions.cross_repo_all": "All repositories in this organization",
"actions.general.token_permissions.cross_repo_selected": "Selected repositories only",
"actions.general.token_permissions.allowed_repos": "Allowed Repositories",
"actions.general.token_permissions.add_repo": "Add Repository",
"actions.general.token_permissions.follow_org": "Follow organization-level configuration",
"actions.general.token_permissions.follow_org_desc": "Use the Actions settings configured at the organization level instead of repository-specific settings.",
"all_repositories": "All Repositories",
"specific_repositories": "Specific Repositories"
}

View File

@ -105,21 +105,26 @@ func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.Context) {
packageRepoID = ctx.Package.Descriptor.Package.RepoID
}
// If package is not linked to any repo (org-level package), deny access from Actions
// Actions tokens should only access packages linked to repos
if packageRepoID == 0 {
ctx.HTTPError(http.StatusForbidden, "reqPackageAccess", "Actions tokens cannot access packages not linked to a repository")
return
}
if task.RepoID != packageRepoID {
// Cross-repository access - check org policy first
// Cross-repository access - check org policy
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")
// Use selective cross-repo access check
if !cfg.IsRepoAllowedCrossAccess(packageRepoID) {
ctx.HTTPError(http.StatusForbidden, "reqPackageAccess", "cross-repository package access is not allowed for this repository")
return
}
// Cross-repo is enabled. For org-level packages (RepoID=0), allow access.
// For repo-linked packages, allow read access (fallthrough to permission check below).
}
}
}

View File

@ -33,9 +33,11 @@ func ActionsGeneral(ctx *context.Context) {
ctx.Data["TokenPermissionMode"] = actionsCfg.GetTokenPermissionMode()
ctx.Data["TokenPermissionModePermissive"] = repo_model.ActionsTokenPermissionModePermissive
ctx.Data["TokenPermissionModeRestricted"] = repo_model.ActionsTokenPermissionModeRestricted
ctx.Data["TokenPermissionModeCustom"] = repo_model.ActionsTokenPermissionModeCustom
ctx.Data["MaxTokenPermissions"] = actionsCfg.GetMaxTokenPermissions()
ctx.Data["AllowCrossRepoAccess"] = actionsCfg.AllowCrossRepoAccess
ctx.Data["HasSelectedRepos"] = len(actionsCfg.AllowedCrossRepoIDs) > 0
ctx.HTML(http.StatusOK, tplSettingsActionsGeneral)
}
@ -55,7 +57,8 @@ func ActionsGeneralPost(ctx *context.Context) {
// Update Token Permission Mode
permissionMode := repo_model.ActionsTokenPermissionMode(ctx.FormString("token_permission_mode"))
if permissionMode == repo_model.ActionsTokenPermissionModeRestricted ||
permissionMode == repo_model.ActionsTokenPermissionModePermissive {
permissionMode == repo_model.ActionsTokenPermissionModePermissive ||
permissionMode == repo_model.ActionsTokenPermissionModeCustom {
actionsCfg.TokenPermissionMode = permissionMode
}
@ -81,8 +84,19 @@ func ActionsGeneralPost(ctx *context.Context) {
Wiki: parseMaxPerm("wiki"),
}
// Update Cross-Repo Access
actionsCfg.AllowCrossRepoAccess = ctx.FormBool("allow_cross_repo_access")
// Update Cross-Repo Access Mode
crossRepoMode := ctx.FormString("cross_repo_mode")
switch crossRepoMode {
case "none":
actionsCfg.AllowCrossRepoAccess = false
actionsCfg.AllowedCrossRepoIDs = nil
case "all":
actionsCfg.AllowCrossRepoAccess = true
actionsCfg.AllowedCrossRepoIDs = nil
case "selected":
actionsCfg.AllowCrossRepoAccess = true
// Keep existing AllowedCrossRepoIDs, will be updated by separate API
}
if err := actions_model.SetOrgActionsConfig(ctx, ctx.Org.Organization.AsUser().ID, actionsCfg); err != nil {
ctx.ServerError("SetOrgActionsConfig", err)

View File

@ -41,8 +41,13 @@ func ActionsGeneralSettings(ctx *context.Context) {
ctx.Data["TokenPermissionMode"] = actionsCfg.GetTokenPermissionMode()
ctx.Data["TokenPermissionModePermissive"] = repo_model.ActionsTokenPermissionModePermissive
ctx.Data["TokenPermissionModeRestricted"] = repo_model.ActionsTokenPermissionModeRestricted
ctx.Data["TokenPermissionModeCustom"] = repo_model.ActionsTokenPermissionModeCustom
ctx.Data["MaxTokenPermissions"] = actionsCfg.GetMaxTokenPermissions()
// Follow org config (only for repos in orgs)
ctx.Data["IsInOrg"] = ctx.Repo.Repository.Owner.IsOrganization()
ctx.Data["FollowOrgConfig"] = actionsCfg.FollowOrgConfig
if ctx.Repo.Repository.IsPrivate {
collaborativeOwnerIDs := actionsCfg.CollaborativeOwnerIDs
collaborativeOwners, err := user_model.GetUsersByIDs(ctx, collaborativeOwnerIDs)
@ -141,15 +146,21 @@ func UpdateTokenPermissions(ctx *context.Context) {
actionsCfg := actionsUnit.ActionsConfig()
// Update permission mode
permissionMode := repo_model.ActionsTokenPermissionMode(ctx.FormString("token_permission_mode"))
if permissionMode == repo_model.ActionsTokenPermissionModeRestricted ||
permissionMode == repo_model.ActionsTokenPermissionModePermissive {
actionsCfg.TokenPermissionMode = permissionMode
} else {
ctx.Flash.Error("Invalid token permission mode")
ctx.Redirect(redirectURL)
return
// Update Follow Org Config (for repos in orgs)
actionsCfg.FollowOrgConfig = ctx.FormBool("follow_org_config")
// Update permission mode (only if not following org config)
if !actionsCfg.FollowOrgConfig {
permissionMode := repo_model.ActionsTokenPermissionMode(ctx.FormString("token_permission_mode"))
if permissionMode == repo_model.ActionsTokenPermissionModeRestricted ||
permissionMode == repo_model.ActionsTokenPermissionModePermissive ||
permissionMode == repo_model.ActionsTokenPermissionModeCustom {
actionsCfg.TokenPermissionMode = permissionMode
} else {
ctx.Flash.Error("Invalid token permission mode")
ctx.Redirect(redirectURL)
return
}
}
// Update Maximum Permissions (radio buttons: none/read/write)

View File

@ -8,12 +8,30 @@
{{.CsrfTokenHtml}}
<!-- Cross-Repository Access -->
<div class="field">
<div class="ui checkbox">
<input type="checkbox" name="allow_cross_repo_access" {{if .AllowCrossRepoAccess}}checked{{end}}>
<label><strong>{{ctx.Locale.Tr "actions.general.token_permissions.cross_repo"}}</strong></label>
<h5 class="ui header">
{{ctx.Locale.Tr "actions.general.token_permissions.cross_repo"}}
</h5>
<p class="help">{{ctx.Locale.Tr "actions.general.token_permissions.cross_repo_desc"}}</p>
<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}}>
<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}}>
<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}}>
<label>{{ctx.Locale.Tr "actions.general.token_permissions.cross_repo_selected"}}</label>
</div>
</div>
<p class="help">{{ctx.Locale.Tr "actions.general.token_permissions.cross_repo_desc"}}</p>
</div>
<div class="divider"></div>
@ -25,29 +43,38 @@
<div class="grouped fields">
<div class="field">
<div class="ui radio checkbox">
<input type="radio" name="token_permission_mode" value="permissive" {{if eq .TokenPermissionMode .TokenPermissionModePermissive}}checked{{end}}>
<label>{{ctx.Locale.Tr "actions.general.token_permissions.permissive"}}</label>
<input type="radio" name="token_permission_mode" value="permissive" {{if eq .TokenPermissionMode .TokenPermissionModePermissive}}checked{{end}} class="js-permission-mode-radio">
<label>{{ctx.Locale.Tr "actions.general.token_permissions.mode.permissive"}}</label>
</div>
<p class="help">{{ctx.Locale.Tr "actions.general.token_permissions.permissive.description"}}</p>
<p class="help">{{ctx.Locale.Tr "actions.general.token_permissions.mode.permissive.desc"}}</p>
</div>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" name="token_permission_mode" value="restricted" {{if eq .TokenPermissionMode .TokenPermissionModeRestricted}}checked{{end}}>
<label>{{ctx.Locale.Tr "actions.general.token_permissions.restricted"}}</label>
<input type="radio" name="token_permission_mode" value="restricted" {{if eq .TokenPermissionMode .TokenPermissionModeRestricted}}checked{{end}} class="js-permission-mode-radio">
<label>{{ctx.Locale.Tr "actions.general.token_permissions.mode.restricted"}}</label>
</div>
<p class="help">{{ctx.Locale.Tr "actions.general.token_permissions.restricted.description"}}</p>
<p class="help">{{ctx.Locale.Tr "actions.general.token_permissions.mode.restricted.desc"}}</p>
</div>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" name="token_permission_mode" value="custom" {{if eq .TokenPermissionMode .TokenPermissionModeCustom}}checked{{end}} class="js-permission-mode-radio">
<label>{{ctx.Locale.Tr "actions.general.token_permissions.mode.custom"}}</label>
</div>
<p class="help">{{ctx.Locale.Tr "actions.general.token_permissions.mode.custom.desc"}}</p>
</div>
</div>
<div class="divider"></div>
<!-- Maximum Permissions Table -->
<h5 class="ui header">
{{ctx.Locale.Tr "actions.general.token_permissions.maximum"}}
</h5>
<p class="help">{{ctx.Locale.Tr "actions.general.token_permissions.maximum.description"}}</p>
<div id="max-permissions-section">
<h5 class="ui header">
{{ctx.Locale.Tr "actions.general.token_permissions.max_permissions"}}
</h5>
<p class="help">{{ctx.Locale.Tr "actions.general.token_permissions.max_permissions.desc"}}</p>
</div>
<table class="ui celled table">
<table class="ui celled table js-permissions-table">
<thead>
<tr>
<th style="width: 40%">{{ctx.Locale.Tr "units.unit"}}</th>

View File

@ -32,22 +32,42 @@
<form class="ui form" action="{{.RepoLink}}/settings/actions/general/token_permissions" method="post">
{{.CsrfTokenHtml}}
<!-- Permission Mode Selection -->
{{if .IsInOrg}}
<!-- Follow Organization Configuration -->
<div class="field">
<div class="ui checkbox">
<input type="checkbox" name="follow_org_config" id="follow-org-config" {{if .FollowOrgConfig}}checked{{end}} class="js-follow-org-config">
<label><strong>{{ctx.Locale.Tr "actions.general.token_permissions.follow_org"}}</strong></label>
</div>
<p class="help">{{ctx.Locale.Tr "actions.general.token_permissions.follow_org_desc"}}</p>
</div>
<div class="divider"></div>
{{end}}
<!-- Permission Mode Selection -->
<div class="field js-permission-mode-section">
<label>{{ctx.Locale.Tr "actions.general.token_permissions.mode"}}</label>
<div class="grouped fields">
<div class="field">
<div class="ui radio checkbox">
<input type="radio" name="token_permission_mode" value="{{.TokenPermissionModePermissive}}" {{if eq .TokenPermissionMode .TokenPermissionModePermissive}}checked{{end}}>
<label>{{ctx.Locale.Tr "actions.general.token_permissions.permissive"}}</label>
<p class="help">{{ctx.Locale.Tr "actions.general.token_permissions.permissive.description"}}</p>
<input type="radio" name="token_permission_mode" value="{{.TokenPermissionModePermissive}}" {{if eq .TokenPermissionMode .TokenPermissionModePermissive}}checked{{end}} class="js-permission-mode-radio">
<label>{{ctx.Locale.Tr "actions.general.token_permissions.mode.permissive"}}</label>
<p class="help">{{ctx.Locale.Tr "actions.general.token_permissions.mode.permissive.desc"}}</p>
</div>
</div>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" name="token_permission_mode" value="{{.TokenPermissionModeRestricted}}" {{if eq .TokenPermissionMode .TokenPermissionModeRestricted}}checked{{end}}>
<label>{{ctx.Locale.Tr "actions.general.token_permissions.restricted"}}</label>
<p class="help">{{ctx.Locale.Tr "actions.general.token_permissions.restricted.description"}}</p>
<input type="radio" name="token_permission_mode" value="{{.TokenPermissionModeRestricted}}" {{if eq .TokenPermissionMode .TokenPermissionModeRestricted}}checked{{end}} class="js-permission-mode-radio">
<label>{{ctx.Locale.Tr "actions.general.token_permissions.mode.restricted"}}</label>
<p class="help">{{ctx.Locale.Tr "actions.general.token_permissions.mode.restricted.desc"}}</p>
</div>
</div>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" name="token_permission_mode" value="{{.TokenPermissionModeCustom}}" {{if eq .TokenPermissionMode .TokenPermissionModeCustom}}checked{{end}} class="js-permission-mode-radio">
<label>{{ctx.Locale.Tr "actions.general.token_permissions.mode.custom"}}</label>
<p class="help">{{ctx.Locale.Tr "actions.general.token_permissions.mode.custom.desc"}}</p>
</div>
</div>
</div>
@ -58,13 +78,15 @@
<div class="divider"></div>
<!-- Maximum Permissions Table -->
<h5 class="ui header">
{{ctx.Locale.Tr "actions.general.token_permissions.maximum"}}
<span class="ui red text">*</span>
</h5>
<p class="help">{{ctx.Locale.Tr "actions.general.token_permissions.maximum.description"}}</p>
<div id="max-permissions-section">
<h5 class="ui header">
{{ctx.Locale.Tr "actions.general.token_permissions.maximum"}}
<span class="ui red text">*</span>
</h5>
<p class="help">{{ctx.Locale.Tr "actions.general.token_permissions.maximum.description"}}</p>
</div>
<table class="ui celled table">
<table class="ui celled table js-permissions-table">
<thead>
<tr>
<th style="width: 40%">{{ctx.Locale.Tr "units.unit"}}</th>

View File

@ -15,6 +15,7 @@ import (
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
org_model "code.gitea.io/gitea/models/organization"
packages_model "code.gitea.io/gitea/models/packages"
"code.gitea.io/gitea/models/perm"
repo_model "code.gitea.io/gitea/models/repo"
unit_model "code.gitea.io/gitea/models/unit"
@ -376,17 +377,18 @@ func TestActionsCrossRepoAccess(t *testing.T) {
uploadReq := NewRequestWithBody(t, "PUT", packageURL, bytes.NewReader(content)).AddBasicAuth("user2")
MakeRequest(t, uploadReq, http.StatusCreated)
// Disable cross-repo access to test denied state
require.NoError(t, actions_model.SetOrgActionsConfig(t.Context(), org.ID, &repo_model.ActionsConfig{
AllowCrossRepoAccess: false,
}))
// Link the package to repo-B (per reviewer feedback: packages must be linked to repos)
pkg, err := packages_model.GetPackageByName(t.Context(), org.ID, packages_model.TypeGeneric, packageName)
require.NoError(t, err)
require.NoError(t, packages_model.SetRepositoryLink(t.Context(), pkg.ID, repoBID))
// By default, cross-repo is disabled
// Try to download with cross-repo disabled - should fail
downloadReqDenied := NewRequest(t, "GET", packageURL)
downloadReqDenied.Header.Set("Authorization", "Bearer "+task.Token)
MakeRequest(t, downloadReqDenied, http.StatusForbidden)
// Re-enable cross-repo access
// Enable cross-repo access
require.NoError(t, actions_model.SetOrgActionsConfig(t.Context(), org.ID, &repo_model.ActionsConfig{
AllowCrossRepoAccess: true,
}))

View File

@ -0,0 +1,46 @@
export function initActionsPermissionsTable(): void {
const modeRadios = document.querySelectorAll<HTMLInputElement>('.js-permission-mode-radio');
const permTable = document.querySelector<HTMLTableElement>('table.js-permissions-table');
const tableSection = document.querySelector<HTMLElement>('#max-permissions-section');
const followOrgCheckbox = document.querySelector<HTMLInputElement>('.js-follow-org-config');
const modeSection = document.querySelector<HTMLElement>('.js-permission-mode-section');
if (!modeRadios.length) return;
function updateTableState(): void {
const followOrg = followOrgCheckbox?.checked ?? false;
const selectedMode = document.querySelector<HTMLInputElement>('input[name="token_permission_mode"]:checked');
const isCustom = selectedMode?.value === 'custom';
// Disable entire form when following org config
for (const radio of modeRadios) {
radio.disabled = followOrg;
}
if (modeSection) {
modeSection.style.opacity = followOrg ? '0.5' : '1';
}
// Disable table if not custom OR following org
const tableDisabled = !isCustom || followOrg;
if (permTable) {
const inputs = permTable.querySelectorAll<HTMLInputElement>('input[type="radio"]');
for (const input of inputs) {
input.disabled = tableDisabled;
}
permTable.style.opacity = tableDisabled ? '0.5' : '1';
}
if (tableSection) {
tableSection.style.opacity = tableDisabled ? '0.5' : '1';
}
}
for (const radio of modeRadios) {
radio.addEventListener('change', updateTableState);
}
followOrgCheckbox?.addEventListener('change', updateTableState);
updateTableState();
}

View File

@ -64,6 +64,7 @@ import {initGlobalButtonClickOnEnter, initGlobalButtons, initGlobalDeleteButton}
import {initGlobalComboMarkdownEditor, initGlobalEnterQuickSubmit, initGlobalFormDirtyLeaveConfirm} from './features/common-form.ts';
import {callInitFunctions} from './modules/init.ts';
import {initRepoViewFileTree} from './features/repo-view-file-tree.ts';
import {initActionsPermissionsTable} from './features/repo-settings-actions.ts';
const initStartTime = performance.now();
const initPerformanceTracer = callInitFunctions([
@ -158,6 +159,7 @@ const initPerformanceTracer = callInitFunctions([
initOAuth2SettingsDisableCheckbox,
initRepoFileView,
initActionsPermissionsTable,
]);
// it must be the last one, then the "querySelectorAll" only needs to be executed once for global init functions.