forgejo/services/user/user_test.go
Gusted b2469c2a9c fix: allow admins to always rename users (#8715)
Do not apply the rename restriction of non-local users if the doer is an admin (changes via the admin interface). This is a conscious choice and the admin knows better if they make such changes.

Regression of c59a057297

Resolves forgejo/forgejo#3657

<!--start release-notes-assistant-->

## Release notes
<!--URL:https://codeberg.org/forgejo/forgejo-->
- Bug fixes
  - [PR](https://codeberg.org/forgejo/forgejo/pulls/8715): <!--number 8715 --><!--line 0 --><!--description YWxsb3cgYWRtaW5zIHRvIGFsd2F5cyByZW5hbWUgdXNlcnM=-->allow admins to always rename users<!--description-->
<!--end release-notes-assistant-->

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/8715
Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
Co-authored-by: Gusted <postmaster@gusted.xyz>
Co-committed-by: Gusted <postmaster@gusted.xyz>
2025-07-29 08:17:17 +02:00

341 lines
13 KiB
Go

// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package user
import (
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"time"
"forgejo.org/models"
asymkey_model "forgejo.org/models/asymkey"
"forgejo.org/models/auth"
"forgejo.org/models/db"
"forgejo.org/models/moderation"
"forgejo.org/models/organization"
repo_model "forgejo.org/models/repo"
"forgejo.org/models/unittest"
user_model "forgejo.org/models/user"
"forgejo.org/modules/json"
"forgejo.org/modules/optional"
"forgejo.org/modules/setting"
"forgejo.org/modules/test"
"forgejo.org/modules/timeutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMain(m *testing.M) {
unittest.MainTest(m)
}
func TestDeleteUser(t *testing.T) {
test := func(userID int64) {
require.NoError(t, unittest.PrepareTestDatabase())
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: userID})
ownedRepos := make([]*repo_model.Repository, 0, 10)
require.NoError(t, db.GetEngine(db.DefaultContext).Find(&ownedRepos, &repo_model.Repository{OwnerID: userID}))
if len(ownedRepos) > 0 {
err := DeleteUser(db.DefaultContext, user, false)
require.Error(t, err)
assert.True(t, models.IsErrUserOwnRepos(err))
return
}
orgUsers := make([]*organization.OrgUser, 0, 10)
require.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 {
assert.True(t, organization.IsErrLastOrgOwner(err))
return
}
}
require.NoError(t, DeleteUser(db.DefaultContext, user, false))
unittest.AssertNotExistsBean(t, &user_model.User{ID: userID})
unittest.CheckConsistencyFor(t, &user_model.User{}, &repo_model.Repository{})
}
test(2)
test(4)
test(8)
test(11)
org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3})
require.Error(t, DeleteUser(db.DefaultContext, org, false))
}
func TestPurgeUser(t *testing.T) {
defer unittest.OverrideFixtures("services/user/TestPurgeUser")()
require.NoError(t, unittest.PrepareTestDatabase())
defer test.MockVariableValue(&setting.SSH.RootPath, t.TempDir())()
defer test.MockVariableValue(&setting.SSH.CreateAuthorizedKeysFile, true)()
defer test.MockVariableValue(&setting.SSH.CreateAuthorizedPrincipalsFile, true)()
defer test.MockVariableValue(&setting.SSH.StartBuiltinServer, false)()
require.NoError(t, asymkey_model.RewriteAllPublicKeys(db.DefaultContext))
require.NoError(t, asymkey_model.RewriteAllPrincipalKeys(db.DefaultContext))
test := func(userID int64, modifySSHKey bool) {
require.NoError(t, unittest.PrepareTestDatabase())
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: userID})
fAuthorizedKeys, err := os.Open(filepath.Join(setting.SSH.RootPath, "authorized_keys"))
require.NoError(t, err)
authorizedKeysStatBefore, err := fAuthorizedKeys.Stat()
require.NoError(t, err)
fAuthorizedPrincipals, err := os.Open(filepath.Join(setting.SSH.RootPath, "authorized_principals"))
require.NoError(t, err)
authorizedPrincipalsBefore, err := fAuthorizedPrincipals.Stat()
require.NoError(t, err)
require.NoError(t, DeleteUser(db.DefaultContext, user, true))
unittest.AssertNotExistsBean(t, &user_model.User{ID: userID})
unittest.CheckConsistencyFor(t, &user_model.User{}, &repo_model.Repository{})
fAuthorizedKeys, err = os.Open(filepath.Join(setting.SSH.RootPath, "authorized_keys"))
require.NoError(t, err)
fAuthorizedPrincipals, err = os.Open(filepath.Join(setting.SSH.RootPath, "authorized_principals"))
require.NoError(t, err)
authorizedKeysStatAfter, err := fAuthorizedKeys.Stat()
require.NoError(t, err)
authorizedPrincipalsAfter, err := fAuthorizedPrincipals.Stat()
require.NoError(t, err)
if modifySSHKey {
assert.Greater(t, authorizedKeysStatAfter.ModTime(), authorizedKeysStatBefore.ModTime())
assert.Greater(t, authorizedPrincipalsAfter.ModTime(), authorizedPrincipalsBefore.ModTime())
} else {
assert.Equal(t, authorizedKeysStatAfter.ModTime(), authorizedKeysStatBefore.ModTime())
assert.Equal(t, authorizedPrincipalsAfter.ModTime(), authorizedPrincipalsBefore.ModTime())
}
}
test(2, true)
test(4, false)
test(8, false)
test(11, false)
org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3})
require.Error(t, DeleteUser(db.DefaultContext, org, false))
}
func TestCreateUser(t *testing.T) {
user := &user_model.User{
Name: "GiteaBot",
Email: "GiteaBot@gitea.io",
Passwd: ";p['////..-++']",
IsAdmin: false,
Theme: setting.UI.DefaultTheme,
MustChangePassword: false,
}
require.NoError(t, user_model.CreateUser(db.DefaultContext, user))
require.NoError(t, DeleteUser(db.DefaultContext, user, false))
}
func TestRenameUser(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 21})
t.Run("Non-Local", func(t *testing.T) {
u := &user_model.User{
ID: 2,
Name: "old-name",
Type: user_model.UserTypeIndividual,
LoginType: auth.OAuth2,
}
require.ErrorIs(t, RenameUser(db.DefaultContext, u, "user_rename2"), user_model.ErrUserIsNotLocal{UID: 2, Name: "old-name"})
t.Run("Admin", func(t *testing.T) {
require.NoError(t, AdminRenameUser(t.Context(), u, "user_rename2"))
})
})
t.Run("Same username", func(t *testing.T) {
require.NoError(t, RenameUser(db.DefaultContext, user, user.Name))
})
t.Run("Non usable username", func(t *testing.T) {
usernames := []string{"--diff", ".well-known", "gitea-actions", "aaa.atom", "aa.png"}
for _, username := range usernames {
require.Error(t, user_model.IsUsableUsername(username), "non-usable username: %s", username)
require.Error(t, RenameUser(db.DefaultContext, user, username), "non-usable username: %s", username)
}
})
t.Run("Only capitalization", func(t *testing.T) {
caps := strings.ToUpper(user.Name)
unittest.AssertNotExistsBean(t, &user_model.User{ID: user.ID, Name: caps})
unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: user.ID, OwnerName: user.Name})
require.NoError(t, RenameUser(db.DefaultContext, user, caps))
unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: user.ID, Name: caps})
unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: user.ID, OwnerName: caps})
})
t.Run("Already exists", func(t *testing.T) {
existUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
require.ErrorIs(t, RenameUser(db.DefaultContext, user, existUser.Name), user_model.ErrUserAlreadyExist{Name: existUser.Name})
require.ErrorIs(t, RenameUser(db.DefaultContext, user, existUser.LowerName), user_model.ErrUserAlreadyExist{Name: existUser.LowerName})
newUsername := fmt.Sprintf("uSEr%d", existUser.ID)
require.ErrorIs(t, RenameUser(db.DefaultContext, user, newUsername), user_model.ErrUserAlreadyExist{Name: newUsername})
})
t.Run("Normal", func(t *testing.T) {
oldUsername := user.Name
newUsername := "User_Rename"
require.NoError(t, RenameUser(db.DefaultContext, user, newUsername))
unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: user.ID, Name: newUsername, LowerName: strings.ToLower(newUsername)})
redirectUID, err := user_model.LookupUserRedirect(db.DefaultContext, oldUsername)
require.NoError(t, err)
assert.Equal(t, user.ID, redirectUID)
unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: user.ID, OwnerName: user.Name})
})
t.Run("Keep N redirects", func(t *testing.T) {
defer test.MockProtect(&setting.Service.MaxUserRedirects)()
// Start clean
unittest.AssertSuccessfulDelete(t, &user_model.Redirect{RedirectUserID: user.ID})
setting.Service.MaxUserRedirects = 1
require.NoError(t, RenameUser(db.DefaultContext, user, "redirect-1"))
unittest.AssertExistsIf(t, true, &user_model.Redirect{LowerName: "user_rename"})
// The granularity of created_unix is a second.
test.SleepTillNextSecond()
require.NoError(t, RenameUser(db.DefaultContext, user, "redirect-2"))
unittest.AssertExistsIf(t, false, &user_model.Redirect{LowerName: "user_rename"})
unittest.AssertExistsIf(t, true, &user_model.Redirect{LowerName: "redirect-1"})
setting.Service.MaxUserRedirects = 2
test.SleepTillNextSecond()
require.NoError(t, RenameUser(db.DefaultContext, user, "redirect-3"))
unittest.AssertExistsIf(t, true, &user_model.Redirect{LowerName: "redirect-1"})
unittest.AssertExistsIf(t, true, &user_model.Redirect{LowerName: "redirect-2"})
})
}
func TestCreateUser_Issue5882(t *testing.T) {
// Init settings
_ = setting.Admin
passwd := ".//.;1;;//.,-=_"
tt := []struct {
user *user_model.User
disableOrgCreation bool
}{
{&user_model.User{Name: "GiteaBot", Email: "GiteaBot@gitea.io", Passwd: passwd, MustChangePassword: false}, false},
{&user_model.User{Name: "GiteaBot2", Email: "GiteaBot2@gitea.io", Passwd: passwd, MustChangePassword: false}, true},
}
setting.Service.DefaultAllowCreateOrganization = true
for _, v := range tt {
setting.Admin.DisableRegularOrgCreation = v.disableOrgCreation
require.NoError(t, user_model.CreateUser(db.DefaultContext, v.user))
u, err := user_model.GetUserByEmail(db.DefaultContext, v.user.Email)
require.NoError(t, err)
assert.Equal(t, !u.AllowCreateOrganization, v.disableOrgCreation)
require.NoError(t, DeleteUser(db.DefaultContext, v.user, false))
}
}
func TestDeleteInactiveUsers(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
// Add an inactive user older than a minute, with an associated email_address record.
oldUser := &user_model.User{Name: "OldInactive", LowerName: "oldinactive", Email: "old@example.com", CreatedUnix: timeutil.TimeStampNow().Add(-120)}
_, err := db.GetEngine(db.DefaultContext).NoAutoTime().Insert(oldUser)
require.NoError(t, err)
oldEmail := &user_model.EmailAddress{UID: oldUser.ID, IsPrimary: true, Email: "old@example.com", LowerEmail: "old@example.com"}
err = db.Insert(db.DefaultContext, oldEmail)
require.NoError(t, err)
// Add an inactive user that's not older than a minute, with an associated email_address record.
newUser := &user_model.User{Name: "NewInactive", LowerName: "newinactive", Email: "new@example.com"}
err = db.Insert(db.DefaultContext, newUser)
require.NoError(t, err)
newEmail := &user_model.EmailAddress{UID: newUser.ID, IsPrimary: true, Email: "new@example.com", LowerEmail: "new@example.com"}
err = db.Insert(db.DefaultContext, newEmail)
require.NoError(t, err)
err = DeleteInactiveUsers(db.DefaultContext, time.Minute)
require.NoError(t, err)
// User older than a minute should be deleted along with their email address.
unittest.AssertExistsIf(t, false, oldUser)
unittest.AssertExistsIf(t, false, oldEmail)
// User not older than a minute shouldn't be deleted and their emaill address should still exist.
unittest.AssertExistsIf(t, true, newUser)
unittest.AssertExistsIf(t, true, newEmail)
}
func TestCreateShadowCopyOnUserUpdate(t *testing.T) {
defer unittest.OverrideFixtures("models/fixtures/ModerationFeatures")()
require.NoError(t, unittest.PrepareTestDatabase())
userAlexSmithID := int64(1002)
abuseReportID := int64(2) // submitted for @alexsmith
newDummyValue := "[REDACTED]" // used for updating profile text fields
// Retrieve the abusive user (@alexsmith) and the abuse report already created for this user.
abuser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: userAlexSmithID})
report := unittest.AssertExistsAndLoadBean(t, &moderation.AbuseReport{
ID: abuseReportID,
ContentType: moderation.ReportedContentTypeUser,
ContentID: abuser.ID,
})
// The report should not already have a shadow copy linked.
assert.False(t, report.ShadowCopyID.Valid)
// Keep a copy of old field values before updating them.
oldUserData := user_model.UserData{
FullName: abuser.FullName,
Location: abuser.Location,
Website: abuser.Website,
Pronouns: abuser.Pronouns,
Description: abuser.Description,
}
// The abusive user is updating their profile.
opts := &UpdateOptions{
FullName: optional.Some(newDummyValue),
Location: optional.Some(newDummyValue),
Website: optional.Some(newDummyValue),
Pronouns: optional.Some(newDummyValue),
Description: optional.Some(newDummyValue),
}
require.NoError(t, UpdateUser(t.Context(), abuser, opts))
// Reload the report.
report = unittest.AssertExistsAndLoadBean(t, &moderation.AbuseReport{ID: report.ID})
// A shadow copy should have been created and linked to our report.
assert.True(t, report.ShadowCopyID.Valid)
// Retrieve the newly created shadow copy and unmarshal the stored JSON so that we can check the values.
shadowCopy := unittest.AssertExistsAndLoadBean(t, &moderation.AbuseReportShadowCopy{ID: report.ShadowCopyID.Int64})
shadowCopyUserData := new(user_model.UserData)
require.NoError(t, json.Unmarshal([]byte(shadowCopy.RawValue), &shadowCopyUserData))
// Check to see if the initial field values of the user were stored within the shadow copy.
assert.Equal(t, oldUserData.FullName, shadowCopyUserData.FullName)
assert.Equal(t, oldUserData.Location, shadowCopyUserData.Location)
assert.Equal(t, oldUserData.Website, shadowCopyUserData.Website)
assert.Equal(t, oldUserData.Pronouns, shadowCopyUserData.Pronouns)
assert.Equal(t, oldUserData.Description, shadowCopyUserData.Description)
}