Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unify user update methods #28733

Merged
merged 12 commits into from Feb 4, 2024
41 changes: 19 additions & 22 deletions cmd/admin_user_change_password.go
Expand Up @@ -4,13 +4,14 @@
package cmd

import (
"context"
"errors"
"fmt"

user_model "code.gitea.io/gitea/models/user"
pwd "code.gitea.io/gitea/modules/auth/password"
"code.gitea.io/gitea/modules/auth/password"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
user_service "code.gitea.io/gitea/services/user"

"github.com/urfave/cli/v2"
)
Expand Down Expand Up @@ -46,31 +47,27 @@ func runChangePassword(c *cli.Context) error {
if err := initDB(ctx); err != nil {
return err
}
if len(c.String("password")) < setting.MinPasswordLength {
return fmt.Errorf("Password is not long enough. Needs to be at least %d", setting.MinPasswordLength)
}

if !pwd.IsComplexEnough(c.String("password")) {
return errors.New("Password does not meet complexity requirements")
}
pwned, err := pwd.IsPwned(context.Background(), c.String("password"))
if err != nil {
return err
}
if pwned {
return errors.New("The password you chose is on a list of stolen passwords previously exposed in public data breaches. Please try again with a different password.\nFor more details, see https://haveibeenpwned.com/Passwords")
}
uname := c.String("username")
user, err := user_model.GetUserByName(ctx, uname)
user, err := user_model.GetUserByName(ctx, c.String("username"))
if err != nil {
return err
}
if err = user.SetPassword(c.String("password")); err != nil {
return err
}

if err = user_model.UpdateUserCols(ctx, user, "passwd", "passwd_hash_algo", "salt"); err != nil {
return err
opts := &user_service.UpdateAuthOptions{
Password: optional.Some(c.String("password")),
MustChangePassword: optional.Some(false),
}
if err := user_service.UpdateAuth(ctx, user, opts); err != nil {
switch {
case errors.Is(err, password.ErrMinLength):
return fmt.Errorf("Password is not long enough. Needs to be at least %d", setting.MinPasswordLength)
case errors.Is(err, password.ErrComplexity):
return errors.New("Password does not meet complexity requirements")
case errors.Is(err, password.ErrIsPwned):
return errors.New("The password you chose is on a list of stolen passwords previously exposed in public data breaches. Please try again with a different password.\nFor more details, see https://haveibeenpwned.com/Passwords")
default:
return err
}
}

fmt.Printf("%s's password has been successfully updated!\n", user.Name)
Expand Down
8 changes: 8 additions & 0 deletions models/fixtures/email_address.yml
Expand Up @@ -285,3 +285,11 @@
lower_email: abcde@gitea.com
is_activated: true
is_primary: false

-
id: 37
uid: 37
email: user37@example.com
lower_email: user37@example.com
is_activated: true
is_primary: true
39 changes: 38 additions & 1 deletion models/fixtures/user.yml
Expand Up @@ -1095,7 +1095,7 @@
allow_git_hook: false
allow_import_local: false
allow_create_organization: true
prohibit_login: true
prohibit_login: false
avatar: avatar29
avatar_email: user30@example.com
use_custom_avatar: false
Expand Down Expand Up @@ -1332,3 +1332,40 @@
repo_admin_change_team_access: false
theme: ""
keep_activity_private: false

-
id: 37
lower_name: user37
name: user37
full_name: User 37
email: user37@example.com
keep_email_private: false
email_notifications_preference: enabled
passwd: ZogKvWdyEx:password
passwd_hash_algo: dummy
must_change_password: false
login_source: 0
login_name: user37
type: 0
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: avatar29
avatar_email: user37@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
134 changes: 48 additions & 86 deletions models/user/email_address.go
Expand Up @@ -142,12 +142,29 @@ func (email *EmailAddress) BeforeInsert() {
}
}

func InsertEmailAddress(ctx context.Context, email *EmailAddress) (*EmailAddress, error) {
if _, err := db.GetEngine(ctx).Insert(email); err != nil {
return nil, err
}
return email, nil
}

func UpdateEmailAddress(ctx context.Context, email *EmailAddress) error {
_, err := db.GetEngine(ctx).ID(email.ID).AllCols().Update(email)
return err
}

func DeleteEmailAddressByID(ctx context.Context, id int64) error {
KN4CK3R marked this conversation as resolved.
Show resolved Hide resolved
_, err := db.GetEngine(ctx).ID(id).Delete(&EmailAddress{})
return err
}

var emailRegexp = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]*@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")

// ValidateEmail check if email is a allowed address
func ValidateEmail(email string) error {
if len(email) == 0 {
return nil
return ErrEmailInvalid{email}
}

if !emailRegexp.MatchString(email) {
Expand Down Expand Up @@ -177,6 +194,36 @@ func ValidateEmail(email string) error {
return nil
}

func GetEmailAddressByEmail(ctx context.Context, email string) (*EmailAddress, error) {
delvh marked this conversation as resolved.
Show resolved Hide resolved
ea := &EmailAddress{}
if has, err := db.GetEngine(ctx).Where("lower_email=?", strings.ToLower(email)).Get(ea); err != nil {
return nil, err
} else if !has {
return nil, ErrEmailAddressNotExist{email}
}
return ea, nil
}

func GetEmailAddressOfUser(ctx context.Context, email string, uid int64) (*EmailAddress, error) {
ea := &EmailAddress{}
if has, err := db.GetEngine(ctx).Where("lower_email=? AND uid=?", strings.ToLower(email), uid).Get(ea); err != nil {
return nil, err
} else if !has {
return nil, ErrEmailAddressNotExist{email}
}
return ea, nil
}

func GetPrimaryEmailAddressOfUser(ctx context.Context, uid int64) (*EmailAddress, error) {
ea := &EmailAddress{}
if has, err := db.GetEngine(ctx).Where("uid=? AND is_primary=?", uid, true).Get(ea); err != nil {
return nil, err
} else if !has {
return nil, ErrEmailAddressNotExist{}
}
return ea, nil
}

// GetEmailAddresses returns all email addresses belongs to given user.
func GetEmailAddresses(ctx context.Context, uid int64) ([]*EmailAddress, error) {
emails := make([]*EmailAddress, 0, 5)
Expand Down Expand Up @@ -235,91 +282,6 @@ func IsEmailUsed(ctx context.Context, email string) (bool, error) {
return db.GetEngine(ctx).Where("lower_email=?", strings.ToLower(email)).Get(&EmailAddress{})
}

// AddEmailAddress adds an email address to given user.
func AddEmailAddress(ctx context.Context, email *EmailAddress) error {
email.Email = strings.TrimSpace(email.Email)
used, err := IsEmailUsed(ctx, email.Email)
if err != nil {
return err
} else if used {
return ErrEmailAlreadyUsed{email.Email}
}

if err = ValidateEmail(email.Email); err != nil {
return err
}

return db.Insert(ctx, email)
}

// AddEmailAddresses adds an email address to given user.
func AddEmailAddresses(ctx context.Context, emails []*EmailAddress) error {
if len(emails) == 0 {
return nil
}

// Check if any of them has been used
for i := range emails {
emails[i].Email = strings.TrimSpace(emails[i].Email)
used, err := IsEmailUsed(ctx, emails[i].Email)
if err != nil {
return err
} else if used {
return ErrEmailAlreadyUsed{emails[i].Email}
}
if err = ValidateEmail(emails[i].Email); err != nil {
return err
}
}

if err := db.Insert(ctx, emails); err != nil {
return fmt.Errorf("Insert: %w", err)
}

return nil
}

// DeleteEmailAddress deletes an email address of given user.
func DeleteEmailAddress(ctx context.Context, email *EmailAddress) (err error) {
if email.IsPrimary {
return ErrPrimaryEmailCannotDelete{Email: email.Email}
}

var deleted int64
// ask to check UID
address := EmailAddress{
UID: email.UID,
}
if email.ID > 0 {
deleted, err = db.GetEngine(ctx).ID(email.ID).Delete(&address)
} else {
if email.Email != "" && email.LowerEmail == "" {
email.LowerEmail = strings.ToLower(email.Email)
}
deleted, err = db.GetEngine(ctx).
Where("lower_email=?", email.LowerEmail).
Delete(&address)
}

if err != nil {
return err
} else if deleted != 1 {
return ErrEmailAddressNotExist{Email: email.Email}
}
return nil
}

// DeleteEmailAddresses deletes multiple email addresses
func DeleteEmailAddresses(ctx context.Context, emails []*EmailAddress) (err error) {
for i := range emails {
if err = DeleteEmailAddress(ctx, emails[i]); err != nil {
return err
}
}

return nil
}

// DeleteInactiveEmailAddresses deletes inactive email addresses
func DeleteInactiveEmailAddresses(ctx context.Context) error {
_, err := db.GetEngine(ctx).
Expand Down
90 changes: 0 additions & 90 deletions models/user/email_address_test.go
Expand Up @@ -42,96 +42,6 @@ func TestIsEmailUsed(t *testing.T) {
assert.False(t, isExist)
}

func TestAddEmailAddress(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())

assert.NoError(t, user_model.AddEmailAddress(db.DefaultContext, &user_model.EmailAddress{
Email: "user1234567890@example.com",
LowerEmail: "user1234567890@example.com",
IsPrimary: true,
IsActivated: true,
}))

// ErrEmailAlreadyUsed
err := user_model.AddEmailAddress(db.DefaultContext, &user_model.EmailAddress{
Email: "user1234567890@example.com",
LowerEmail: "user1234567890@example.com",
})
assert.Error(t, err)
assert.True(t, user_model.IsErrEmailAlreadyUsed(err))
}

func TestAddEmailAddresses(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())

// insert multiple email address
emails := make([]*user_model.EmailAddress, 2)
emails[0] = &user_model.EmailAddress{
Email: "user1234@example.com",
LowerEmail: "user1234@example.com",
IsActivated: true,
}
emails[1] = &user_model.EmailAddress{
Email: "user5678@example.com",
LowerEmail: "user5678@example.com",
IsActivated: true,
}
assert.NoError(t, user_model.AddEmailAddresses(db.DefaultContext, emails))

// ErrEmailAlreadyUsed
err := user_model.AddEmailAddresses(db.DefaultContext, emails)
assert.Error(t, err)
assert.True(t, user_model.IsErrEmailAlreadyUsed(err))
}

func TestDeleteEmailAddress(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())

assert.NoError(t, user_model.DeleteEmailAddress(db.DefaultContext, &user_model.EmailAddress{
UID: int64(1),
ID: int64(33),
Email: "user1-2@example.com",
LowerEmail: "user1-2@example.com",
}))

assert.NoError(t, user_model.DeleteEmailAddress(db.DefaultContext, &user_model.EmailAddress{
UID: int64(1),
Email: "user1-3@example.com",
LowerEmail: "user1-3@example.com",
}))

// Email address does not exist
err := user_model.DeleteEmailAddress(db.DefaultContext, &user_model.EmailAddress{
UID: int64(1),
Email: "user1234567890@example.com",
LowerEmail: "user1234567890@example.com",
})
assert.Error(t, err)
}

func TestDeleteEmailAddresses(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())

// delete multiple email address
emails := make([]*user_model.EmailAddress, 2)
emails[0] = &user_model.EmailAddress{
UID: int64(2),
ID: int64(3),
Email: "user2@example.com",
LowerEmail: "user2@example.com",
}
emails[1] = &user_model.EmailAddress{
UID: int64(2),
Email: "user2-2@example.com",
LowerEmail: "user2-2@example.com",
}
assert.NoError(t, user_model.DeleteEmailAddresses(db.DefaultContext, emails))

// ErrEmailAlreadyUsed
err := user_model.DeleteEmailAddresses(db.DefaultContext, emails)
assert.Error(t, err)
}

func TestMakeEmailPrimary(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())

Expand Down