mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2025-08-19 17:01:12 +00:00
Add a new config option for OAuth2 authentication sources: allow users to change their username. In the case where OAuth2 is more like a social OAuth2 login there's no need to not allow users to change their username. The information how the user is linked to the authentication source is stored in different fields. Resolves forgejo/forgejo#687 Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/8714 Reviewed-by: 0ko <0ko@noreply.codeberg.org> Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org> Co-authored-by: Gusted <postmaster@gusted.xyz> Co-committed-by: Gusted <postmaster@gusted.xyz>
353 lines
14 KiB
Go
353 lines
14 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"
|
|
"forgejo.org/services/auth/source/oauth2"
|
|
|
|
"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) {
|
|
defer unittest.OverrideFixtures("models/user/fixtures/")()
|
|
require.NoError(t, unittest.PrepareTestDatabase())
|
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 21})
|
|
|
|
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"})
|
|
})
|
|
|
|
t.Run("Non-local", func(t *testing.T) {
|
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1041, LoginSource: 1001})
|
|
authSource := unittest.AssertExistsAndLoadBean(t, &auth.Source{ID: user.LoginSource})
|
|
assert.False(t, user.IsLocal())
|
|
assert.True(t, user.IsOAuth2())
|
|
|
|
t.Run("Allowed", func(t *testing.T) {
|
|
require.NoError(t, RenameUser(t.Context(), user, "I-am-a-local-username"))
|
|
})
|
|
|
|
t.Run("Not allowed", func(t *testing.T) {
|
|
authSourceCfg := authSource.Cfg.(*oauth2.Source)
|
|
authSourceCfg.AllowUsernameChange = false
|
|
authSource.Cfg = authSourceCfg
|
|
_, err := db.GetEngine(t.Context()).Cols("cfg").ID(authSource.ID).Update(authSource)
|
|
require.NoError(t, err)
|
|
|
|
require.ErrorIs(t, RenameUser(t.Context(), user, "Another-username-change"), user_model.ErrUserIsNotLocal{UID: user.ID, Name: user.Name})
|
|
t.Run("Admin", func(t *testing.T) {
|
|
require.NoError(t, AdminRenameUser(t.Context(), user, "Another-username-change"))
|
|
})
|
|
})
|
|
})
|
|
}
|
|
|
|
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)
|
|
}
|