0
Fork 0
mirror of https://codeberg.org/forgejo/forgejo.git synced 2025-02-22 22:06:21 -05:00
forgejo/services/user/user_test.go
Gusted a9c97110f9 feat: add configurable cooldown to claim usernames (#6422)
Add a new option that allows instances to set a cooldown period to claim
old usernames. In the context of public instances this can be used to
prevent old usernames to be claimed after they are free and allow
graceful migration (by making use of the redirect feature) to a new
username. The granularity of this cooldown is a day. By default this
feature is disabled and thus no cooldown period.

The `CreatedUnix` column is added the `user_redirect` table, for
existing redirects the timestamp is simply zero as we simply do not know
when they were created and are likely already over the cooldown period
if the instance configures one.

Users can always reclaim their 'old' user name again within the cooldown
period. Users can also always reclaim 'old' names of organization they
currently own within the cooldown period.

Creating and renaming users as an admin user are not affected by the
cooldown period for moderation and user support reasons.

To avoid abuse of the cooldown feature, such that a user holds a lot of
usernames, a new option is added `MAX_USER_REDIRECTS` which sets a limit
to the amount of user redirects a user may have, by default this is
disabled. If a cooldown period is set then the default is 5. This
feature operates independently of the cooldown period feature.

Added integration and unit testing.

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6422
Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
Reviewed-by: 0ko <0ko@noreply.codeberg.org>
Reviewed-by: Otto <otto@codeberg.org>
Co-authored-by: Gusted <postmaster@gusted.xyz>
Co-committed-by: Gusted <postmaster@gusted.xyz>
2025-01-24 04:16:56 +00:00

285 lines
11 KiB
Go

// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package user
import (
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"time"
"code.gitea.io/gitea/models"
asymkey_model "code.gitea.io/gitea/models/asymkey"
"code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization"
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"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/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(
unittest.FixturesOptions{
Dir: filepath.Join(setting.AppWorkPath, "models/fixtures/"),
Base: setting.AppWorkPath,
Dirs: []string{"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{
Type: user_model.UserTypeIndividual,
LoginType: auth.OAuth2,
}
require.ErrorIs(t, RenameUser(db.DefaultContext, u, "user_rename"), user_model.ErrUserIsNotLocal{})
})
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.EqualValues(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.
time.Sleep(time.Second)
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
time.Sleep(time.Second)
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)
}