Files
Gitea/tests/integration/api_issue_dependency_test.go
0964106988 fix: add missing access checks and size limits (#39942) (#40085)
Backport https://github.com/go-gitea/gitea/pull/39942

Several handlers and parsers skipped checks that their siblings already
perform.

- Reject issue, comment, label, milestone and release mutations in
archived repositories via the API with one guard per route group,
matching the web UI
- Hide timeline references and dependencies pointing at issues the doer
cannot read, in the web UI and the API, including pull requests of the
same repository without pull request access
- Reject workflow runs with more than `MaxJobNumPerRun` jobs, including
inline reusable workflow jobs, and show rejected pushes as invalid
workflow runs
- Bound how much of a Helm chart archive is inflated while looking for
`Chart.yaml`

`release/v28` lacks two helpers this needs, so they are included:
`insertInvalidWorkflowRun`, split out of `handleInvalidWorkflows` by
https://github.com/go-gitea/gitea/pull/39480, and
`packages.NewLimitedReader` from
https://github.com/go-gitea/gitea/pull/39366.

Co-authored-by: bircni <bircni@icloud.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-10-30 08:08:32 +01:00

222 lines
9.6 KiB
Go

// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"fmt"
"net/http"
"slices"
"strconv"
"testing"
auth_model "gitea.dev/models/auth"
"gitea.dev/models/db"
issues_model "gitea.dev/models/issues"
"gitea.dev/models/perm"
access_model "gitea.dev/models/perm/access"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unit"
"gitea.dev/models/unittest"
user_model "gitea.dev/models/user"
api "gitea.dev/modules/structs"
"gitea.dev/modules/test"
repo_service "gitea.dev/services/repository"
"gitea.dev/tests"
"github.com/stretchr/testify/assert"
)
func enableRepoDependencies(t *testing.T, repoID int64) {
t.Helper()
repoUnit := unittest.AssertExistsAndLoadBean(t, &repo_model.RepoUnit{RepoID: repoID, Type: unit.TypeIssues})
repoUnit.IssuesConfig().EnableDependencies = true
assert.NoError(t, repo_model.UpdateRepoUnitConfig(t.Context(), repoUnit))
}
func TestAPICreateIssueDependencyCrossRepoPermission(t *testing.T) {
defer tests.PrepareTestEnv(t)()
targetRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
targetIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: targetRepo.ID, Index: 1})
dependencyRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
assert.True(t, dependencyRepo.IsPrivate)
dependencyIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: dependencyRepo.ID, Index: 1})
enableRepoDependencies(t, targetIssue.RepoID)
enableRepoDependencies(t, dependencyRepo.ID)
// remove user 40 access from target repository
_, err := db.DeleteByID[access_model.Access](t.Context(), 30)
assert.NoError(t, err)
url := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/dependencies", "user2", "repo1", targetIssue.Index)
dependencyMeta := &api.IssueMeta{
Owner: "org3",
Name: "repo3",
Index: dependencyIssue.Index,
}
user40 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 40})
// user40 has no access to both target issue and dependency issue
writerToken := getUserToken(t, "user40", auth_model.AccessTokenScopeWriteIssue)
req := NewRequestWithJSON(t, "POST", url, dependencyMeta).
AddTokenAuth(writerToken)
MakeRequest(t, req, http.StatusNotFound)
unittest.AssertNotExistsBean(t, &issues_model.IssueDependency{
IssueID: targetIssue.ID,
DependencyID: dependencyIssue.ID,
})
// add user40 as a collaborator to dependency repository with read permission
assert.NoError(t, repo_service.AddOrUpdateCollaborator(t.Context(), dependencyRepo, user40, perm.AccessModeRead))
// try again after getting read permission to dependency repository
req = NewRequestWithJSON(t, "POST", url, dependencyMeta).
AddTokenAuth(writerToken)
MakeRequest(t, req, http.StatusNotFound)
unittest.AssertNotExistsBean(t, &issues_model.IssueDependency{
IssueID: targetIssue.ID,
DependencyID: dependencyIssue.ID,
})
// add user40 as a collaborator to target repository with write permission
assert.NoError(t, repo_service.AddOrUpdateCollaborator(t.Context(), targetRepo, user40, perm.AccessModeWrite))
req = NewRequestWithJSON(t, "POST", url, dependencyMeta).
AddTokenAuth(writerToken)
MakeRequest(t, req, http.StatusCreated)
unittest.AssertExistsAndLoadBean(t, &issues_model.IssueDependency{
IssueID: targetIssue.ID,
DependencyID: dependencyIssue.ID,
})
}
func TestAPIDeleteIssueDependencyCrossRepoPermission(t *testing.T) {
defer tests.PrepareTestEnv(t)()
targetRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
targetIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: targetRepo.ID, Index: 1})
dependencyRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
assert.True(t, dependencyRepo.IsPrivate)
dependencyIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: dependencyRepo.ID, Index: 1})
enableRepoDependencies(t, targetIssue.RepoID)
enableRepoDependencies(t, dependencyRepo.ID)
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
assert.NoError(t, issues_model.CreateIssueDependency(t.Context(), user1, targetIssue, dependencyIssue))
// remove user 40 access from target repository
_, err := db.DeleteByID[access_model.Access](t.Context(), 30)
assert.NoError(t, err)
url := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/dependencies", "user2", "repo1", targetIssue.Index)
dependencyMeta := &api.IssueMeta{
Owner: "org3",
Name: "repo3",
Index: dependencyIssue.Index,
}
user40 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 40})
// user40 has no access to both target issue and dependency issue
writerToken := getUserToken(t, "user40", auth_model.AccessTokenScopeWriteIssue)
req := NewRequestWithJSON(t, "DELETE", url, dependencyMeta).
AddTokenAuth(writerToken)
MakeRequest(t, req, http.StatusNotFound)
unittest.AssertExistsAndLoadBean(t, &issues_model.IssueDependency{
IssueID: targetIssue.ID,
DependencyID: dependencyIssue.ID,
})
// add user40 as a collaborator to dependency repository with read permission
assert.NoError(t, repo_service.AddOrUpdateCollaborator(t.Context(), dependencyRepo, user40, perm.AccessModeRead))
// try again after getting read permission to dependency repository
req = NewRequestWithJSON(t, "DELETE", url, dependencyMeta).
AddTokenAuth(writerToken)
MakeRequest(t, req, http.StatusNotFound)
unittest.AssertExistsAndLoadBean(t, &issues_model.IssueDependency{
IssueID: targetIssue.ID,
DependencyID: dependencyIssue.ID,
})
// add user40 as a collaborator to target repository with write permission
assert.NoError(t, repo_service.AddOrUpdateCollaborator(t.Context(), targetRepo, user40, perm.AccessModeWrite))
req = NewRequestWithJSON(t, "DELETE", url, dependencyMeta).
AddTokenAuth(writerToken)
MakeRequest(t, req, http.StatusCreated)
unittest.AssertNotExistsBean(t, &issues_model.IssueDependency{
IssueID: targetIssue.ID,
DependencyID: dependencyIssue.ID,
})
}
func TestWebDeleteIssueDependencyCrossRepoPermission(t *testing.T) {
defer tests.PrepareTestEnv(t)()
targetRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
targetIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: targetRepo.ID, Index: 1})
dependencyRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
assert.True(t, dependencyRepo.IsPrivate)
dependencyIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: dependencyRepo.ID, Index: 1})
enableRepoDependencies(t, targetIssue.RepoID)
enableRepoDependencies(t, dependencyRepo.ID)
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
assert.NoError(t, issues_model.CreateIssueDependency(t.Context(), user1, targetIssue, dependencyIssue))
user40 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 40})
assert.NoError(t, repo_service.AddOrUpdateCollaborator(t.Context(), targetRepo, user40, perm.AccessModeWrite))
session := loginUser(t, user40.Name)
req := NewRequestWithValues(t, "POST", fmt.Sprintf("/user2/repo1/issues/%d/dependency/delete", targetIssue.Index), map[string]string{
"removeDependencyID": strconv.FormatInt(dependencyIssue.ID, 10),
"dependencyType": "blockedBy",
})
resp := session.MakeRequest(t, req, http.StatusBadRequest)
assert.Equal(t, "Permission denied.", test.ParseJSONError(resp.Body.Bytes()).ErrorMessage)
unittest.AssertExistsAndLoadBean(t, &issues_model.IssueDependency{IssueID: targetIssue.ID, DependencyID: dependencyIssue.ID})
assert.NoError(t, repo_service.AddOrUpdateCollaborator(t.Context(), dependencyRepo, user40, perm.AccessModeRead))
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/user2/repo1/issues/%d/dependency/delete", targetIssue.Index), map[string]string{
"removeDependencyID": strconv.FormatInt(dependencyIssue.ID, 10),
"dependencyType": "blockedBy",
})
session.MakeRequest(t, req, http.StatusOK)
unittest.AssertNotExistsBean(t, &issues_model.IssueDependency{IssueID: targetIssue.ID, DependencyID: dependencyIssue.ID})
}
func TestIssueTimelineDependencyCrossRepoPermission(t *testing.T) {
defer tests.PrepareTestEnv(t)()
targetIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: 1, Index: 1})
dependencyRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
assert.True(t, dependencyRepo.IsPrivate)
dependencyIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: dependencyRepo.ID, Index: 1})
enableRepoDependencies(t, targetIssue.RepoID)
enableRepoDependencies(t, dependencyRepo.ID)
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
assert.NoError(t, issues_model.CreateIssueDependency(t.Context(), user1, targetIssue, dependencyIssue))
session := loginUser(t, "user40")
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadIssue)
assertDependencyVisible := func(visible bool) {
t.Helper()
req := NewRequest(t, "GET", "/api/v1/repos/user2/repo1/issues/1/timeline").AddTokenAuth(token)
comments := DecodeJSON(t, MakeRequest(t, req, http.StatusOK), []*api.TimelineComment{})
assert.Equal(t, visible, slices.ContainsFunc(comments, func(c *api.TimelineComment) bool { return c.Type == "add_dependency" }))
resp := session.MakeRequest(t, NewRequest(t, "GET", "/user2/repo1/issues/1"), http.StatusOK)
assert.Equal(t, visible, NewHTMLParser(t, resp.Body).Find(`.timeline-item a[href$="/org3/repo3/issues/1"]`).Length() > 0)
}
assertDependencyVisible(false)
user40 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 40})
assert.NoError(t, repo_service.AddOrUpdateCollaborator(t.Context(), dependencyRepo, user40, perm.AccessModeRead))
assertDependencyVisible(true)
}