1
1
mirror of https://github.com/go-gitea/gitea synced 2025-07-22 18:28:37 +00:00

Add user blocking (#29028)

Fixes #17453

This PR adds the abbility to block a user from a personal account or
organization to restrict how the blocked user can interact with the
blocker. The docs explain what's the consequence of blocking a user.

Screenshots:


![grafik](https://github.com/go-gitea/gitea/assets/1666336/4ed884f3-e06a-4862-afd3-3b8aa2488dc6)


![grafik](https://github.com/go-gitea/gitea/assets/1666336/ae6d4981-f252-4f50-a429-04f0f9f1cdf1)


![grafik](https://github.com/go-gitea/gitea/assets/1666336/ca153599-5b0f-4b4a-90fe-18bdfd6f0b6b)

---------

Co-authored-by: Lauris BH <lauris@nix.lv>
This commit is contained in:
KN4CK3R
2024-03-04 09:16:03 +01:00
committed by GitHub
parent 8e12ba34ba
commit c337ff0ec7
109 changed files with 2878 additions and 548 deletions

View File

@@ -36,14 +36,44 @@ type Collaborator struct {
Collaboration *Collaboration
}
type FindCollaborationOptions struct {
db.ListOptions
RepoID int64
RepoOwnerID int64
CollaboratorID int64
}
func (opts *FindCollaborationOptions) ToConds() builder.Cond {
cond := builder.NewCond()
if opts.RepoID != 0 {
cond = cond.And(builder.Eq{"collaboration.repo_id": opts.RepoID})
}
if opts.RepoOwnerID != 0 {
cond = cond.And(builder.Eq{"repository.owner_id": opts.RepoOwnerID})
}
if opts.CollaboratorID != 0 {
cond = cond.And(builder.Eq{"collaboration.user_id": opts.CollaboratorID})
}
return cond
}
func (opts *FindCollaborationOptions) ToJoins() []db.JoinFunc {
if opts.RepoOwnerID != 0 {
return []db.JoinFunc{
func(e db.Engine) error {
e.Join("INNER", "repository", "repository.id = collaboration.repo_id")
return nil
},
}
}
return nil
}
// GetCollaborators returns the collaborators for a repository
func GetCollaborators(ctx context.Context, repoID int64, listOptions db.ListOptions) ([]*Collaborator, error) {
collaborations, err := db.Find[Collaboration](ctx, FindCollaborationOptions{
ListOptions: listOptions,
RepoID: repoID,
})
func GetCollaborators(ctx context.Context, opts *FindCollaborationOptions) ([]*Collaborator, int64, error) {
collaborations, total, err := db.FindAndCount[Collaboration](ctx, opts)
if err != nil {
return nil, fmt.Errorf("db.Find[Collaboration]: %w", err)
return nil, 0, fmt.Errorf("db.FindAndCount[Collaboration]: %w", err)
}
collaborators := make([]*Collaborator, 0, len(collaborations))
@@ -54,7 +84,7 @@ func GetCollaborators(ctx context.Context, repoID int64, listOptions db.ListOpti
usersMap := make(map[int64]*user_model.User)
if err := db.GetEngine(ctx).In("id", userIDs).Find(&usersMap); err != nil {
return nil, fmt.Errorf("Find users map by user ids: %w", err)
return nil, 0, fmt.Errorf("Find users map by user ids: %w", err)
}
for _, c := range collaborations {
@@ -67,7 +97,7 @@ func GetCollaborators(ctx context.Context, repoID int64, listOptions db.ListOpti
Collaboration: c,
})
}
return collaborators, nil
return collaborators, total, nil
}
// GetCollaboration get collaboration for a repository id with a user id
@@ -88,15 +118,6 @@ func IsCollaborator(ctx context.Context, repoID, userID int64) (bool, error) {
return db.GetEngine(ctx).Get(&Collaboration{RepoID: repoID, UserID: userID})
}
type FindCollaborationOptions struct {
db.ListOptions
RepoID int64
}
func (opts FindCollaborationOptions) ToConds() builder.Cond {
return builder.And(builder.Eq{"repo_id": opts.RepoID})
}
// ChangeCollaborationAccessMode sets new access mode for the collaboration.
func ChangeCollaborationAccessMode(ctx context.Context, repo *Repository, uid int64, mode perm.AccessMode) error {
// Discard invalid input

View File

@@ -19,7 +19,7 @@ func TestRepository_GetCollaborators(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
test := func(repoID int64) {
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID})
collaborators, err := repo_model.GetCollaborators(db.DefaultContext, repo.ID, db.ListOptions{})
collaborators, _, err := repo_model.GetCollaborators(db.DefaultContext, &repo_model.FindCollaborationOptions{RepoID: repo.ID})
assert.NoError(t, err)
expectedLen, err := db.GetEngine(db.DefaultContext).Count(&repo_model.Collaboration{RepoID: repoID})
assert.NoError(t, err)
@@ -37,11 +37,17 @@ func TestRepository_GetCollaborators(t *testing.T) {
// Test db.ListOptions
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 22})
collaborators1, err := repo_model.GetCollaborators(db.DefaultContext, repo.ID, db.ListOptions{PageSize: 1, Page: 1})
collaborators1, _, err := repo_model.GetCollaborators(db.DefaultContext, &repo_model.FindCollaborationOptions{
ListOptions: db.ListOptions{PageSize: 1, Page: 1},
RepoID: repo.ID,
})
assert.NoError(t, err)
assert.Len(t, collaborators1, 1)
collaborators2, err := repo_model.GetCollaborators(db.DefaultContext, repo.ID, db.ListOptions{PageSize: 1, Page: 2})
collaborators2, _, err := repo_model.GetCollaborators(db.DefaultContext, &repo_model.FindCollaborationOptions{
ListOptions: db.ListOptions{PageSize: 1, Page: 2},
RepoID: repo.ID,
})
assert.NoError(t, err)
assert.Len(t, collaborators2, 1)
@@ -85,31 +91,6 @@ func TestRepository_ChangeCollaborationAccessMode(t *testing.T) {
unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repo.ID})
}
func TestRepository_CountCollaborators(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
count, err := db.Count[repo_model.Collaboration](db.DefaultContext, repo_model.FindCollaborationOptions{
RepoID: repo1.ID,
})
assert.NoError(t, err)
assert.EqualValues(t, 2, count)
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 22})
count, err = db.Count[repo_model.Collaboration](db.DefaultContext, repo_model.FindCollaborationOptions{
RepoID: repo2.ID,
})
assert.NoError(t, err)
assert.EqualValues(t, 2, count)
// Non-existent repository.
count, err = db.Count[repo_model.Collaboration](db.DefaultContext, repo_model.FindCollaborationOptions{
RepoID: unittest.NonexistentID,
})
assert.NoError(t, err)
assert.EqualValues(t, 0, count)
}
func TestRepository_IsOwnerMemberCollaborator(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())

View File

@@ -64,16 +64,17 @@ func TestRepoAPIURL(t *testing.T) {
func TestWatchRepo(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
const repoID = 3
const userID = 2
assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, userID, repoID, true))
unittest.AssertExistsAndLoadBean(t, &repo_model.Watch{RepoID: repoID, UserID: userID})
unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repoID})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, userID, repoID, false))
unittest.AssertNotExistsBean(t, &repo_model.Watch{RepoID: repoID, UserID: userID})
unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repoID})
assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, user, repo, true))
unittest.AssertExistsAndLoadBean(t, &repo_model.Watch{RepoID: repo.ID, UserID: user.ID})
unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repo.ID})
assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, user, repo, false))
unittest.AssertNotExistsBean(t, &repo_model.Watch{RepoID: repo.ID, UserID: user.ID})
unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repo.ID})
}
func TestMetas(t *testing.T) {

View File

@@ -24,26 +24,30 @@ func init() {
}
// StarRepo or unstar repository.
func StarRepo(ctx context.Context, userID, repoID int64, star bool) error {
func StarRepo(ctx context.Context, doer *user_model.User, repo *Repository, star bool) error {
ctx, committer, err := db.TxContext(ctx)
if err != nil {
return err
}
defer committer.Close()
staring := IsStaring(ctx, userID, repoID)
staring := IsStaring(ctx, doer.ID, repo.ID)
if star {
if user_model.IsUserBlockedBy(ctx, doer, repo.OwnerID) {
return user_model.ErrBlockedUser
}
if staring {
return nil
}
if err := db.Insert(ctx, &Star{UID: userID, RepoID: repoID}); err != nil {
if err := db.Insert(ctx, &Star{UID: doer.ID, RepoID: repo.ID}); err != nil {
return err
}
if _, err := db.Exec(ctx, "UPDATE `repository` SET num_stars = num_stars + 1 WHERE id = ?", repoID); err != nil {
if _, err := db.Exec(ctx, "UPDATE `repository` SET num_stars = num_stars + 1 WHERE id = ?", repo.ID); err != nil {
return err
}
if _, err := db.Exec(ctx, "UPDATE `user` SET num_stars = num_stars + 1 WHERE id = ?", userID); err != nil {
if _, err := db.Exec(ctx, "UPDATE `user` SET num_stars = num_stars + 1 WHERE id = ?", doer.ID); err != nil {
return err
}
} else {
@@ -51,13 +55,13 @@ func StarRepo(ctx context.Context, userID, repoID int64, star bool) error {
return nil
}
if _, err := db.DeleteByBean(ctx, &Star{UID: userID, RepoID: repoID}); err != nil {
if _, err := db.DeleteByBean(ctx, &Star{UID: doer.ID, RepoID: repo.ID}); err != nil {
return err
}
if _, err := db.Exec(ctx, "UPDATE `repository` SET num_stars = num_stars - 1 WHERE id = ?", repoID); err != nil {
if _, err := db.Exec(ctx, "UPDATE `repository` SET num_stars = num_stars - 1 WHERE id = ?", repo.ID); err != nil {
return err
}
if _, err := db.Exec(ctx, "UPDATE `user` SET num_stars = num_stars - 1 WHERE id = ?", userID); err != nil {
if _, err := db.Exec(ctx, "UPDATE `user` SET num_stars = num_stars - 1 WHERE id = ?", doer.ID); err != nil {
return err
}
}

View File

@@ -9,21 +9,24 @@ import (
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"github.com/stretchr/testify/assert"
)
func TestStarRepo(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
const userID = 2
const repoID = 1
unittest.AssertNotExistsBean(t, &repo_model.Star{UID: userID, RepoID: repoID})
assert.NoError(t, repo_model.StarRepo(db.DefaultContext, userID, repoID, true))
unittest.AssertExistsAndLoadBean(t, &repo_model.Star{UID: userID, RepoID: repoID})
assert.NoError(t, repo_model.StarRepo(db.DefaultContext, userID, repoID, true))
unittest.AssertExistsAndLoadBean(t, &repo_model.Star{UID: userID, RepoID: repoID})
assert.NoError(t, repo_model.StarRepo(db.DefaultContext, userID, repoID, false))
unittest.AssertNotExistsBean(t, &repo_model.Star{UID: userID, RepoID: repoID})
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
unittest.AssertNotExistsBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
assert.NoError(t, repo_model.StarRepo(db.DefaultContext, user, repo, true))
unittest.AssertExistsAndLoadBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
assert.NoError(t, repo_model.StarRepo(db.DefaultContext, user, repo, true))
unittest.AssertExistsAndLoadBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
assert.NoError(t, repo_model.StarRepo(db.DefaultContext, user, repo, false))
unittest.AssertNotExistsBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
}
func TestIsStaring(t *testing.T) {
@@ -54,17 +57,18 @@ func TestRepository_GetStargazers2(t *testing.T) {
func TestClearRepoStars(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
const userID = 2
const repoID = 1
unittest.AssertNotExistsBean(t, &repo_model.Star{UID: userID, RepoID: repoID})
assert.NoError(t, repo_model.StarRepo(db.DefaultContext, userID, repoID, true))
unittest.AssertExistsAndLoadBean(t, &repo_model.Star{UID: userID, RepoID: repoID})
assert.NoError(t, repo_model.StarRepo(db.DefaultContext, userID, repoID, false))
unittest.AssertNotExistsBean(t, &repo_model.Star{UID: userID, RepoID: repoID})
assert.NoError(t, repo_model.ClearRepoStars(db.DefaultContext, repoID))
unittest.AssertNotExistsBean(t, &repo_model.Star{UID: userID, RepoID: repoID})
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
unittest.AssertNotExistsBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
assert.NoError(t, repo_model.StarRepo(db.DefaultContext, user, repo, true))
unittest.AssertExistsAndLoadBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
assert.NoError(t, repo_model.StarRepo(db.DefaultContext, user, repo, false))
unittest.AssertNotExistsBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
assert.NoError(t, repo_model.ClearRepoStars(db.DefaultContext, repo.ID))
unittest.AssertNotExistsBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
gazers, err := repo_model.GetStargazers(db.DefaultContext, repo, db.ListOptions{Page: 0})
assert.NoError(t, err)
assert.Len(t, gazers, 0)

View File

@@ -16,47 +16,82 @@ import (
"xorm.io/builder"
)
type StarredReposOptions struct {
db.ListOptions
StarrerID int64
RepoOwnerID int64
IncludePrivate bool
}
func (opts *StarredReposOptions) ToConds() builder.Cond {
var cond builder.Cond = builder.Eq{
"star.uid": opts.StarrerID,
}
if opts.RepoOwnerID != 0 {
cond = cond.And(builder.Eq{
"repository.owner_id": opts.RepoOwnerID,
})
}
if !opts.IncludePrivate {
cond = cond.And(builder.Eq{
"repository.is_private": false,
})
}
return cond
}
func (opts *StarredReposOptions) ToJoins() []db.JoinFunc {
return []db.JoinFunc{
func(e db.Engine) error {
e.Join("INNER", "star", "`repository`.id=`star`.repo_id")
return nil
},
}
}
// GetStarredRepos returns the repos starred by a particular user
func GetStarredRepos(ctx context.Context, userID int64, private bool, listOptions db.ListOptions) ([]*Repository, error) {
sess := db.GetEngine(ctx).
Where("star.uid=?", userID).
Join("LEFT", "star", "`repository`.id=`star`.repo_id")
if !private {
sess = sess.And("is_private=?", false)
func GetStarredRepos(ctx context.Context, opts *StarredReposOptions) ([]*Repository, error) {
return db.Find[Repository](ctx, opts)
}
type WatchedReposOptions struct {
db.ListOptions
WatcherID int64
RepoOwnerID int64
IncludePrivate bool
}
func (opts *WatchedReposOptions) ToConds() builder.Cond {
var cond builder.Cond = builder.Eq{
"watch.user_id": opts.WatcherID,
}
if listOptions.Page != 0 {
sess = db.SetSessionPagination(sess, &listOptions)
repos := make([]*Repository, 0, listOptions.PageSize)
return repos, sess.Find(&repos)
if opts.RepoOwnerID != 0 {
cond = cond.And(builder.Eq{
"repository.owner_id": opts.RepoOwnerID,
})
}
if !opts.IncludePrivate {
cond = cond.And(builder.Eq{
"repository.is_private": false,
})
}
return cond.And(builder.Neq{
"watch.mode": WatchModeDont,
})
}
repos := make([]*Repository, 0, 10)
return repos, sess.Find(&repos)
func (opts *WatchedReposOptions) ToJoins() []db.JoinFunc {
return []db.JoinFunc{
func(e db.Engine) error {
e.Join("INNER", "watch", "`repository`.id=`watch`.repo_id")
return nil
},
}
}
// GetWatchedRepos returns the repos watched by a particular user
func GetWatchedRepos(ctx context.Context, userID int64, private bool, listOptions db.ListOptions) ([]*Repository, int64, error) {
sess := db.GetEngine(ctx).
Where("watch.user_id=?", userID).
And("`watch`.mode<>?", WatchModeDont).
Join("LEFT", "watch", "`repository`.id=`watch`.repo_id")
if !private {
sess = sess.And("is_private=?", false)
}
if listOptions.Page != 0 {
sess = db.SetSessionPagination(sess, &listOptions)
repos := make([]*Repository, 0, listOptions.PageSize)
total, err := sess.FindAndCount(&repos)
return repos, total, err
}
repos := make([]*Repository, 0, 10)
total, err := sess.FindAndCount(&repos)
return repos, total, err
func GetWatchedRepos(ctx context.Context, opts *WatchedReposOptions) ([]*Repository, int64, error) {
return db.FindAndCount[Repository](ctx, opts)
}
// GetRepoAssignees returns all users that have write access and can be assigned to issues

View File

@@ -25,10 +25,8 @@ func TestRepoAssignees(t *testing.T) {
repo21 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 21})
users, err = repo_model.GetRepoAssignees(db.DefaultContext, repo21)
assert.NoError(t, err)
assert.Len(t, users, 3)
assert.Equal(t, users[0].ID, int64(15))
assert.Equal(t, users[1].ID, int64(18))
assert.Equal(t, users[2].ID, int64(16))
assert.Len(t, users, 4)
assert.ElementsMatch(t, []int64{10, 15, 16, 18}, []int64{users[0].ID, users[1].ID, users[2].ID, users[3].ID})
}
func TestRepoGetReviewers(t *testing.T) {

View File

@@ -104,29 +104,23 @@ func watchRepoMode(ctx context.Context, watch Watch, mode WatchMode) (err error)
return err
}
// WatchRepoMode watch repository in specific mode.
func WatchRepoMode(ctx context.Context, userID, repoID int64, mode WatchMode) (err error) {
var watch Watch
if watch, err = GetWatch(ctx, userID, repoID); err != nil {
return err
}
return watchRepoMode(ctx, watch, mode)
}
// WatchRepo watch or unwatch repository.
func WatchRepo(ctx context.Context, userID, repoID int64, doWatch bool) (err error) {
var watch Watch
if watch, err = GetWatch(ctx, userID, repoID); err != nil {
func WatchRepo(ctx context.Context, doer *user_model.User, repo *Repository, doWatch bool) error {
watch, err := GetWatch(ctx, doer.ID, repo.ID)
if err != nil {
return err
}
if !doWatch && watch.Mode == WatchModeAuto {
err = watchRepoMode(ctx, watch, WatchModeDont)
return watchRepoMode(ctx, watch, WatchModeDont)
} else if !doWatch {
err = watchRepoMode(ctx, watch, WatchModeNone)
} else {
err = watchRepoMode(ctx, watch, WatchModeNormal)
return watchRepoMode(ctx, watch, WatchModeNone)
}
return err
if user_model.IsUserBlockedBy(ctx, doer, repo.OwnerID) {
return user_model.ErrBlockedUser
}
return watchRepoMode(ctx, watch, WatchModeNormal)
}
// GetWatchers returns all watchers of given repository.

View File

@@ -9,6 +9,7 @@ import (
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
"github.com/stretchr/testify/assert"
@@ -64,6 +65,8 @@ func TestWatchIfAuto(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
user12 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 12})
watchers, err := repo_model.GetRepoWatchers(db.DefaultContext, repo.ID, db.ListOptions{Page: 1})
assert.NoError(t, err)
assert.Len(t, watchers, repo.NumWatches)
@@ -105,7 +108,7 @@ func TestWatchIfAuto(t *testing.T) {
assert.Len(t, watchers, prevCount+1)
// Should remove watch, inhibit from adding auto
assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, 12, 1, false))
assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, user12, repo, false))
watchers, err = repo_model.GetRepoWatchers(db.DefaultContext, repo.ID, db.ListOptions{Page: 1})
assert.NoError(t, err)
assert.Len(t, watchers, prevCount)
@@ -116,24 +119,3 @@ func TestWatchIfAuto(t *testing.T) {
assert.NoError(t, err)
assert.Len(t, watchers, prevCount)
}
func TestWatchRepoMode(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1}, 0)
assert.NoError(t, repo_model.WatchRepoMode(db.DefaultContext, 12, 1, repo_model.WatchModeAuto))
unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1}, 1)
unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1, Mode: repo_model.WatchModeAuto}, 1)
assert.NoError(t, repo_model.WatchRepoMode(db.DefaultContext, 12, 1, repo_model.WatchModeNormal))
unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1}, 1)
unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1, Mode: repo_model.WatchModeNormal}, 1)
assert.NoError(t, repo_model.WatchRepoMode(db.DefaultContext, 12, 1, repo_model.WatchModeDont))
unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1}, 1)
unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1, Mode: repo_model.WatchModeDont}, 1)
assert.NoError(t, repo_model.WatchRepoMode(db.DefaultContext, 12, 1, repo_model.WatchModeNone))
unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1}, 0)
}