Skip to content

Commit cc9943a

Browse files
authored
Merge branch 'master' into 13682-review-requested-filter
2 parents 05a71d1 + 9659808 commit cc9943a

File tree

19 files changed

+287
-77
lines changed

19 files changed

+287
-77
lines changed

cmd/admin.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -349,12 +349,11 @@ func runChangePassword(c *cli.Context) error {
349349
if err != nil {
350350
return err
351351
}
352-
if user.Salt, err = models.GetUserSalt(); err != nil {
352+
if err = user.SetPassword(c.String("password")); err != nil {
353353
return err
354354
}
355-
user.HashPassword(c.String("password"))
356355

357-
if err := models.UpdateUserCols(user, "passwd", "passwd_hash_algo", "salt"); err != nil {
356+
if err = models.UpdateUserCols(user, "passwd", "passwd_hash_algo", "salt"); err != nil {
358357
return err
359358
}
360359

integrations/admin_user_test.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Copyright 2021 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package integrations
6+
7+
import (
8+
"net/http"
9+
"strconv"
10+
"testing"
11+
12+
"code.gitea.io/gitea/models"
13+
14+
"github.com/stretchr/testify/assert"
15+
)
16+
17+
func TestAdminViewUsers(t *testing.T) {
18+
prepareTestEnv(t)
19+
20+
session := loginUser(t, "user1")
21+
req := NewRequest(t, "GET", "/admin/users")
22+
session.MakeRequest(t, req, http.StatusOK)
23+
24+
session = loginUser(t, "user2")
25+
req = NewRequest(t, "GET", "/admin/users")
26+
session.MakeRequest(t, req, http.StatusForbidden)
27+
}
28+
29+
func TestAdminViewUser(t *testing.T) {
30+
prepareTestEnv(t)
31+
32+
session := loginUser(t, "user1")
33+
req := NewRequest(t, "GET", "/admin/users/1")
34+
session.MakeRequest(t, req, http.StatusOK)
35+
36+
session = loginUser(t, "user2")
37+
req = NewRequest(t, "GET", "/admin/users/1")
38+
session.MakeRequest(t, req, http.StatusForbidden)
39+
}
40+
41+
func TestAdminEditUser(t *testing.T) {
42+
prepareTestEnv(t)
43+
44+
testSuccessfullEdit(t, models.User{ID: 2, Name: "newusername", LoginName: "otherlogin", Email: "[email protected]"})
45+
}
46+
47+
func testSuccessfullEdit(t *testing.T, formData models.User) {
48+
makeRequest(t, formData, http.StatusFound)
49+
}
50+
51+
func makeRequest(t *testing.T, formData models.User, headerCode int) {
52+
session := loginUser(t, "user1")
53+
csrf := GetCSRF(t, session, "/admin/users/"+strconv.Itoa(int(formData.ID)))
54+
req := NewRequestWithValues(t, "POST", "/admin/users/"+strconv.Itoa(int(formData.ID)), map[string]string{
55+
"_csrf": csrf,
56+
"user_name": formData.Name,
57+
"login_name": formData.LoginName,
58+
"login_type": "0-0",
59+
"email": formData.Email,
60+
})
61+
62+
session.MakeRequest(t, req, headerCode)
63+
user := models.AssertExistsAndLoadBean(t, &models.User{ID: formData.ID}).(*models.User)
64+
assert.Equal(t, formData.Name, user.Name)
65+
assert.Equal(t, formData.LoginName, user.LoginName)
66+
assert.Equal(t, formData.Email, user.Email)
67+
}
68+
69+
func TestAdminDeleteUser(t *testing.T) {
70+
defer prepareTestEnv(t)()
71+
72+
session := loginUser(t, "user1")
73+
74+
csrf := GetCSRF(t, session, "/admin/users/8")
75+
req := NewRequestWithValues(t, "POST", "/admin/users/8/delete", map[string]string{
76+
"_csrf": csrf,
77+
})
78+
session.MakeRequest(t, req, http.StatusOK)
79+
80+
assertUserDeleted(t, 8)
81+
models.CheckConsistencyFor(t, &models.User{})
82+
}

integrations/delete_user_test.go

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,21 +24,6 @@ func assertUserDeleted(t *testing.T, userID int64) {
2424
models.AssertNotExistsBean(t, &models.Star{UID: userID})
2525
}
2626

27-
func TestAdminDeleteUser(t *testing.T) {
28-
defer prepareTestEnv(t)()
29-
30-
session := loginUser(t, "user1")
31-
32-
csrf := GetCSRF(t, session, "/admin/users/8")
33-
req := NewRequestWithValues(t, "POST", "/admin/users/8/delete", map[string]string{
34-
"_csrf": csrf,
35-
})
36-
session.MakeRequest(t, req, http.StatusOK)
37-
38-
assertUserDeleted(t, 8)
39-
models.CheckConsistencyFor(t, &models.User{})
40-
}
41-
4227
func TestUserDeleteAccount(t *testing.T) {
4328
defer prepareTestEnv(t)()
4429

models/login_source.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -771,8 +771,10 @@ func UserSignIn(username, password string) (*User, error) {
771771

772772
// Update password hash if server password hash algorithm have changed
773773
if user.PasswdHashAlgo != setting.PasswordHashAlgo {
774-
user.HashPassword(password)
775-
if err := UpdateUserCols(user, "passwd", "passwd_hash_algo"); err != nil {
774+
if err = user.SetPassword(password); err != nil {
775+
return nil, err
776+
}
777+
if err = UpdateUserCols(user, "passwd", "passwd_hash_algo", "salt"); err != nil {
776778
return nil, err
777779
}
778780
}

models/migrations/migrations.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,8 @@ var migrations = []Migration{
277277
NewMigration("Add scope and nonce columns to oauth2_grant table", addScopeAndNonceColumnsToOAuth2Grant),
278278
// v165 -> v166
279279
NewMigration("Convert hook task type from char(16) to varchar(16) and trim the column", convertHookTaskTypeToVarcharAndTrim),
280+
// v166 -> v167
281+
NewMigration("Where Password is Valid with Empty String delete it", recalculateUserEmptyPWD),
280282
}
281283

282284
// GetCurrentDBVersion returns the current db version

models/migrations/v166.go

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
// Copyright 2021 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package migrations
6+
7+
import (
8+
"crypto/sha256"
9+
"fmt"
10+
11+
"golang.org/x/crypto/argon2"
12+
"golang.org/x/crypto/bcrypt"
13+
"golang.org/x/crypto/pbkdf2"
14+
"golang.org/x/crypto/scrypt"
15+
"xorm.io/builder"
16+
"xorm.io/xorm"
17+
)
18+
19+
func recalculateUserEmptyPWD(x *xorm.Engine) (err error) {
20+
const (
21+
algoBcrypt = "bcrypt"
22+
algoScrypt = "scrypt"
23+
algoArgon2 = "argon2"
24+
algoPbkdf2 = "pbkdf2"
25+
)
26+
27+
type User struct {
28+
ID int64 `xorm:"pk autoincr"`
29+
Passwd string `xorm:"NOT NULL"`
30+
PasswdHashAlgo string `xorm:"NOT NULL DEFAULT 'argon2'"`
31+
MustChangePassword bool `xorm:"NOT NULL DEFAULT false"`
32+
LoginType int
33+
LoginName string
34+
Type int
35+
Salt string `xorm:"VARCHAR(10)"`
36+
}
37+
38+
// hashPassword hash password based on algo and salt
39+
// state 461406070c
40+
hashPassword := func(passwd, salt, algo string) string {
41+
var tempPasswd []byte
42+
43+
switch algo {
44+
case algoBcrypt:
45+
tempPasswd, _ = bcrypt.GenerateFromPassword([]byte(passwd), bcrypt.DefaultCost)
46+
return string(tempPasswd)
47+
case algoScrypt:
48+
tempPasswd, _ = scrypt.Key([]byte(passwd), []byte(salt), 65536, 16, 2, 50)
49+
case algoArgon2:
50+
tempPasswd = argon2.IDKey([]byte(passwd), []byte(salt), 2, 65536, 8, 50)
51+
case algoPbkdf2:
52+
fallthrough
53+
default:
54+
tempPasswd = pbkdf2.Key([]byte(passwd), []byte(salt), 10000, 50, sha256.New)
55+
}
56+
57+
return fmt.Sprintf("%x", tempPasswd)
58+
}
59+
60+
// ValidatePassword checks if given password matches the one belongs to the user.
61+
// state 461406070c, changed since it's not necessary to be time constant
62+
ValidatePassword := func(u *User, passwd string) bool {
63+
tempHash := hashPassword(passwd, u.Salt, u.PasswdHashAlgo)
64+
65+
if u.PasswdHashAlgo != algoBcrypt && u.Passwd == tempHash {
66+
return true
67+
}
68+
if u.PasswdHashAlgo == algoBcrypt && bcrypt.CompareHashAndPassword([]byte(u.Passwd), []byte(passwd)) == nil {
69+
return true
70+
}
71+
return false
72+
}
73+
74+
sess := x.NewSession()
75+
defer sess.Close()
76+
77+
const batchSize = 100
78+
79+
for start := 0; ; start += batchSize {
80+
users := make([]*User, 0, batchSize)
81+
if err = sess.Limit(batchSize, start).Where(builder.Neq{"passwd": ""}, 0).Find(&users); err != nil {
82+
return
83+
}
84+
if len(users) == 0 {
85+
break
86+
}
87+
88+
if err = sess.Begin(); err != nil {
89+
return
90+
}
91+
92+
for _, user := range users {
93+
if ValidatePassword(user, "") {
94+
user.Passwd = ""
95+
user.Salt = ""
96+
user.PasswdHashAlgo = ""
97+
if _, err = sess.ID(user.ID).Cols("passwd", "salt", "passwd_hash_algo").Update(user); err != nil {
98+
return err
99+
}
100+
}
101+
}
102+
103+
if err = sess.Commit(); err != nil {
104+
return
105+
}
106+
}
107+
108+
// delete salt and algo where password is empty
109+
if _, err = sess.Where(builder.Eq{"passwd": ""}.And(builder.Neq{"salt": ""}.Or(builder.Neq{"passwd_hash_algo": ""}))).
110+
Cols("salt", "passwd_hash_algo").Update(&User{}); err != nil {
111+
return err
112+
}
113+
114+
return sess.Commit()
115+
}

models/user.go

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -395,10 +395,23 @@ func hashPassword(passwd, salt, algo string) string {
395395
return fmt.Sprintf("%x", tempPasswd)
396396
}
397397

398-
// HashPassword hashes a password using the algorithm defined in the config value of PASSWORD_HASH_ALGO.
399-
func (u *User) HashPassword(passwd string) {
398+
// SetPassword hashes a password using the algorithm defined in the config value of PASSWORD_HASH_ALGO
399+
// change passwd, salt and passwd_hash_algo fields
400+
func (u *User) SetPassword(passwd string) (err error) {
401+
if len(passwd) == 0 {
402+
u.Passwd = ""
403+
u.Salt = ""
404+
u.PasswdHashAlgo = ""
405+
return nil
406+
}
407+
408+
if u.Salt, err = GetUserSalt(); err != nil {
409+
return err
410+
}
400411
u.PasswdHashAlgo = setting.PasswordHashAlgo
401412
u.Passwd = hashPassword(passwd, u.Salt, setting.PasswordHashAlgo)
413+
414+
return nil
402415
}
403416

404417
// ValidatePassword checks if given password matches the one belongs to the user.
@@ -416,7 +429,7 @@ func (u *User) ValidatePassword(passwd string) bool {
416429

417430
// IsPasswordSet checks if the password is set or left empty
418431
func (u *User) IsPasswordSet() bool {
419-
return !u.ValidatePassword("")
432+
return len(u.Passwd) != 0
420433
}
421434

422435
// IsOrganization returns true if user is actually a organization.
@@ -826,10 +839,9 @@ func CreateUser(u *User) (err error) {
826839
if u.Rands, err = GetUserSalt(); err != nil {
827840
return err
828841
}
829-
if u.Salt, err = GetUserSalt(); err != nil {
842+
if err = u.SetPassword(u.Passwd); err != nil {
830843
return err
831844
}
832-
u.HashPassword(u.Passwd)
833845
u.AllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization && !setting.Admin.DisableRegularOrgCreation
834846
u.EmailNotificationsPreference = setting.Admin.DefaultEmailNotification
835847
u.MaxRepoCreation = -1
@@ -913,19 +925,19 @@ func ChangeUserName(u *User, newUserName string) (err error) {
913925
return err
914926
}
915927

916-
isExist, err := IsUserExist(0, newUserName)
917-
if err != nil {
918-
return err
919-
} else if isExist {
920-
return ErrUserAlreadyExist{newUserName}
921-
}
922-
923928
sess := x.NewSession()
924929
defer sess.Close()
925930
if err = sess.Begin(); err != nil {
926931
return err
927932
}
928933

934+
isExist, err := isUserExist(sess, 0, newUserName)
935+
if err != nil {
936+
return err
937+
} else if isExist {
938+
return ErrUserAlreadyExist{newUserName}
939+
}
940+
929941
if _, err = sess.Exec("UPDATE `repository` SET owner_name=? WHERE owner_name=?", newUserName, u.Name); err != nil {
930942
return fmt.Errorf("Change repo owner name: %v", err)
931943
}

models/user_test.go

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -220,8 +220,7 @@ func TestEmailNotificationPreferences(t *testing.T) {
220220

221221
func TestHashPasswordDeterministic(t *testing.T) {
222222
b := make([]byte, 16)
223-
rand.Read(b)
224-
u := &User{Salt: string(b)}
223+
u := &User{}
225224
algos := []string{"argon2", "pbkdf2", "scrypt", "bcrypt"}
226225
for j := 0; j < len(algos); j++ {
227226
u.PasswdHashAlgo = algos[j]
@@ -231,19 +230,15 @@ func TestHashPasswordDeterministic(t *testing.T) {
231230
pass := string(b)
232231

233232
// save the current password in the user - hash it and store the result
234-
u.HashPassword(pass)
233+
u.SetPassword(pass)
235234
r1 := u.Passwd
236235

237236
// run again
238-
u.HashPassword(pass)
237+
u.SetPassword(pass)
239238
r2 := u.Passwd
240239

241-
// assert equal (given the same salt+pass, the same result is produced) except bcrypt
242-
if u.PasswdHashAlgo == "bcrypt" {
243-
assert.NotEqual(t, r1, r2)
244-
} else {
245-
assert.Equal(t, r1, r2)
246-
}
240+
assert.NotEqual(t, r1, r2)
241+
assert.True(t, u.ValidatePassword(pass))
247242
}
248243
}
249244
}
@@ -252,12 +247,10 @@ func BenchmarkHashPassword(b *testing.B) {
252247
// BenchmarkHashPassword ensures that it takes a reasonable amount of time
253248
// to hash a password - in order to protect from brute-force attacks.
254249
pass := "password1337"
255-
bs := make([]byte, 16)
256-
rand.Read(bs)
257-
u := &User{Salt: string(bs), Passwd: pass}
250+
u := &User{Passwd: pass}
258251
b.ResetTimer()
259252
for i := 0; i < b.N; i++ {
260-
u.HashPassword(pass)
253+
u.SetPassword(pass)
261254
}
262255
}
263256

modules/auth/admin.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ func (f *AdminCreateUserForm) Validate(ctx *macaron.Context, errs binding.Errors
2828
// AdminEditUserForm form for admin to create user
2929
type AdminEditUserForm struct {
3030
LoginType string `binding:"Required"`
31+
UserName string `binding:"AlphaDashDot;MaxSize(40)"`
3132
LoginName string
3233
FullName string `binding:"MaxSize(100)"`
3334
Email string `binding:"Required;Email;MaxSize(254)"`

options/locale/locale_en-US.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,7 @@ password_not_match = The passwords do not match.
359359
lang_select_error = Select a language from the list.
360360
361361
username_been_taken = The username is already taken.
362+
username_change_not_local_user = Non-local users are not allowed to change their username.
362363
repo_name_been_taken = The repository name is already used.
363364
repository_files_already_exist = Files already exist for this repository. Contact the system administrator.
364365
repository_files_already_exist.adopt = Files already exist for this repository and can only be Adopted.

0 commit comments

Comments
 (0)