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

308
services/user/block.go Normal file
View File

@@ -0,0 +1,308 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package user
import (
"context"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
org_model "code.gitea.io/gitea/models/organization"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
repo_service "code.gitea.io/gitea/services/repository"
)
func CanBlockUser(ctx context.Context, doer, blocker, blockee *user_model.User) bool {
if blocker.ID == blockee.ID {
return false
}
if doer.ID == blockee.ID {
return false
}
if blockee.IsOrganization() {
return false
}
if user_model.IsUserBlockedBy(ctx, blockee, blocker.ID) {
return false
}
if blocker.IsOrganization() {
org := org_model.OrgFromUser(blocker)
if isMember, _ := org.IsOrgMember(ctx, blockee.ID); isMember {
return false
}
if isAdmin, _ := org.IsOwnedBy(ctx, doer.ID); !isAdmin && !doer.IsAdmin {
return false
}
} else if !doer.IsAdmin && doer.ID != blocker.ID {
return false
}
return true
}
func CanUnblockUser(ctx context.Context, doer, blocker, blockee *user_model.User) bool {
if doer.ID == blockee.ID {
return false
}
if !user_model.IsUserBlockedBy(ctx, blockee, blocker.ID) {
return false
}
if blocker.IsOrganization() {
org := org_model.OrgFromUser(blocker)
if isAdmin, _ := org.IsOwnedBy(ctx, doer.ID); !isAdmin && !doer.IsAdmin {
return false
}
} else if !doer.IsAdmin && doer.ID != blocker.ID {
return false
}
return true
}
func BlockUser(ctx context.Context, doer, blocker, blockee *user_model.User, note string) error {
if blockee.IsOrganization() {
return user_model.ErrBlockOrganization
}
if !CanBlockUser(ctx, doer, blocker, blockee) {
return user_model.ErrCanNotBlock
}
return db.WithTx(ctx, func(ctx context.Context) error {
// unfollow each other
if err := user_model.UnfollowUser(ctx, blocker.ID, blockee.ID); err != nil {
return err
}
if err := user_model.UnfollowUser(ctx, blockee.ID, blocker.ID); err != nil {
return err
}
// unstar each other
if err := unstarRepos(ctx, blocker, blockee); err != nil {
return err
}
if err := unstarRepos(ctx, blockee, blocker); err != nil {
return err
}
// unwatch each others repositories
if err := unwatchRepos(ctx, blocker, blockee); err != nil {
return err
}
if err := unwatchRepos(ctx, blockee, blocker); err != nil {
return err
}
// unassign each other from issues
if err := unassignIssues(ctx, blocker, blockee); err != nil {
return err
}
if err := unassignIssues(ctx, blockee, blocker); err != nil {
return err
}
// remove each other from repository collaborations
if err := removeCollaborations(ctx, blocker, blockee); err != nil {
return err
}
if err := removeCollaborations(ctx, blockee, blocker); err != nil {
return err
}
// cancel each other repository transfers
if err := cancelRepositoryTransfers(ctx, blocker, blockee); err != nil {
return err
}
if err := cancelRepositoryTransfers(ctx, blockee, blocker); err != nil {
return err
}
return db.Insert(ctx, &user_model.Blocking{
BlockerID: blocker.ID,
BlockeeID: blockee.ID,
Note: note,
})
})
}
func unstarRepos(ctx context.Context, starrer, repoOwner *user_model.User) error {
opts := &repo_model.StarredReposOptions{
ListOptions: db.ListOptions{
Page: 1,
PageSize: 25,
},
StarrerID: starrer.ID,
RepoOwnerID: repoOwner.ID,
}
for {
repos, err := repo_model.GetStarredRepos(ctx, opts)
if err != nil {
return err
}
if len(repos) == 0 {
return nil
}
for _, repo := range repos {
if err := repo_model.StarRepo(ctx, starrer, repo, false); err != nil {
return err
}
}
opts.Page++
}
}
func unwatchRepos(ctx context.Context, watcher, repoOwner *user_model.User) error {
opts := &repo_model.WatchedReposOptions{
ListOptions: db.ListOptions{
Page: 1,
PageSize: 25,
},
WatcherID: watcher.ID,
RepoOwnerID: repoOwner.ID,
}
for {
repos, _, err := repo_model.GetWatchedRepos(ctx, opts)
if err != nil {
return err
}
if len(repos) == 0 {
return nil
}
for _, repo := range repos {
if err := repo_model.WatchRepo(ctx, watcher, repo, false); err != nil {
return err
}
}
opts.Page++
}
}
func cancelRepositoryTransfers(ctx context.Context, sender, recipient *user_model.User) error {
transfers, err := models.GetPendingRepositoryTransfers(ctx, &models.PendingRepositoryTransferOptions{
SenderID: sender.ID,
RecipientID: recipient.ID,
})
if err != nil {
return err
}
for _, transfer := range transfers {
repo, err := repo_model.GetRepositoryByID(ctx, transfer.RepoID)
if err != nil {
return err
}
if err := repo_service.CancelRepositoryTransfer(ctx, repo); err != nil {
return err
}
}
return nil
}
func unassignIssues(ctx context.Context, assignee, repoOwner *user_model.User) error {
opts := &issues_model.AssignedIssuesOptions{
ListOptions: db.ListOptions{
Page: 1,
PageSize: 25,
},
AssigneeID: assignee.ID,
RepoOwnerID: repoOwner.ID,
}
for {
issues, _, err := issues_model.GetAssignedIssues(ctx, opts)
if err != nil {
return err
}
if len(issues) == 0 {
return nil
}
for _, issue := range issues {
if err := issue.LoadAssignees(ctx); err != nil {
return err
}
if _, _, err := issues_model.ToggleIssueAssignee(ctx, issue, assignee, assignee.ID); err != nil {
return err
}
}
opts.Page++
}
}
func removeCollaborations(ctx context.Context, repoOwner, collaborator *user_model.User) error {
opts := &repo_model.FindCollaborationOptions{
ListOptions: db.ListOptions{
Page: 1,
PageSize: 25,
},
CollaboratorID: collaborator.ID,
RepoOwnerID: repoOwner.ID,
}
for {
collaborations, _, err := repo_model.GetCollaborators(ctx, opts)
if err != nil {
return err
}
if len(collaborations) == 0 {
return nil
}
for _, collaboration := range collaborations {
repo, err := repo_model.GetRepositoryByID(ctx, collaboration.Collaboration.RepoID)
if err != nil {
return err
}
if err := repo_service.DeleteCollaboration(ctx, repo, collaborator); err != nil {
return err
}
}
opts.Page++
}
}
func UnblockUser(ctx context.Context, doer, blocker, blockee *user_model.User) error {
if blockee.IsOrganization() {
return user_model.ErrBlockOrganization
}
if !CanUnblockUser(ctx, doer, blocker, blockee) {
return user_model.ErrCanNotUnblock
}
return db.WithTx(ctx, func(ctx context.Context) error {
block, err := user_model.GetBlocking(ctx, blocker.ID, blockee.ID)
if err != nil {
return err
}
if block != nil {
_, err = db.DeleteByID[user_model.Blocking](ctx, block.ID)
return err
}
return nil
})
}

View File

@@ -0,0 +1,66 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package user
import (
"testing"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"github.com/stretchr/testify/assert"
)
func TestCanBlockUser(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
user29 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 29})
org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3})
// Doer can't self block
assert.False(t, CanBlockUser(db.DefaultContext, user1, user2, user1))
// Blocker can't be blockee
assert.False(t, CanBlockUser(db.DefaultContext, user1, user2, user2))
// Can't block already blocked user
assert.False(t, CanBlockUser(db.DefaultContext, user1, user2, user29))
// Blockee can't be an organization
assert.False(t, CanBlockUser(db.DefaultContext, user1, user2, org3))
// Doer must be blocker or admin
assert.False(t, CanBlockUser(db.DefaultContext, user2, user4, user29))
// Organization can't block a member
assert.False(t, CanBlockUser(db.DefaultContext, user1, org3, user4))
// Doer must be organization owner or admin if blocker is an organization
assert.False(t, CanBlockUser(db.DefaultContext, user4, org3, user2))
assert.True(t, CanBlockUser(db.DefaultContext, user1, user2, user4))
assert.True(t, CanBlockUser(db.DefaultContext, user2, user2, user4))
assert.True(t, CanBlockUser(db.DefaultContext, user2, org3, user29))
}
func TestCanUnblockUser(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
user28 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 28})
user29 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 29})
org17 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 17})
// Doer can't self unblock
assert.False(t, CanUnblockUser(db.DefaultContext, user1, user2, user1))
// Can't unblock not blocked user
assert.False(t, CanUnblockUser(db.DefaultContext, user1, user2, user28))
// Doer must be blocker or admin
assert.False(t, CanUnblockUser(db.DefaultContext, user28, user2, user29))
// Doer must be organization owner or admin if blocker is an organization
assert.False(t, CanUnblockUser(db.DefaultContext, user2, org17, user28))
assert.True(t, CanUnblockUser(db.DefaultContext, user1, user2, user29))
assert.True(t, CanUnblockUser(db.DefaultContext, user2, user2, user29))
assert.True(t, CanUnblockUser(db.DefaultContext, user1, org17, user28))
}

View File

@@ -92,6 +92,8 @@ func deleteUser(ctx context.Context, u *user_model.User, purge bool) (err error)
&pull_model.ReviewState{UserID: u.ID},
&user_model.Redirect{RedirectUserID: u.ID},
&actions_model.ActionRunner{OwnerID: u.ID},
&user_model.Blocking{BlockerID: u.ID},
&user_model.Blocking{BlockeeID: u.ID},
); err != nil {
return fmt.Errorf("deleteBeans: %w", err)
}

View File

@@ -188,7 +188,7 @@ func DeleteUser(ctx context.Context, u *user_model.User, purge bool) error {
break
}
for _, org := range orgs {
if err := models.RemoveOrgUser(ctx, org.ID, u.ID); err != nil {
if err := models.RemoveOrgUser(ctx, org, u); err != nil {
if organization.IsErrLastOrgOwner(err) {
err = org_service.DeleteOrganization(ctx, org, true)
if err != nil {

View File

@@ -41,7 +41,8 @@ func TestDeleteUser(t *testing.T) {
orgUsers := make([]*organization.OrgUser, 0, 10)
assert.NoError(t, db.GetEngine(db.DefaultContext).Find(&orgUsers, &organization.OrgUser{UID: userID}))
for _, orgUser := range orgUsers {
if err := models.RemoveOrgUser(db.DefaultContext, orgUser.OrgID, orgUser.UID); err != nil {
org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: orgUser.OrgID})
if err := models.RemoveOrgUser(db.DefaultContext, org, user); err != nil {
assert.True(t, organization.IsErrLastOrgOwner(err))
return
}