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:    --------- Co-authored-by: Lauris BH <lauris@nix.lv>
This commit is contained in:
@@ -100,12 +100,12 @@ func syncGroupsToTeamsCached(ctx context.Context, user *user_model.User, orgTeam
|
||||
}
|
||||
|
||||
if action == syncAdd && !isMember {
|
||||
if err := models.AddTeamMember(ctx, team, user.ID); err != nil {
|
||||
if err := models.AddTeamMember(ctx, team, user); err != nil {
|
||||
log.Error("group sync: Could not add user to team: %v", err)
|
||||
return err
|
||||
}
|
||||
} else if action == syncRemove && isMember {
|
||||
if err := models.RemoveTeamMember(ctx, team, user.ID); err != nil {
|
||||
if err := models.RemoveTeamMember(ctx, team, user); err != nil {
|
||||
log.Error("group sync: Could not remove user from team: %v", err)
|
||||
return err
|
||||
}
|
||||
|
@@ -449,3 +449,14 @@ func (f *PackageSettingForm) Validate(req *http.Request, errs binding.Errors) bi
|
||||
ctx := context.GetValidateContext(req)
|
||||
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
|
||||
}
|
||||
|
||||
type BlockUserForm struct {
|
||||
Action string `binding:"Required;In(block,unblock,note)"`
|
||||
Blockee string `binding:"Required"`
|
||||
Note string
|
||||
}
|
||||
|
||||
func (f *BlockUserForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
|
||||
ctx := context.GetValidateContext(req)
|
||||
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
|
||||
}
|
||||
|
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
@@ -21,6 +22,12 @@ func CreateRefComment(ctx context.Context, doer *user_model.User, repo *repo_mod
|
||||
return fmt.Errorf("cannot create reference with empty commit SHA")
|
||||
}
|
||||
|
||||
if user_model.IsUserBlockedBy(ctx, doer, issue.PosterID, repo.OwnerID) {
|
||||
if isAdmin, _ := access_model.IsUserRepoAdmin(ctx, repo, doer); !isAdmin {
|
||||
return user_model.ErrBlockedUser
|
||||
}
|
||||
}
|
||||
|
||||
// Check if same reference from same commit has already existed.
|
||||
has, err := db.GetEngine(ctx).Get(&issues_model.Comment{
|
||||
Type: issues_model.CommentTypeCommitRef,
|
||||
@@ -46,6 +53,12 @@ func CreateRefComment(ctx context.Context, doer *user_model.User, repo *repo_mod
|
||||
|
||||
// CreateIssueComment creates a plain issue comment.
|
||||
func CreateIssueComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue, content string, attachments []string) (*issues_model.Comment, error) {
|
||||
if user_model.IsUserBlockedBy(ctx, doer, issue.PosterID, repo.OwnerID) {
|
||||
if isAdmin, _ := access_model.IsUserRepoAdmin(ctx, repo, doer); !isAdmin {
|
||||
return nil, user_model.ErrBlockedUser
|
||||
}
|
||||
}
|
||||
|
||||
comment, err := issues_model.CreateComment(ctx, &issues_model.CreateCommentOptions{
|
||||
Type: issues_model.CommentTypeComment,
|
||||
Doer: doer,
|
||||
@@ -70,6 +83,19 @@ func CreateIssueComment(ctx context.Context, doer *user_model.User, repo *repo_m
|
||||
|
||||
// UpdateComment updates information of comment.
|
||||
func UpdateComment(ctx context.Context, c *issues_model.Comment, doer *user_model.User, oldContent string) error {
|
||||
if err := c.LoadIssue(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.Issue.LoadRepo(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if user_model.IsUserBlockedBy(ctx, doer, c.Issue.PosterID, c.Issue.Repo.OwnerID) {
|
||||
if isAdmin, _ := access_model.IsUserRepoAdmin(ctx, c.Issue.Repo, doer); !isAdmin {
|
||||
return user_model.ErrBlockedUser
|
||||
}
|
||||
}
|
||||
|
||||
needsContentHistory := c.Content != oldContent && c.Type.HasContentSupport()
|
||||
if needsContentHistory {
|
||||
hasContentHistory, err := issues_model.HasIssueContentHistory(ctx, c.IssueID, c.ID)
|
||||
|
@@ -5,6 +5,7 @@ package issue
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html"
|
||||
"net/url"
|
||||
@@ -160,6 +161,9 @@ func UpdateIssuesCommit(ctx context.Context, doer *user_model.User, repo *repo_m
|
||||
|
||||
message := fmt.Sprintf(`<a href="%s/commit/%s">%s</a>`, html.EscapeString(repo.Link()), html.EscapeString(url.PathEscape(c.Sha1)), html.EscapeString(strings.SplitN(c.Message, "\n", 2)[0]))
|
||||
if err = CreateRefComment(ctx, doer, refRepo, refIssue, message, c.Sha1); err != nil {
|
||||
if errors.Is(err, user_model.ErrBlockedUser) {
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
|
@@ -7,12 +7,23 @@ import (
|
||||
"context"
|
||||
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
notify_service "code.gitea.io/gitea/services/notify"
|
||||
)
|
||||
|
||||
// ChangeContent changes issue content, as the given user.
|
||||
func ChangeContent(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, content string) (err error) {
|
||||
func ChangeContent(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, content string) error {
|
||||
if err := issue.LoadRepo(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if user_model.IsUserBlockedBy(ctx, doer, issue.PosterID, issue.Repo.OwnerID) {
|
||||
if isAdmin, _ := access_model.IsUserRepoAdmin(ctx, issue.Repo, doer); !isAdmin {
|
||||
return user_model.ErrBlockedUser
|
||||
}
|
||||
}
|
||||
|
||||
oldContent := issue.Content
|
||||
|
||||
if err := issues_model.ChangeIssueContent(ctx, issue, doer, content); err != nil {
|
||||
|
@@ -15,6 +15,7 @@ import (
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
system_model "code.gitea.io/gitea/models/system"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/storage"
|
||||
notify_service "code.gitea.io/gitea/services/notify"
|
||||
@@ -22,6 +23,14 @@ import (
|
||||
|
||||
// NewIssue creates new issue with labels for repository.
|
||||
func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *issues_model.Issue, labelIDs []int64, uuids []string, assigneeIDs []int64) error {
|
||||
if err := issue.LoadPoster(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if user_model.IsUserBlockedBy(ctx, issue.Poster, repo.OwnerID) || user_model.IsUserBlockedBy(ctx, issue.Poster, assigneeIDs...) {
|
||||
return user_model.ErrBlockedUser
|
||||
}
|
||||
|
||||
if err := issues_model.NewIssue(ctx, repo, issue, labelIDs, uuids); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -57,6 +66,16 @@ func ChangeTitle(ctx context.Context, issue *issues_model.Issue, doer *user_mode
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := issue.LoadRepo(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if user_model.IsUserBlockedBy(ctx, doer, issue.PosterID, issue.Repo.OwnerID) {
|
||||
if isAdmin, _ := access_model.IsUserRepoAdmin(ctx, issue.Repo, doer); !isAdmin {
|
||||
return user_model.ErrBlockedUser
|
||||
}
|
||||
}
|
||||
|
||||
if err := issues_model.ChangeIssueTitle(ctx, issue, doer, oldTitle); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -93,31 +112,25 @@ func ChangeIssueRef(ctx context.Context, issue *issues_model.Issue, doer *user_m
|
||||
// Pass one or more user logins to replace the set of assignees on this Issue.
|
||||
// Send an empty array ([]) to clear all assignees from the Issue.
|
||||
func UpdateAssignees(ctx context.Context, issue *issues_model.Issue, oneAssignee string, multipleAssignees []string, doer *user_model.User) (err error) {
|
||||
var allNewAssignees []*user_model.User
|
||||
uniqueAssignees := container.SetOf(multipleAssignees...)
|
||||
|
||||
// Keep the old assignee thingy for compatibility reasons
|
||||
if oneAssignee != "" {
|
||||
// Prevent double adding assignees
|
||||
var isDouble bool
|
||||
for _, assignee := range multipleAssignees {
|
||||
if assignee == oneAssignee {
|
||||
isDouble = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !isDouble {
|
||||
multipleAssignees = append(multipleAssignees, oneAssignee)
|
||||
}
|
||||
uniqueAssignees.Add(oneAssignee)
|
||||
}
|
||||
|
||||
// Loop through all assignees to add them
|
||||
for _, assigneeName := range multipleAssignees {
|
||||
allNewAssignees := make([]*user_model.User, 0, len(uniqueAssignees))
|
||||
for _, assigneeName := range uniqueAssignees.Values() {
|
||||
assignee, err := user_model.GetUserByName(ctx, assigneeName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if user_model.IsUserBlockedBy(ctx, doer, assignee.ID) {
|
||||
return user_model.ErrBlockedUser
|
||||
}
|
||||
|
||||
allNewAssignees = append(allNewAssignees, assignee)
|
||||
}
|
||||
|
||||
|
50
services/issue/reaction.go
Normal file
50
services/issue/reaction.go
Normal file
@@ -0,0 +1,50 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package issue
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
)
|
||||
|
||||
// CreateIssueReaction creates a reaction on an issue.
|
||||
func CreateIssueReaction(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, content string) (*issues_model.Reaction, error) {
|
||||
if err := issue.LoadRepo(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if user_model.IsUserBlockedBy(ctx, doer, issue.PosterID, issue.Repo.OwnerID) {
|
||||
return nil, user_model.ErrBlockedUser
|
||||
}
|
||||
|
||||
return issues_model.CreateReaction(ctx, &issues_model.ReactionOptions{
|
||||
Type: content,
|
||||
DoerID: doer.ID,
|
||||
IssueID: issue.ID,
|
||||
})
|
||||
}
|
||||
|
||||
// CreateCommentReaction creates a reaction on a comment.
|
||||
func CreateCommentReaction(ctx context.Context, doer *user_model.User, comment *issues_model.Comment, content string) (*issues_model.Reaction, error) {
|
||||
if err := comment.LoadIssue(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := comment.Issue.LoadRepo(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if user_model.IsUserBlockedBy(ctx, doer, comment.Issue.PosterID, comment.Issue.Repo.OwnerID, comment.PosterID) {
|
||||
return nil, user_model.ErrBlockedUser
|
||||
}
|
||||
|
||||
return issues_model.CreateReaction(ctx, &issues_model.ReactionOptions{
|
||||
Type: content,
|
||||
DoerID: doer.ID,
|
||||
IssueID: comment.Issue.ID,
|
||||
CommentID: comment.ID,
|
||||
})
|
||||
}
|
162
services/issue/reaction_test.go
Normal file
162
services/issue/reaction_test.go
Normal file
@@ -0,0 +1,162 @@
|
||||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package issue
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
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"
|
||||
)
|
||||
|
||||
func addReaction(t *testing.T, doer *user_model.User, issue *issues_model.Issue, comment *issues_model.Comment, content string) {
|
||||
var reaction *issues_model.Reaction
|
||||
var err error
|
||||
if comment == nil {
|
||||
reaction, err = CreateIssueReaction(db.DefaultContext, doer, issue, content)
|
||||
} else {
|
||||
reaction, err = CreateCommentReaction(db.DefaultContext, doer, comment, content)
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, reaction)
|
||||
}
|
||||
|
||||
func TestIssueAddReaction(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
|
||||
|
||||
addReaction(t, user1, issue, nil, "heart")
|
||||
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue.ID})
|
||||
}
|
||||
|
||||
func TestIssueAddDuplicateReaction(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
|
||||
|
||||
addReaction(t, user1, issue, nil, "heart")
|
||||
|
||||
reaction, err := CreateIssueReaction(db.DefaultContext, user1, issue, "heart")
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, issues_model.ErrReactionAlreadyExist{Reaction: "heart"}, err)
|
||||
|
||||
existingR := unittest.AssertExistsAndLoadBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue.ID})
|
||||
assert.Equal(t, existingR.ID, reaction.ID)
|
||||
}
|
||||
|
||||
func TestIssueDeleteReaction(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
|
||||
|
||||
addReaction(t, user1, issue, nil, "heart")
|
||||
|
||||
err := issues_model.DeleteIssueReaction(db.DefaultContext, user1.ID, issue.ID, "heart")
|
||||
assert.NoError(t, err)
|
||||
|
||||
unittest.AssertNotExistsBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue.ID})
|
||||
}
|
||||
|
||||
func TestIssueReactionCount(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
setting.UI.ReactionMaxUserNum = 2
|
||||
|
||||
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3})
|
||||
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||||
ghost := user_model.NewGhostUser()
|
||||
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2})
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
|
||||
addReaction(t, user1, issue, nil, "heart")
|
||||
addReaction(t, user2, issue, nil, "heart")
|
||||
addReaction(t, org3, issue, nil, "heart")
|
||||
addReaction(t, org3, issue, nil, "+1")
|
||||
addReaction(t, user4, issue, nil, "+1")
|
||||
addReaction(t, user4, issue, nil, "heart")
|
||||
addReaction(t, ghost, issue, nil, "-1")
|
||||
|
||||
reactionsList, _, err := issues_model.FindReactions(db.DefaultContext, issues_model.FindReactionsOptions{
|
||||
IssueID: issue.ID,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, reactionsList, 7)
|
||||
_, err = reactionsList.LoadUsers(db.DefaultContext, repo)
|
||||
assert.NoError(t, err)
|
||||
|
||||
reactions := reactionsList.GroupByType()
|
||||
assert.Len(t, reactions["heart"], 4)
|
||||
assert.Equal(t, 2, reactions["heart"].GetMoreUserCount())
|
||||
assert.Equal(t, user1.Name+", "+user2.Name, reactions["heart"].GetFirstUsers())
|
||||
assert.True(t, reactions["heart"].HasUser(1))
|
||||
assert.False(t, reactions["heart"].HasUser(5))
|
||||
assert.False(t, reactions["heart"].HasUser(0))
|
||||
assert.Len(t, reactions["+1"], 2)
|
||||
assert.Equal(t, 0, reactions["+1"].GetMoreUserCount())
|
||||
assert.Len(t, reactions["-1"], 1)
|
||||
}
|
||||
|
||||
func TestIssueCommentAddReaction(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||
comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 1})
|
||||
|
||||
addReaction(t, user1, nil, comment, "heart")
|
||||
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: comment.IssueID, CommentID: comment.ID})
|
||||
}
|
||||
|
||||
func TestIssueCommentDeleteReaction(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})
|
||||
org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3})
|
||||
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||||
|
||||
comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 1})
|
||||
|
||||
addReaction(t, user1, nil, comment, "heart")
|
||||
addReaction(t, user2, nil, comment, "heart")
|
||||
addReaction(t, org3, nil, comment, "heart")
|
||||
addReaction(t, user4, nil, comment, "+1")
|
||||
|
||||
reactionsList, _, err := issues_model.FindReactions(db.DefaultContext, issues_model.FindReactionsOptions{
|
||||
IssueID: comment.IssueID,
|
||||
CommentID: comment.ID,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, reactionsList, 4)
|
||||
|
||||
reactions := reactionsList.GroupByType()
|
||||
assert.Len(t, reactions["heart"], 3)
|
||||
assert.Len(t, reactions["+1"], 1)
|
||||
}
|
||||
|
||||
func TestIssueCommentReactionCount(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||
comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 1})
|
||||
|
||||
addReaction(t, user1, nil, comment, "heart")
|
||||
assert.NoError(t, issues_model.DeleteCommentReaction(db.DefaultContext, user1.ID, comment.IssueID, comment.ID, "heart"))
|
||||
|
||||
unittest.AssertNotExistsBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: comment.IssueID, CommentID: comment.ID})
|
||||
}
|
@@ -40,6 +40,14 @@ var pullWorkingPool = sync.NewExclusivePool()
|
||||
|
||||
// NewPullRequest creates new pull request with labels for repository.
|
||||
func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *issues_model.Issue, labelIDs []int64, uuids []string, pr *issues_model.PullRequest, assigneeIDs []int64) error {
|
||||
if err := issue.LoadPoster(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if user_model.IsUserBlockedBy(ctx, issue.Poster, repo.OwnerID) || user_model.IsUserBlockedBy(ctx, issue.Poster, assigneeIDs...) {
|
||||
return user_model.ErrBlockedUser
|
||||
}
|
||||
|
||||
prCtx, cancel, err := createTemporaryRepoForPR(ctx, pr)
|
||||
if err != nil {
|
||||
if !git_model.IsErrBranchNotExist(err) {
|
||||
|
@@ -11,13 +11,14 @@ import (
|
||||
"code.gitea.io/gitea/models/db"
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
)
|
||||
|
||||
// DeleteCollaboration removes collaboration relation between the user and repository.
|
||||
func DeleteCollaboration(ctx context.Context, repo *repo_model.Repository, uid int64) (err error) {
|
||||
func DeleteCollaboration(ctx context.Context, repo *repo_model.Repository, collaborator *user_model.User) (err error) {
|
||||
collaboration := &repo_model.Collaboration{
|
||||
RepoID: repo.ID,
|
||||
UserID: uid,
|
||||
UserID: collaborator.ID,
|
||||
}
|
||||
|
||||
ctx, committer, err := db.TxContext(ctx)
|
||||
@@ -31,20 +32,25 @@ func DeleteCollaboration(ctx context.Context, repo *repo_model.Repository, uid i
|
||||
} else if has == 0 {
|
||||
return committer.Commit()
|
||||
}
|
||||
|
||||
if err := repo.LoadOwner(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = access_model.RecalculateAccesses(ctx, repo); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = repo_model.WatchRepo(ctx, uid, repo.ID, false); err != nil {
|
||||
if err = repo_model.WatchRepo(ctx, collaborator, repo, false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = models.ReconsiderWatches(ctx, repo, uid); err != nil {
|
||||
if err = models.ReconsiderWatches(ctx, repo, collaborator); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Unassign a user from any issue (s)he has been assigned to in the repository
|
||||
if err := models.ReconsiderRepoIssuesAssignee(ctx, repo, uid); err != nil {
|
||||
if err := models.ReconsiderRepoIssuesAssignee(ctx, repo, collaborator); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@@ -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"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -16,13 +17,15 @@ import (
|
||||
func TestRepository_DeleteCollaboration(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
||||
assert.NoError(t, repo.LoadOwner(db.DefaultContext))
|
||||
assert.NoError(t, DeleteCollaboration(db.DefaultContext, repo, 4))
|
||||
unittest.AssertNotExistsBean(t, &repo_model.Collaboration{RepoID: repo.ID, UserID: 4})
|
||||
|
||||
assert.NoError(t, DeleteCollaboration(db.DefaultContext, repo, 4))
|
||||
unittest.AssertNotExistsBean(t, &repo_model.Collaboration{RepoID: repo.ID, UserID: 4})
|
||||
assert.NoError(t, repo.LoadOwner(db.DefaultContext))
|
||||
assert.NoError(t, DeleteCollaboration(db.DefaultContext, repo, user))
|
||||
unittest.AssertNotExistsBean(t, &repo_model.Collaboration{RepoID: repo.ID, UserID: user.ID})
|
||||
|
||||
assert.NoError(t, DeleteCollaboration(db.DefaultContext, repo, user))
|
||||
unittest.AssertNotExistsBean(t, &repo_model.Collaboration{RepoID: repo.ID, UserID: user.ID})
|
||||
|
||||
unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repo.ID})
|
||||
}
|
||||
|
@@ -365,24 +365,26 @@ func removeRepositoryFromTeam(ctx context.Context, t *organization.Team, repo *r
|
||||
}
|
||||
}
|
||||
|
||||
teamUsers, err := organization.GetTeamUsersByTeamID(ctx, t.ID)
|
||||
teamMembers, err := organization.GetTeamMembers(ctx, &organization.SearchMembersOptions{
|
||||
TeamID: t.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("getTeamUsersByTeamID: %w", err)
|
||||
return fmt.Errorf("GetTeamMembers: %w", err)
|
||||
}
|
||||
for _, teamUser := range teamUsers {
|
||||
has, err := access_model.HasAccess(ctx, teamUser.UID, repo)
|
||||
for _, member := range teamMembers {
|
||||
has, err := access_model.HasAccess(ctx, member.ID, repo)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if has {
|
||||
continue
|
||||
}
|
||||
|
||||
if err = repo_model.WatchRepo(ctx, teamUser.UID, repo.ID, false); err != nil {
|
||||
if err = repo_model.WatchRepo(ctx, member, repo, false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Remove all IssueWatches a user has subscribed to in the repositories
|
||||
if err := issues_model.RemoveIssueWatchersByRepoID(ctx, teamUser.UID, repo.ID); err != nil {
|
||||
if err := issues_model.RemoveIssueWatchersByRepoID(ctx, member.ID, repo.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
@@ -53,6 +53,14 @@ type ForkRepoOptions struct {
|
||||
|
||||
// ForkRepository forks a repository
|
||||
func ForkRepository(ctx context.Context, doer, owner *user_model.User, opts ForkRepoOptions) (*repo_model.Repository, error) {
|
||||
if err := opts.BaseRepo.LoadOwner(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if user_model.IsUserBlockedBy(ctx, doer, opts.BaseRepo.Owner.ID) {
|
||||
return nil, user_model.ErrBlockedUser
|
||||
}
|
||||
|
||||
// Fork is prohibited, if user has reached maximum limit of repositories
|
||||
if !owner.CanForkRepo() {
|
||||
return nil, repo_model.ErrReachLimitOfRepo{
|
||||
|
@@ -139,9 +139,9 @@ func transferOwnership(ctx context.Context, doer *user_model.User, newOwnerName
|
||||
}
|
||||
|
||||
// Remove redundant collaborators.
|
||||
collaborators, err := repo_model.GetCollaborators(ctx, repo.ID, db.ListOptions{})
|
||||
collaborators, _, err := repo_model.GetCollaborators(ctx, &repo_model.FindCollaborationOptions{RepoID: repo.ID})
|
||||
if err != nil {
|
||||
return fmt.Errorf("getCollaborators: %w", err)
|
||||
return fmt.Errorf("GetCollaborators: %w", err)
|
||||
}
|
||||
|
||||
// Dummy object.
|
||||
@@ -201,13 +201,13 @@ func transferOwnership(ctx context.Context, doer *user_model.User, newOwnerName
|
||||
return fmt.Errorf("decrease old owner repository count: %w", err)
|
||||
}
|
||||
|
||||
if err := repo_model.WatchRepo(ctx, doer.ID, repo.ID, true); err != nil {
|
||||
if err := repo_model.WatchRepo(ctx, doer, repo, true); err != nil {
|
||||
return fmt.Errorf("watchRepo: %w", err)
|
||||
}
|
||||
|
||||
// Remove watch for organization.
|
||||
if oldOwner.IsOrganization() {
|
||||
if err := repo_model.WatchRepo(ctx, oldOwner.ID, repo.ID, false); err != nil {
|
||||
if err := repo_model.WatchRepo(ctx, oldOwner, repo, false); err != nil {
|
||||
return fmt.Errorf("watchRepo [false]: %w", err)
|
||||
}
|
||||
}
|
||||
@@ -371,6 +371,10 @@ func StartRepositoryTransfer(ctx context.Context, doer, newOwner *user_model.Use
|
||||
return TransferOwnership(ctx, doer, newOwner, repo, teams)
|
||||
}
|
||||
|
||||
if user_model.IsUserBlockedBy(ctx, doer, newOwner.ID) {
|
||||
return user_model.ErrBlockedUser
|
||||
}
|
||||
|
||||
// If new owner is an org and user can create repos he can transfer directly too
|
||||
if newOwner.IsOrganization() {
|
||||
allowed, err := organization.CanCreateOrgRepo(ctx, newOwner.ID, doer.ID)
|
||||
|
308
services/user/block.go
Normal file
308
services/user/block.go
Normal 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
|
||||
})
|
||||
}
|
66
services/user/block_test.go
Normal file
66
services/user/block_test.go
Normal 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))
|
||||
}
|
@@ -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)
|
||||
}
|
||||
|
@@ -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 {
|
||||
|
@@ -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
|
||||
}
|
||||
|
Reference in New Issue
Block a user