Files
Shivansh Gargandwxiaoguang 640cd615c4 feat: restrict pull/issue creation on access basis (#40069)
Adds a repository-level **"Restrict to collaborators"** option under the
Issues and Pull Request unit settings. When enabled, only users with
collaborator access to the base repository (owner, team members, or
explicit collaborators) can open issues or pull requests against it.

Public repositories often need to be visible/cloneable by everyone, but
owners may want to limit who can open pull requests (e.g. restrict to an
internal team while still keeping the code public)

* Fixes: https://github.com/go-gitea/gitea/issues/40015
* Fixes: https://github.com/go-gitea/gitea/issues/31890
* Fixes: https://github.com/go-gitea/gitea/issues/28021
* Fixes: https://github.com/go-gitea/gitea/issues/14801

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-11-10 00:53:49 +00:00

165 lines
5.5 KiB
Go

// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package issue
import (
"testing"
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"
"gitea.dev/modules/util"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCheckCreationRestriction(t *testing.T) {
unittest.PrepareTestEnv(t)
for _, tc := range []struct {
name string
repoID int64
userID int64
member bool
}{
{"owner", 1, 2, true},
{"site admin", 1, 1, true},
{"team member", 3, 4, true},
{"collaborator", 4, 4, true},
{"public reader", 1, 4, false},
} {
t.Run(tc.name, func(t *testing.T) {
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: tc.repoID})
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: tc.userID})
permission, err := access_model.GetDoerRepoPermission(t.Context(), repo, user)
require.NoError(t, err)
for _, unitType := range []unit.Type{unit.TypeIssues, unit.TypePullRequests} {
require.NoError(t, CheckCreationRestriction(permission, unitType, repo_model.CreationRestrictionReader))
for _, restriction := range []string{repo_model.CreationRestrictionMember, repo_model.CreationRestrictionWriter} {
err := CheckCreationRestriction(permission, unitType, restriction)
if tc.member {
assert.NoError(t, err)
} else {
assert.ErrorIs(t, err, util.ErrPermissionDenied)
}
}
}
})
}
}
func TestCanSelfChangeOrAccessContent(t *testing.T) {
doer := &user_model.User{ID: 1}
for _, u := range []struct {
name string
unitType unit.Type
}{
{"issues", unit.TypeIssues},
{"pulls", unit.TypePullRequests},
} {
t.Run(u.name, func(t *testing.T) {
for _, tc := range []struct {
name string
doer *user_model.User
posterID int64
mode perm.AccessMode
requiredMode perm.AccessMode
locked bool
allowed bool
}{
{"anonymous", nil, 1, perm.AccessModeWrite, perm.AccessModeWrite, false, false},
{"author without read access", doer, 1, perm.AccessModeNone, perm.AccessModeWrite, false, false},
{"author with read access", doer, 1, perm.AccessModeRead, perm.AccessModeWrite, false, true},
{"reader cannot edit others", doer, 2, perm.AccessModeRead, perm.AccessModeWrite, false, false},
{"reader can access others", doer, 2, perm.AccessModeRead, perm.AccessModeRead, false, true},
{"writer can edit others", doer, 2, perm.AccessModeWrite, perm.AccessModeWrite, false, true},
{"locked author needs write access", doer, 1, perm.AccessModeRead, perm.AccessModeWrite, true, false},
{"locked reader needs write access", doer, 2, perm.AccessModeRead, perm.AccessModeRead, true, false},
{"locked writer can edit", doer, 2, perm.AccessModeWrite, perm.AccessModeWrite, true, true},
} {
t.Run(tc.name, func(t *testing.T) {
var permission access_model.Permission
permission.SetUnitsWithDefaultAccessMode([]*repo_model.RepoUnit{{Type: u.unitType}}, tc.mode)
issue := &issues_model.Issue{IsPull: u.unitType == unit.TypePullRequests, IsLocked: tc.locked}
assert.Equal(t, tc.allowed, CanSelfChangeOrAccessContent(tc.doer, permission, issue, tc.posterID, tc.requiredMode))
})
}
})
}
}
func TestGetRefEndNamesAndURLs(t *testing.T) {
issues := []*issues_model.Issue{
{ID: 1, Ref: "refs/heads/branch1"},
{ID: 2, Ref: "refs/tags/tag1"},
{ID: 3, Ref: "c0ffee"},
}
repoLink := "/foo/bar"
endNames, urls := GetRefEndNamesAndURLs(issues, repoLink)
assert.Equal(t, map[int64]string{1: "branch1", 2: "tag1", 3: "c0ffee"}, endNames)
assert.Equal(t, map[int64]string{
1: repoLink + "/src/branch/branch1",
2: repoLink + "/src/tag/tag1",
3: repoLink + "/src/commit/c0ffee",
}, urls)
}
func TestIssue_DeleteIssue(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
issueIDs, err := issues_model.GetIssueIDsByRepoID(t.Context(), 1)
assert.NoError(t, err)
assert.Len(t, issueIDs, 5)
issue := &issues_model.Issue{
RepoID: 1,
ID: issueIDs[2],
}
_, err = deleteIssue(t.Context(), issue)
assert.NoError(t, err)
issueIDs, err = issues_model.GetIssueIDsByRepoID(t.Context(), 1)
assert.NoError(t, err)
assert.Len(t, issueIDs, 4)
// check attachment removal
attachments, err := repo_model.GetAttachmentsByIssueID(t.Context(), 4)
assert.NoError(t, err)
issue, err = issues_model.GetIssueByID(t.Context(), 4)
assert.NoError(t, err)
_, err = deleteIssue(t.Context(), issue)
assert.NoError(t, err)
assert.Len(t, attachments, 2)
for i := range attachments {
attachment, err := repo_model.GetAttachmentByUUID(t.Context(), attachments[i].UUID)
assert.Error(t, err)
assert.True(t, repo_model.IsErrAttachmentNotExist(err))
assert.Nil(t, attachment)
}
// check issue dependencies
user, err := user_model.GetUserByID(t.Context(), 1)
assert.NoError(t, err)
issue1, err := issues_model.GetIssueByID(t.Context(), 1)
assert.NoError(t, err)
issue2, err := issues_model.GetIssueByID(t.Context(), 2)
assert.NoError(t, err)
err = issues_model.CreateIssueDependency(t.Context(), user, issue1, issue2)
assert.NoError(t, err)
left, err := issues_model.IssueNoDependenciesLeft(t.Context(), issue1)
assert.NoError(t, err)
assert.False(t, left)
_, err = deleteIssue(t.Context(), issue2)
assert.NoError(t, err)
left, err = issues_model.IssueNoDependenciesLeft(t.Context(), issue1)
assert.NoError(t, err)
assert.True(t, left)
}