diff --git a/models/actions/config.go b/models/actions/config.go index 7bd64b74d4..45d248af65 100644 --- a/models/actions/config.go +++ b/models/actions/config.go @@ -22,6 +22,7 @@ func GetOrgActionsConfig(ctx context.Context, orgID int64) (*repo_model.ActionsC cfg := &repo_model.ActionsConfig{} if val == "" { // Return defaults if no config exists + cfg.AllowCrossRepoAccess = true return cfg, nil } diff --git a/models/perm/access/repo_permission.go b/models/perm/access/repo_permission.go index f76ea20b5c..8844e5bad2 100644 --- a/models/perm/access/repo_permission.go +++ b/models/perm/access/repo_permission.go @@ -311,15 +311,16 @@ func GetActionsUserRepoPermission(ctx context.Context, repo *repo_model.Reposito // The task repo can access the current repo only if the task repo is private and // the owner of the task repo is a collaborative owner of the current repo. // FIXME should owner's visibility also be considered here? + // + // If not, we check if they are in the same org and cross-repo access is allowed. + // If allowed, we grant Read Access (consistent with old behavior and package access). + // If NOT allowed (checked above for sameOrg), we fall through to here. - // check permission like simple user but limit to read-only - perm, err = GetUserRepoPermission(ctx, repo, user_model.NewActionsUser()) - if err != nil { - return perm, err + if !isSameOrg { + return perm, nil } - perm.AccessMode = min(perm.AccessMode, perm_model.AccessModeRead) - return perm, nil } + // Cross-repo access is always read-only perm.SetUnitsWithDefaultAccessMode(repo.Units, perm_model.AccessModeRead) return perm, nil diff --git a/models/repo/repo_unit.go b/models/repo/repo_unit.go index a36f8096e2..db92d52a61 100644 --- a/models/repo/repo_unit.go +++ b/models/repo/repo_unit.go @@ -303,9 +303,6 @@ type ActionsConfig struct { CollaborativeOwnerIDs []int64 // TokenPermissionMode defines the default permission mode (permissive, restricted, or custom) TokenPermissionMode ActionsTokenPermissionMode `json:"token_permission_mode,omitempty"` - // DefaultTokenPermissions defines the specific permissions for workflow tokens when TokenPermissionMode is set to "custom" - // and no "permissions" keyword is defined in the workflow YAML. - DefaultTokenPermissions *ActionsTokenPermissions `json:"default_token_permissions,omitempty"` // MaxTokenPermissions defines the absolute maximum permissions any token can have in this context. // Workflow YAML "permissions" keywords can reduce permissions but never exceed this ceiling. MaxTokenPermissions *ActionsTokenPermissions `json:"max_token_permissions,omitempty"` @@ -366,9 +363,8 @@ func (cfg *ActionsConfig) GetEffectiveTokenPermissions(isForkPullRequest bool) A return ForkPullRequestPermissions() } - // Use custom default permissions if set - if cfg.DefaultTokenPermissions != nil { - return *cfg.DefaultTokenPermissions + if cfg.GetTokenPermissionMode() == ActionsTokenPermissionModeCustom { + return cfg.GetMaxTokenPermissions() } // Otherwise use mode-based defaults diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index 39e2a729bd..852154de17 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -3731,6 +3731,7 @@ "git.filemode.executable_file": "Executable", "git.filemode.symbolic_link": "Symlink", "git.filemode.submodule": "Submodule", + "org.repos.none": "No repositories.", "actions.general.token_permissions.title": "Action Token Permissions", "actions.general.token_permissions.desc": "Configure the default permissions for the GITEA_TOKEN running in this repository.", "actions.general.token_permissions.mode": "Default Token Permissions", @@ -3744,7 +3745,7 @@ "actions.general.token_permissions.access_read": "Read", "actions.general.token_permissions.access_write": "Write", "actions.general.token_permissions.code": "Code", - "actions.general.token_permissions.code.description": "Repository contents, commits, branches, downloads, releases, and merges.", + "actions.general.token_permissions.code.description": "Repository contents, commits, branches, downloads, and merges.", "actions.general.token_permissions.issues": "Issues", "actions.general.token_permissions.issues.description": "Issues and related comments, assignees, labels, and milestones.", "actions.general.token_permissions.pull_requests": "Pull Requests", diff --git a/templates/org/settings/actions_general.tmpl b/templates/org/settings/actions_general.tmpl index 0e38c0bdfb..c1d537d29f 100644 --- a/templates/org/settings/actions_general.tmpl +++ b/templates/org/settings/actions_general.tmpl @@ -52,10 +52,7 @@
-
- {{$.CsrfTokenHtml}} - -
+
{{else}} @@ -69,15 +66,14 @@
{{ctx.Locale.Tr "actions.general.token_permissions.add_repo"}}
-
- {{.CsrfTokenHtml}} +
- - + +
diff --git a/tests/integration/actions_job_token_test.go b/tests/integration/actions_job_token_test.go index e862b04c4d..cd3a7ef6d2 100644 --- a/tests/integration/actions_job_token_test.go +++ b/tests/integration/actions_job_token_test.go @@ -177,7 +177,6 @@ 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.DefaultTokenPermissions = nil // Ensure no custom permissions override the mode actionsCfg.MaxTokenPermissions = nil // Ensure no max permissions interfere // Update the config actionsUnit.Config = actionsCfg @@ -265,51 +264,120 @@ func testActionsTokenPermissionsMode(u *url.URL, mode string, expectReadOnly boo func TestActionsTokenPermissionsClamping(t *testing.T) { onGiteaRun(t, func(t *testing.T, u *url.URL) { - httpContext := NewAPITestContext(t, "user2", "repo-clamping", auth_model.AccessTokenScopeWriteUser, auth_model.AccessTokenScopeWriteRepository) - t.Run("Create Repository", doAPICreateRepository(httpContext, false, func(t *testing.T, repository structs.Repository) { - // Enable Actions unit with Clamping Config - err := db.Insert(t.Context(), &repo_model.RepoUnit{ - RepoID: repository.ID, - Type: unit_model.TypeActions, - Config: &repo_model.ActionsConfig{ - TokenPermissionMode: repo_model.ActionsTokenPermissionModePermissive, - MaxTokenPermissions: &repo_model.ActionsTokenPermissions{ - Code: perm.AccessModeRead, // Max is Read - will clamp default Write to Read - }, - }, - }) - require.NoError(t, err) + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + session := loginUser(t, user2.Name) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) - // Create Task and Token - task := &actions_model.ActionTask{ - RepoID: repository.ID, - Status: actions_model.StatusRunning, - IsForkPullRequest: false, - } - require.NoError(t, task.GenerateToken()) - require.NoError(t, db.Insert(t.Context(), task)) + // Create Repo + apiRepo := createActionsTestRepo(t, token, "repo-clamping", false) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiRepo.ID}) + httpContext := NewAPITestContext(t, user2.Name, repo.Name, auth_model.AccessTokenScopeWriteRepository) + defer doAPIDeleteRepository(httpContext)(t) - // Verify Token Permissions - session := emptyTestSession(t) - testCtx := APITestContext{ - Session: session, - Token: task.Token, - Username: "user2", - Reponame: "repo-clamping", - } + // Mock Runner + runner := newMockRunner() + runner.registerAsRepoRunner(t, repo.OwnerName, repo.Name, "mock-runner", []string{"ubuntu-latest"}, false) - // 1. Try to Write (Create File) - Should Fail (403) because Max is Read - testCtx.ExpectedCode = http.StatusForbidden - t.Run("Fail to Create File (Max Clamping)", doAPICreateFile(testCtx, "clamping.txt", &structs.CreateFileOptions{ - ContentBase64: base64.StdEncoding.EncodeToString([]byte("test")), - })) + // Set Clamping Config: Custom Mode (Default=Max), Max Code = Read + req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/actions/general/token_permissions", repo.OwnerName, repo.Name), map[string]string{ + "token_permission_mode": "custom", + "max_code": "read", + }) + session.MakeRequest(t, req, http.StatusSeeOther) - // 2. Try to Read (Get Repository) - Should Succeed (200) - testCtx.ExpectedCode = http.StatusOK - t.Run("Get Repository (Read Allowed)", doAPIGetRepository(testCtx, func(t *testing.T, r structs.Repository) { - assert.Equal(t, "repo-clamping", r.Name) - })) + // Create workflow requesting Write + wfTreePath := ".gitea/workflows/clamping.yml" + wfFileContent := `name: Clamping +on: [push] +permissions: + contents: write +jobs: + job-clamping: + runs-on: ubuntu-latest + steps: + - run: echo test +` + opts := getWorkflowCreateFileOptions(user2, repo.DefaultBranch, "create "+wfTreePath, wfFileContent) + createWorkflowFile(t, token, user2.Name, repo.Name, wfTreePath, opts) + + // Fetch task + runnerTask := runner.fetchTask(t) + taskToken := runnerTask.Secrets["GITEA_TOKEN"] + require.NotEmpty(t, taskToken) + + // Verify Permissions + testCtx := APITestContext{ + Session: emptyTestSession(t), + Token: taskToken, + Username: user2.Name, + Reponame: repo.Name, + } + + // 1. Try to Write (Create File) - Should Fail (403) because Max is Read + testCtx.ExpectedCode = http.StatusForbidden + t.Run("Fail to Create File (Max Clamping)", doAPICreateFile(testCtx, "clamping.txt", &structs.CreateFileOptions{ + ContentBase64: base64.StdEncoding.EncodeToString([]byte("test")), })) + + // 2. Try to Read (Get Repository) - Should Succeed (200) + testCtx.ExpectedCode = http.StatusOK + t.Run("Get Repository (Read Allowed)", doAPIGetRepository(testCtx, func(t *testing.T, r structs.Repository) { + assert.Equal(t, repo.Name, r.Name) + })) + }) +} + +func TestActionsTokenPackagePermission(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + session := loginUser(t, user2.Name) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + + // Create Repo + apiRepo := createActionsTestRepo(t, token, "repo-package-perm", false) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiRepo.ID}) + httpContext := NewAPITestContext(t, user2.Name, repo.Name, auth_model.AccessTokenScopeWriteRepository) + defer doAPIDeleteRepository(httpContext)(t) + + // Mock Runner + 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) + req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/actions/general/token_permissions", repo.OwnerName, repo.Name), map[string]string{ + "token_permission_mode": "custom", + "max_packages": "write", + }) + session.MakeRequest(t, req, http.StatusSeeOther) + + // Create workflow with NO explicit permissions (inherits Default) + wfTreePath := ".gitea/workflows/package.yml" + wfFileContent := `name: Package +on: [push] +jobs: + job-package: + runs-on: ubuntu-latest + steps: + - run: echo test +` + opts := getWorkflowCreateFileOptions(user2, repo.DefaultBranch, "create "+wfTreePath, wfFileContent) + createWorkflowFile(t, token, user2.Name, repo.Name, wfTreePath, opts) + + // Fetch task + runnerTask := runner.fetchTask(t) + taskToken := runnerTask.Secrets["GITEA_TOKEN"] + require.NotEmpty(t, taskToken) + + // Verify Package Upload Access + packageName := "test-pkg" + packageVersion := "1.0.0" + 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) }) }