fix: prevent orgs from being added as members of orgs (#9757)

Resolves #4167

Changes it to check that the user being added is `User` or `Bot` type before allowing it

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/9757
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
Co-authored-by: Cyborus <cyborus@cyborus.xyz>
Co-committed-by: Cyborus <cyborus@cyborus.xyz>
This commit is contained in:
Cyborus 2025-11-08 22:05:21 +01:00 committed by Gusted
parent 2f6ca55a1c
commit a68791d707
7 changed files with 100 additions and 1 deletions

View file

@ -54,6 +54,7 @@ forgejo.org/models/repo
WatchRepoMode WatchRepoMode
forgejo.org/models/user forgejo.org/models/user
IsErrUserWrongType
IsErrExternalLoginUserAlreadyExist IsErrExternalLoginUserAlreadyExist
IsErrExternalLoginUserNotExist IsErrExternalLoginUserNotExist
NewFederatedUser NewFederatedUser

View file

@ -510,6 +510,13 @@ func ChangeOrgUserStatus(ctx context.Context, orgID, uid int64, public bool) err
// AddOrgUser adds new user to given organization. // AddOrgUser adds new user to given organization.
func AddOrgUser(ctx context.Context, orgID, uid int64) error { func AddOrgUser(ctx context.Context, orgID, uid int64) error {
isUser, err := user_model.IsUserByID(ctx, uid)
if err != nil {
return err
} else if !isUser {
return user_model.ErrUserWrongType{UID: uid}
}
isAlreadyMember, err := IsOrganizationMember(ctx, orgID, uid) isAlreadyMember, err := IsOrganizationMember(ctx, orgID, uid)
if err != nil || isAlreadyMember { if err != nil || isAlreadyMember {
return err return err

View file

@ -142,6 +142,15 @@ func TestAddOrgUser(t *testing.T) {
org = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: orgID}) org = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: orgID})
assert.Equal(t, expectedNumMembers, org.NumMembers) assert.Equal(t, expectedNumMembers, org.NumMembers)
} }
testFailure := func(orgID, userID int64, isPublic bool) {
org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: orgID})
expectedNumMembers := org.NumMembers
require.ErrorIs(t, organization.AddOrgUser(db.DefaultContext, orgID, userID), user_model.ErrUserWrongType{UID: userID})
ou := &organization.OrgUser{OrgID: orgID, UID: userID}
unittest.AssertNotExistsBean(t, ou)
org = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: orgID})
assert.Equal(t, expectedNumMembers, org.NumMembers)
}
setting.Service.DefaultOrgMemberVisible = false setting.Service.DefaultOrgMemberVisible = false
testSuccess(3, 5, false) testSuccess(3, 5, false)
@ -149,7 +158,7 @@ func TestAddOrgUser(t *testing.T) {
testSuccess(6, 2, false) testSuccess(6, 2, false)
setting.Service.DefaultOrgMemberVisible = true setting.Service.DefaultOrgMemberVisible = true
testSuccess(6, 3, true) testFailure(6, 3, true)
unittest.CheckConsistencyFor(t, &user_model.User{}, &organization.Team{}) unittest.CheckConsistencyFor(t, &user_model.User{}, &organization.Team{})
} }

View file

@ -50,6 +50,25 @@ func (err ErrUserNotExist) Unwrap() error {
return util.ErrNotExist return util.ErrNotExist
} }
// ErrUserWrongType is returned if the user is of the wrong type (i.e. is an org when a user was expected)
type ErrUserWrongType struct {
UID int64
}
func IsErrUserWrongType(err error) bool {
_, ok := err.(ErrUserNotExist)
return ok
}
func (err ErrUserWrongType) Error() string {
return fmt.Sprintf("user is the wrong user type [uid: %d]", err.UID)
}
// Unwrap unwraps this error as a ErrNotExist error
func (err ErrUserWrongType) Unwrap() error {
return util.ErrInvalidArgument
}
// ErrUserProhibitLogin represents a "ErrUserProhibitLogin" kind of error. // ErrUserProhibitLogin represents a "ErrUserProhibitLogin" kind of error.
type ErrUserProhibitLogin struct { type ErrUserProhibitLogin struct {
UID int64 UID int64

View file

@ -35,3 +35,40 @@
repo_admin_change_team_access: false repo_admin_change_team_access: false
theme: "" theme: ""
keep_activity_private: false keep_activity_private: false
-
id: 1042
lower_name: bot01
name: bot01
full_name: Bot01
email: bot01@example.com
keep_email_private: false
email_notifications_preference: onmention
passwd: ZogKvWdyEx:password
passwd_hash_algo: dummy
must_change_password: false
login_source: 0
login_name: bot01
type: 4
salt: ZogKvWdyEx
max_repo_creation: -1
is_active: true
is_admin: false
is_restricted: false
allow_git_hook: false
allow_import_local: false
allow_create_organization: true
prohibit_login: true
avatar: ""
avatar_email: avatarbot01@example.com
use_custom_avatar: false
num_followers: 0
num_following: 0
num_stars: 0
num_repos: 0
num_teams: 0
num_members: 0
visibility: 0
repo_admin_change_team_access: false
theme: ""
keep_activity_private: false

View file

@ -461,6 +461,15 @@ func (u *User) IsUser() bool {
return u.Type == UserTypeIndividual || u.Type == UserTypeBot return u.Type == UserTypeIndividual || u.Type == UserTypeBot
} }
// Returns true if the given user ID belongs to an actual user, not an organization
func IsUserByID(ctx context.Context, uid int64) (bool, error) {
return db.GetEngine(ctx).
Where("id=?", uid).
In("type", UserTypeIndividual, UserTypeBot).
Table("user").
Exist()
}
// IsBot returns whether or not the user is of type bot // IsBot returns whether or not the user is of type bot
func (u *User) IsBot() bool { func (u *User) IsBot() bool {
return u.Type == UserTypeBot return u.Type == UserTypeBot

View file

@ -1056,3 +1056,20 @@ func TestGetUserByEmailSimple(t *testing.T) {
assert.Nil(t, u) assert.Nil(t, u)
}) })
} }
func TestIsUserConsistency(t *testing.T) {
defer unittest.OverrideFixtures("models/user/fixtures/")()
require.NoError(t, unittest.PrepareTestDatabase())
test := func(userID int64) {
user, err := user_model.GetUserByID(t.Context(), userID)
require.NoError(t, err)
isUser, err := user_model.IsUserByID(t.Context(), userID)
require.NoError(t, err)
assert.Equal(t, user.IsUser(), isUser)
}
test(1)
test(1041)
test(1042)
}