diff --git a/models/perm/access/repo_permission.go b/models/perm/access/repo_permission.go index 8844e5bad2..c3f6a3afc5 100644 --- a/models/perm/access/repo_permission.go +++ b/models/perm/access/repo_permission.go @@ -346,13 +346,37 @@ func GetActionsUserRepoPermission(ctx context.Context, repo *repo_model.Reposito effectivePerms, err = repo_model.UnmarshalTokenPermissions(task.Job.TokenPermissions) if err != nil { // Fall back to repository settings if unmarshal fails - effectivePerms = actionsCfg.GetEffectiveTokenPermissions(task.IsForkPullRequest) - effectivePerms = actionsCfg.ClampPermissions(effectivePerms) + // If following org config, we need to load it + if !actionsCfg.OverrideOrgConfig && repo.Owner.IsOrganization() { + orgCfg, err := actions_model.GetOrgActionsConfig(ctx, repo.OwnerID) + if err != nil { + log.Error("GetOrgActionsConfig: %v", err) + effectivePerms = actionsCfg.GetEffectiveTokenPermissions(task.IsForkPullRequest) // Fallback to repo config on error + } else { + effectivePerms = orgCfg.GetEffectiveTokenPermissions(task.IsForkPullRequest) + effectivePerms = orgCfg.ClampPermissions(effectivePerms) + } + } else { + effectivePerms = actionsCfg.GetEffectiveTokenPermissions(task.IsForkPullRequest) + effectivePerms = actionsCfg.ClampPermissions(effectivePerms) + } } } else { // No workflow permissions or job not found, use repository settings - effectivePerms = actionsCfg.GetEffectiveTokenPermissions(task.IsForkPullRequest) - effectivePerms = actionsCfg.ClampPermissions(effectivePerms) + if !actionsCfg.OverrideOrgConfig && repo.Owner.IsOrganization() { + orgCfg, err := actions_model.GetOrgActionsConfig(ctx, repo.OwnerID) + if err != nil { + log.Error("GetOrgActionsConfig: %v", err) + effectivePerms = actionsCfg.GetEffectiveTokenPermissions(task.IsForkPullRequest) // Fallback to repo config on error + effectivePerms = actionsCfg.ClampPermissions(effectivePerms) + } else { + effectivePerms = orgCfg.GetEffectiveTokenPermissions(task.IsForkPullRequest) + effectivePerms = orgCfg.ClampPermissions(effectivePerms) + } + } else { + effectivePerms = actionsCfg.GetEffectiveTokenPermissions(task.IsForkPullRequest) + effectivePerms = actionsCfg.ClampPermissions(effectivePerms) + } } // Set up per-unit access modes based on configured permissions diff --git a/models/repo/repo_unit.go b/models/repo/repo_unit.go index db92d52a61..cba643cd4f 100644 --- a/models/repo/repo_unit.go +++ b/models/repo/repo_unit.go @@ -310,8 +310,8 @@ type ActionsConfig struct { 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"` + // OverrideOrgConfig indicates if this repository should override the organization-level configuration + OverrideOrgConfig bool `json:"override_org_config,omitempty"` } func (cfg *ActionsConfig) EnableWorkflow(file string) { diff --git a/routers/web/repo/setting/actions.go b/routers/web/repo/setting/actions.go index 59d5b4f160..d52023d0e6 100644 --- a/routers/web/repo/setting/actions.go +++ b/routers/web/repo/setting/actions.go @@ -46,7 +46,7 @@ func ActionsGeneralSettings(ctx *context.Context) { // Follow org config (only for repos in orgs) ctx.Data["IsInOrg"] = ctx.Repo.Repository.Owner.IsOrganization() - ctx.Data["FollowOrgConfig"] = actionsCfg.FollowOrgConfig + ctx.Data["OverrideOrgConfig"] = actionsCfg.OverrideOrgConfig if ctx.Repo.Repository.IsPrivate { collaborativeOwnerIDs := actionsCfg.CollaborativeOwnerIDs @@ -146,11 +146,12 @@ func UpdateTokenPermissions(ctx *context.Context) { actionsCfg := actionsUnit.ActionsConfig() - // Update Follow Org Config (for repos in orgs) - actionsCfg.FollowOrgConfig = ctx.FormBool("follow_org_config") + // Update Override Org Config (for repos in orgs) + // If checked, it means we WANT to override (opt-out of following) + actionsCfg.OverrideOrgConfig = ctx.FormBool("override_org_config") - // Update permission mode (only if not following org config) - if !actionsCfg.FollowOrgConfig { + // Update permission mode (only if overriding org config) + if actionsCfg.OverrideOrgConfig { permissionMode := repo_model.ActionsTokenPermissionMode(ctx.FormString("token_permission_mode")) if permissionMode == repo_model.ActionsTokenPermissionModeRestricted || permissionMode == repo_model.ActionsTokenPermissionModePermissive || @@ -164,7 +165,7 @@ func UpdateTokenPermissions(ctx *context.Context) { } // Update Maximum Permissions (radio buttons: none/read/write) - if actionsCfg.TokenPermissionMode == repo_model.ActionsTokenPermissionModeCustom { + if actionsCfg.OverrideOrgConfig && actionsCfg.TokenPermissionMode == repo_model.ActionsTokenPermissionModeCustom { parseMaxPerm := func(name string) perm.AccessMode { value := ctx.FormString("max_" + name) switch value { diff --git a/templates/repo/settings/actions_general.tmpl b/templates/repo/settings/actions_general.tmpl index 42fa4d10c8..4c84b240fd 100644 --- a/templates/repo/settings/actions_general.tmpl +++ b/templates/repo/settings/actions_general.tmpl @@ -33,13 +33,13 @@ {{.CsrfTokenHtml}} {{if .IsInOrg}} - +
- - + +
-

{{ctx.Locale.Tr "actions.general.token_permissions.follow_org_desc"}}

+

{{ctx.Locale.Tr "actions.general.token_permissions.override_org_desc"}}

diff --git a/tests/integration/actions_job_token_test.go b/tests/integration/actions_job_token_test.go index cd3a7ef6d2..c3120b03ef 100644 --- a/tests/integration/actions_job_token_test.go +++ b/tests/integration/actions_job_token_test.go @@ -177,7 +177,7 @@ func testActionsTokenPermissionsMode(u *url.URL, mode string, expectReadOnly boo require.NoError(t, err, "Actions unit should exist for repo4") actionsCfg := actionsUnit.ActionsConfig() actionsCfg.TokenPermissionMode = repo_model.ActionsTokenPermissionMode(mode) - actionsCfg.MaxTokenPermissions = nil // Ensure no max permissions interfere + actionsCfg.MaxTokenPermissions = nil // Ensure no max permissions interfere // Update the config actionsUnit.Config = actionsCfg require.NoError(t, repo_model.UpdateRepoUnit(t.Context(), actionsUnit)) @@ -343,11 +343,12 @@ func TestActionsTokenPackagePermission(t *testing.T) { runner := newMockRunner() runner.registerAsRepoRunner(t, repo.OwnerName, repo.Name, "mock-runner", []string{"ubuntu-latest"}, false) - // Set Config: Custom Mode, Max Packages = Write - // This should implied Default Packages = Write (because Custom defaults to Max) + // Set Config: Custom Mode, Max Packages = Write, Max Code = Read req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/actions/general/token_permissions", repo.OwnerName, repo.Name), map[string]string{ + "override_org_config": "true", "token_permission_mode": "custom", "max_packages": "write", + "max_code": "read", // Ensure repo read access if needed }) session.MakeRequest(t, req, http.StatusSeeOther) @@ -375,7 +376,7 @@ jobs: writePackageURL := fmt.Sprintf("/api/packages/%s/generic/%s/%s/test.bin", user2.Name, packageName, packageVersion) uploadReq := NewRequestWithBody(t, "PUT", writePackageURL, bytes.NewReader([]byte{1, 2, 3})). AddTokenAuth(taskToken) - + // Should Succeed (201) MakeRequest(t, uploadReq, http.StatusCreated) }) @@ -441,26 +442,26 @@ func TestActionsCrossRepoAccess(t *testing.T) { Reponame: "repo-B", } - // Case A: Default (AllowCrossRepoAccess = false/unset) -> Should Fail (404 Not Found) - // API returns 404 for private repos you can't access, not 403, to avoid leaking existence. - testCtx.ExpectedCode = http.StatusNotFound - t.Run("Cross-Repo Access Denied (Default)", doAPIGetRepository(testCtx, nil)) + // Case A: Default (AllowCrossRepoAccess = true by default now) -> Should Succeed (200) Read-Only + // API returns 404 if denied (hidden), 200 if allowed. + testCtx.ExpectedCode = http.StatusOK + t.Run("Cross-Repo Access Allowed (Default)", doAPIGetRepository(testCtx, func(t *testing.T, r structs.Repository) { + assert.Equal(t, "repo-B", r.Name) + })) - // Case B: Enable AllowCrossRepoAccess + // Case B: Explicitly Disable AllowCrossRepoAccess org, err := org_model.GetOrgByName(t.Context(), orgName) require.NoError(t, err) cfg := &repo_model.ActionsConfig{ - AllowCrossRepoAccess: true, + AllowCrossRepoAccess: false, } err = actions_model.SetOrgActionsConfig(t.Context(), org.ID, cfg) require.NoError(t, err) - // Retry -> Should Succeed (200) - Read Only - testCtx.ExpectedCode = http.StatusOK - t.Run("Cross-Repo Access Allowed", doAPIGetRepository(testCtx, func(t *testing.T, r structs.Repository) { - assert.Equal(t, "repo-B", r.Name) - })) + // Retry -> Should Fail (404 Not Found) + testCtx.ExpectedCode = http.StatusNotFound + t.Run("Cross-Repo Access Denied (Disabled)", doAPIGetRepository(testCtx, nil)) // 6. Test Cross-Repo Package Access t.Run("Cross-Repo Package Access", func(t *testing.T) {