Skip to content

Commit f8b471a

Browse files
authored
Unify user update methods (#28733)
Fixes #28660 Fixes an admin api bug related to `user.LoginSource` Fixed `/user/emails` response not identical to GitHub api This PR unifies the user update methods. The goal is to keep the logic only at one place (having audit logs in mind). For example, do the password checks only in one method not everywhere a password is updated. After that PR is merged, the user creation should be next.
1 parent b4513f4 commit f8b471a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1378
-1063
lines changed

cmd/admin_user_change_password.go

Lines changed: 21 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@
44
package cmd
55

66
import (
7-
"context"
87
"errors"
98
"fmt"
109

1110
user_model "code.gitea.io/gitea/models/user"
12-
pwd "code.gitea.io/gitea/modules/auth/password"
11+
"code.gitea.io/gitea/modules/auth/password"
12+
"code.gitea.io/gitea/modules/optional"
1313
"code.gitea.io/gitea/modules/setting"
14+
user_service "code.gitea.io/gitea/services/user"
1415

1516
"github.com/urfave/cli/v2"
1617
)
@@ -50,35 +51,32 @@ func runChangePassword(c *cli.Context) error {
5051
if err := initDB(ctx); err != nil {
5152
return err
5253
}
53-
if len(c.String("password")) < setting.MinPasswordLength {
54-
return fmt.Errorf("Password is not long enough. Needs to be at least %d", setting.MinPasswordLength)
55-
}
5654

57-
if !pwd.IsComplexEnough(c.String("password")) {
58-
return errors.New("Password does not meet complexity requirements")
59-
}
60-
pwned, err := pwd.IsPwned(context.Background(), c.String("password"))
61-
if err != nil {
62-
return err
63-
}
64-
if pwned {
65-
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")
66-
}
67-
uname := c.String("username")
68-
user, err := user_model.GetUserByName(ctx, uname)
55+
user, err := user_model.GetUserByName(ctx, c.String("username"))
6956
if err != nil {
7057
return err
7158
}
72-
if err = user.SetPassword(c.String("password")); err != nil {
73-
return err
74-
}
7559

60+
var mustChangePassword optional.Option[bool]
7661
if c.IsSet("must-change-password") {
77-
user.MustChangePassword = c.Bool("must-change-password")
62+
mustChangePassword = optional.Some(c.Bool("must-change-password"))
7863
}
7964

80-
if err = user_model.UpdateUserCols(ctx, user, "must_change_password", "passwd", "passwd_hash_algo", "salt"); err != nil {
81-
return err
65+
opts := &user_service.UpdateAuthOptions{
66+
Password: optional.Some(c.String("password")),
67+
MustChangePassword: mustChangePassword,
68+
}
69+
if err := user_service.UpdateAuth(ctx, user, opts); err != nil {
70+
switch {
71+
case errors.Is(err, password.ErrMinLength):
72+
return fmt.Errorf("Password is not long enough. Needs to be at least %d", setting.MinPasswordLength)
73+
case errors.Is(err, password.ErrComplexity):
74+
return errors.New("Password does not meet complexity requirements")
75+
case errors.Is(err, password.ErrIsPwned):
76+
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")
77+
default:
78+
return err
79+
}
8280
}
8381

8482
fmt.Printf("%s's password has been successfully updated!\n", user.Name)

models/fixtures/email_address.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,3 +285,11 @@
285285
lower_email: abcde@gitea.com
286286
is_activated: true
287287
is_primary: false
288+
289+
-
290+
id: 37
291+
uid: 37
292+
email: user37@example.com
293+
lower_email: user37@example.com
294+
is_activated: true
295+
is_primary: true

models/fixtures/user.yml

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1095,7 +1095,7 @@
10951095
allow_git_hook: false
10961096
allow_import_local: false
10971097
allow_create_organization: true
1098-
prohibit_login: true
1098+
prohibit_login: false
10991099
avatar: avatar29
11001100
avatar_email: user30@example.com
11011101
use_custom_avatar: false
@@ -1332,3 +1332,40 @@
13321332
repo_admin_change_team_access: false
13331333
theme: ""
13341334
keep_activity_private: false
1335+
1336+
-
1337+
id: 37
1338+
lower_name: user37
1339+
name: user37
1340+
full_name: User 37
1341+
email: user37@example.com
1342+
keep_email_private: false
1343+
email_notifications_preference: enabled
1344+
passwd: ZogKvWdyEx:password
1345+
passwd_hash_algo: dummy
1346+
must_change_password: false
1347+
login_source: 0
1348+
login_name: user37
1349+
type: 0
1350+
salt: ZogKvWdyEx
1351+
max_repo_creation: -1
1352+
is_active: true
1353+
is_admin: false
1354+
is_restricted: false
1355+
allow_git_hook: false
1356+
allow_import_local: false
1357+
allow_create_organization: true
1358+
prohibit_login: true
1359+
avatar: avatar29
1360+
avatar_email: user37@example.com
1361+
use_custom_avatar: false
1362+
num_followers: 0
1363+
num_following: 0
1364+
num_stars: 0
1365+
num_repos: 0
1366+
num_teams: 0
1367+
num_members: 0
1368+
visibility: 0
1369+
repo_admin_change_team_access: false
1370+
theme: ""
1371+
keep_activity_private: false

models/user/email_address.go

Lines changed: 43 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -142,12 +142,24 @@ func (email *EmailAddress) BeforeInsert() {
142142
}
143143
}
144144

145+
func InsertEmailAddress(ctx context.Context, email *EmailAddress) (*EmailAddress, error) {
146+
if err := db.Insert(ctx, email); err != nil {
147+
return nil, err
148+
}
149+
return email, nil
150+
}
151+
152+
func UpdateEmailAddress(ctx context.Context, email *EmailAddress) error {
153+
_, err := db.GetEngine(ctx).ID(email.ID).AllCols().Update(email)
154+
return err
155+
}
156+
145157
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])?)*$")
146158

147159
// ValidateEmail check if email is a allowed address
148160
func ValidateEmail(email string) error {
149161
if len(email) == 0 {
150-
return nil
162+
return ErrEmailInvalid{email}
151163
}
152164

153165
if !emailRegexp.MatchString(email) {
@@ -177,6 +189,36 @@ func ValidateEmail(email string) error {
177189
return nil
178190
}
179191

192+
func GetEmailAddressByEmail(ctx context.Context, email string) (*EmailAddress, error) {
193+
ea := &EmailAddress{}
194+
if has, err := db.GetEngine(ctx).Where("lower_email=?", strings.ToLower(email)).Get(ea); err != nil {
195+
return nil, err
196+
} else if !has {
197+
return nil, ErrEmailAddressNotExist{email}
198+
}
199+
return ea, nil
200+
}
201+
202+
func GetEmailAddressOfUser(ctx context.Context, email string, uid int64) (*EmailAddress, error) {
203+
ea := &EmailAddress{}
204+
if has, err := db.GetEngine(ctx).Where("lower_email=? AND uid=?", strings.ToLower(email), uid).Get(ea); err != nil {
205+
return nil, err
206+
} else if !has {
207+
return nil, ErrEmailAddressNotExist{email}
208+
}
209+
return ea, nil
210+
}
211+
212+
func GetPrimaryEmailAddressOfUser(ctx context.Context, uid int64) (*EmailAddress, error) {
213+
ea := &EmailAddress{}
214+
if has, err := db.GetEngine(ctx).Where("uid=? AND is_primary=?", uid, true).Get(ea); err != nil {
215+
return nil, err
216+
} else if !has {
217+
return nil, ErrEmailAddressNotExist{}
218+
}
219+
return ea, nil
220+
}
221+
180222
// GetEmailAddresses returns all email addresses belongs to given user.
181223
func GetEmailAddresses(ctx context.Context, uid int64) ([]*EmailAddress, error) {
182224
emails := make([]*EmailAddress, 0, 5)
@@ -235,91 +277,6 @@ func IsEmailUsed(ctx context.Context, email string) (bool, error) {
235277
return db.GetEngine(ctx).Where("lower_email=?", strings.ToLower(email)).Get(&EmailAddress{})
236278
}
237279

238-
// AddEmailAddress adds an email address to given user.
239-
func AddEmailAddress(ctx context.Context, email *EmailAddress) error {
240-
email.Email = strings.TrimSpace(email.Email)
241-
used, err := IsEmailUsed(ctx, email.Email)
242-
if err != nil {
243-
return err
244-
} else if used {
245-
return ErrEmailAlreadyUsed{email.Email}
246-
}
247-
248-
if err = ValidateEmail(email.Email); err != nil {
249-
return err
250-
}
251-
252-
return db.Insert(ctx, email)
253-
}
254-
255-
// AddEmailAddresses adds an email address to given user.
256-
func AddEmailAddresses(ctx context.Context, emails []*EmailAddress) error {
257-
if len(emails) == 0 {
258-
return nil
259-
}
260-
261-
// Check if any of them has been used
262-
for i := range emails {
263-
emails[i].Email = strings.TrimSpace(emails[i].Email)
264-
used, err := IsEmailUsed(ctx, emails[i].Email)
265-
if err != nil {
266-
return err
267-
} else if used {
268-
return ErrEmailAlreadyUsed{emails[i].Email}
269-
}
270-
if err = ValidateEmail(emails[i].Email); err != nil {
271-
return err
272-
}
273-
}
274-
275-
if err := db.Insert(ctx, emails); err != nil {
276-
return fmt.Errorf("Insert: %w", err)
277-
}
278-
279-
return nil
280-
}
281-
282-
// DeleteEmailAddress deletes an email address of given user.
283-
func DeleteEmailAddress(ctx context.Context, email *EmailAddress) (err error) {
284-
if email.IsPrimary {
285-
return ErrPrimaryEmailCannotDelete{Email: email.Email}
286-
}
287-
288-
var deleted int64
289-
// ask to check UID
290-
address := EmailAddress{
291-
UID: email.UID,
292-
}
293-
if email.ID > 0 {
294-
deleted, err = db.GetEngine(ctx).ID(email.ID).Delete(&address)
295-
} else {
296-
if email.Email != "" && email.LowerEmail == "" {
297-
email.LowerEmail = strings.ToLower(email.Email)
298-
}
299-
deleted, err = db.GetEngine(ctx).
300-
Where("lower_email=?", email.LowerEmail).
301-
Delete(&address)
302-
}
303-
304-
if err != nil {
305-
return err
306-
} else if deleted != 1 {
307-
return ErrEmailAddressNotExist{Email: email.Email}
308-
}
309-
return nil
310-
}
311-
312-
// DeleteEmailAddresses deletes multiple email addresses
313-
func DeleteEmailAddresses(ctx context.Context, emails []*EmailAddress) (err error) {
314-
for i := range emails {
315-
if err = DeleteEmailAddress(ctx, emails[i]); err != nil {
316-
return err
317-
}
318-
}
319-
320-
return nil
321-
}
322-
323280
// DeleteInactiveEmailAddresses deletes inactive email addresses
324281
func DeleteInactiveEmailAddresses(ctx context.Context) error {
325282
_, err := db.GetEngine(ctx).

models/user/email_address_test.go

Lines changed: 0 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -42,96 +42,6 @@ func TestIsEmailUsed(t *testing.T) {
4242
assert.False(t, isExist)
4343
}
4444

45-
func TestAddEmailAddress(t *testing.T) {
46-
assert.NoError(t, unittest.PrepareTestDatabase())
47-
48-
assert.NoError(t, user_model.AddEmailAddress(db.DefaultContext, &user_model.EmailAddress{
49-
Email: "user1234567890@example.com",
50-
LowerEmail: "user1234567890@example.com",
51-
IsPrimary: true,
52-
IsActivated: true,
53-
}))
54-
55-
// ErrEmailAlreadyUsed
56-
err := user_model.AddEmailAddress(db.DefaultContext, &user_model.EmailAddress{
57-
Email: "user1234567890@example.com",
58-
LowerEmail: "user1234567890@example.com",
59-
})
60-
assert.Error(t, err)
61-
assert.True(t, user_model.IsErrEmailAlreadyUsed(err))
62-
}
63-
64-
func TestAddEmailAddresses(t *testing.T) {
65-
assert.NoError(t, unittest.PrepareTestDatabase())
66-
67-
// insert multiple email address
68-
emails := make([]*user_model.EmailAddress, 2)
69-
emails[0] = &user_model.EmailAddress{
70-
Email: "user1234@example.com",
71-
LowerEmail: "user1234@example.com",
72-
IsActivated: true,
73-
}
74-
emails[1] = &user_model.EmailAddress{
75-
Email: "user5678@example.com",
76-
LowerEmail: "user5678@example.com",
77-
IsActivated: true,
78-
}
79-
assert.NoError(t, user_model.AddEmailAddresses(db.DefaultContext, emails))
80-
81-
// ErrEmailAlreadyUsed
82-
err := user_model.AddEmailAddresses(db.DefaultContext, emails)
83-
assert.Error(t, err)
84-
assert.True(t, user_model.IsErrEmailAlreadyUsed(err))
85-
}
86-
87-
func TestDeleteEmailAddress(t *testing.T) {
88-
assert.NoError(t, unittest.PrepareTestDatabase())
89-
90-
assert.NoError(t, user_model.DeleteEmailAddress(db.DefaultContext, &user_model.EmailAddress{
91-
UID: int64(1),
92-
ID: int64(33),
93-
Email: "user1-2@example.com",
94-
LowerEmail: "user1-2@example.com",
95-
}))
96-
97-
assert.NoError(t, user_model.DeleteEmailAddress(db.DefaultContext, &user_model.EmailAddress{
98-
UID: int64(1),
99-
Email: "user1-3@example.com",
100-
LowerEmail: "user1-3@example.com",
101-
}))
102-
103-
// Email address does not exist
104-
err := user_model.DeleteEmailAddress(db.DefaultContext, &user_model.EmailAddress{
105-
UID: int64(1),
106-
Email: "user1234567890@example.com",
107-
LowerEmail: "user1234567890@example.com",
108-
})
109-
assert.Error(t, err)
110-
}
111-
112-
func TestDeleteEmailAddresses(t *testing.T) {
113-
assert.NoError(t, unittest.PrepareTestDatabase())
114-
115-
// delete multiple email address
116-
emails := make([]*user_model.EmailAddress, 2)
117-
emails[0] = &user_model.EmailAddress{
118-
UID: int64(2),
119-
ID: int64(3),
120-
Email: "user2@example.com",
121-
LowerEmail: "user2@example.com",
122-
}
123-
emails[1] = &user_model.EmailAddress{
124-
UID: int64(2),
125-
Email: "user2-2@example.com",
126-
LowerEmail: "user2-2@example.com",
127-
}
128-
assert.NoError(t, user_model.DeleteEmailAddresses(db.DefaultContext, emails))
129-
130-
// ErrEmailAlreadyUsed
131-
err := user_model.DeleteEmailAddresses(db.DefaultContext, emails)
132-
assert.Error(t, err)
133-
}
134-
13545
func TestMakeEmailPrimary(t *testing.T) {
13646
assert.NoError(t, unittest.PrepareTestDatabase())
13747

0 commit comments

Comments
 (0)