diff options
author | KN4CK3R <admin@oldschoolhack.me> | 2024-02-04 14:29:09 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-02-04 13:29:09 +0000 |
commit | f8b471ace1b59bd3fc3a04c9ddb5f62dd1dd5396 (patch) | |
tree | 371d8477bd0cd4e41881b91fdb670eb33e38e8b1 /modules | |
parent | b4513f48ce3e748ac621a6f3ddf082feca67e938 (diff) | |
download | gitea-f8b471ace1b59bd3fc3a04c9ddb5f62dd1dd5396.tar.gz gitea-f8b471ace1b59bd3fc3a04c9ddb5f62dd1dd5396.zip |
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.
Diffstat (limited to 'modules')
-rw-r--r-- | modules/auth/password/password.go | 17 | ||||
-rw-r--r-- | modules/auth/password/pwn.go | 36 | ||||
-rw-r--r-- | modules/auth/password/pwn/pwn.go | 2 | ||||
-rw-r--r-- | modules/auth/password/pwn/pwn_test.go | 83 | ||||
-rw-r--r-- | modules/optional/option.go | 45 | ||||
-rw-r--r-- | modules/optional/option_test.go | 48 |
6 files changed, 162 insertions, 69 deletions
diff --git a/modules/auth/password/password.go b/modules/auth/password/password.go index 2172dc8b44..2c7205b708 100644 --- a/modules/auth/password/password.go +++ b/modules/auth/password/password.go @@ -5,8 +5,9 @@ package password import ( "bytes" - goContext "context" + "context" "crypto/rand" + "errors" "math/big" "strings" "sync" @@ -15,6 +16,11 @@ import ( "code.gitea.io/gitea/modules/translation" ) +var ( + ErrComplexity = errors.New("password not complex enough") + ErrMinLength = errors.New("password not long enough") +) + // complexity contains information about a particular kind of password complexity type complexity struct { ValidChars string @@ -101,11 +107,14 @@ func Generate(n int) (string, error) { } buffer[j] = validChars[rnd.Int64()] } - pwned, err := IsPwned(goContext.Background(), string(buffer)) - if err != nil { + + if err := IsPwned(context.Background(), string(buffer)); err != nil { + if errors.Is(err, ErrIsPwned) { + continue + } return "", err } - if IsComplexEnough(string(buffer)) && !pwned && string(buffer[0]) != " " && string(buffer[n-1]) != " " { + if IsComplexEnough(string(buffer)) && string(buffer[0]) != " " && string(buffer[n-1]) != " " { return string(buffer), nil } } diff --git a/modules/auth/password/pwn.go b/modules/auth/password/pwn.go index df425ac659..e00205ea19 100644 --- a/modules/auth/password/pwn.go +++ b/modules/auth/password/pwn.go @@ -5,24 +5,48 @@ package password import ( "context" + "errors" + "fmt" "code.gitea.io/gitea/modules/auth/password/pwn" "code.gitea.io/gitea/modules/setting" ) +var ErrIsPwned = errors.New("password has been pwned") + +type ErrIsPwnedRequest struct { + err error +} + +func IsErrIsPwnedRequest(err error) bool { + _, ok := err.(ErrIsPwnedRequest) + return ok +} + +func (err ErrIsPwnedRequest) Error() string { + return fmt.Sprintf("using Have-I-Been-Pwned service failed: %v", err.err) +} + +func (err ErrIsPwnedRequest) Unwrap() error { + return err.err +} + // IsPwned checks whether a password has been pwned -// NOTE: This func returns true if it encounters an error under the assumption that you ALWAYS want to check against -// HIBP, so not getting a response should block a password until it can be verified. -func IsPwned(ctx context.Context, password string) (bool, error) { +// If a password has not been pwned, no error is returned. +func IsPwned(ctx context.Context, password string) error { if !setting.PasswordCheckPwn { - return false, nil + return nil } client := pwn.New(pwn.WithContext(ctx)) count, err := client.CheckPassword(password, true) if err != nil { - return true, err + return ErrIsPwnedRequest{err} + } + + if count > 0 { + return ErrIsPwned } - return count > 0, nil + return nil } diff --git a/modules/auth/password/pwn/pwn.go b/modules/auth/password/pwn/pwn.go index b5a015fb9c..f77ce9f40b 100644 --- a/modules/auth/password/pwn/pwn.go +++ b/modules/auth/password/pwn/pwn.go @@ -73,7 +73,7 @@ func newRequest(ctx context.Context, method, url string, body io.ReadCloser) (*h // because artificial responses will be added to the response // For more information, see https://www.troyhunt.com/enhancing-pwned-passwords-privacy-with-padding/ func (c *Client) CheckPassword(pw string, padding bool) (int, error) { - if strings.TrimSpace(pw) == "" { + if pw == "" { return -1, ErrEmptyPassword } diff --git a/modules/auth/password/pwn/pwn_test.go b/modules/auth/password/pwn/pwn_test.go index 148208b964..f9deadc8d7 100644 --- a/modules/auth/password/pwn/pwn_test.go +++ b/modules/auth/password/pwn/pwn_test.go @@ -4,13 +4,14 @@ package pwn import ( - "errors" "math/rand" "net/http" "os" "strings" "testing" "time" + + "github.com/stretchr/testify/assert" ) var client = New(WithHTTP(&http.Client{ @@ -25,78 +26,44 @@ func TestMain(m *testing.M) { func TestPassword(t *testing.T) { // Check input error _, err := client.CheckPassword("", false) - if err == nil { - t.Log("blank input should return an error") - t.Fail() - } - if !errors.Is(err, ErrEmptyPassword) { - t.Log("blank input should return ErrEmptyPassword") - t.Fail() - } + assert.ErrorIs(t, err, ErrEmptyPassword, "blank input should return ErrEmptyPassword") // Should fail fail := "password1234" count, err := client.CheckPassword(fail, false) - if err != nil { - t.Log(err) - t.Fail() - } - if count == 0 { - t.Logf("%s should fail as a password\n", fail) - t.Fail() - } + assert.NotEmpty(t, count, "%s should fail as a password", fail) + assert.NoError(t, err) // Should fail (with padding) failPad := "administrator" count, err = client.CheckPassword(failPad, true) - if err != nil { - t.Log(err) - t.Fail() - } - if count == 0 { - t.Logf("%s should fail as a password\n", failPad) - t.Fail() - } + assert.NotEmpty(t, count, "%s should fail as a password", failPad) + assert.NoError(t, err) // Checking for a "good" password isn't going to be perfect, but we can give it a good try // with hopefully minimal error. Try five times? - var good bool - var pw string - for idx := 0; idx <= 5; idx++ { - pw = testPassword() - count, err = client.CheckPassword(pw, false) - if err != nil { - t.Log(err) - t.Fail() + assert.Condition(t, func() bool { + for i := 0; i <= 5; i++ { + count, err = client.CheckPassword(testPassword(), false) + assert.NoError(t, err) + if count == 0 { + return true + } } - if count == 0 { - good = true - break - } - } - if !good { - t.Log("no generated passwords passed. there is a chance this is a fluke") - t.Fail() - } + return false + }, "no generated passwords passed. there is a chance this is a fluke") // Again, but with padded responses - good = false - for idx := 0; idx <= 5; idx++ { - pw = testPassword() - count, err = client.CheckPassword(pw, true) - if err != nil { - t.Log(err) - t.Fail() + assert.Condition(t, func() bool { + for i := 0; i <= 5; i++ { + count, err = client.CheckPassword(testPassword(), true) + assert.NoError(t, err) + if count == 0 { + return true + } } - if count == 0 { - good = true - break - } - } - if !good { - t.Log("no generated passwords passed. there is a chance this is a fluke") - t.Fail() - } + return false + }, "no generated passwords passed. there is a chance this is a fluke") } // Credit to https://golangbyexample.com/generate-random-password-golang/ diff --git a/modules/optional/option.go b/modules/optional/option.go new file mode 100644 index 0000000000..af9e5ac852 --- /dev/null +++ b/modules/optional/option.go @@ -0,0 +1,45 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package optional + +type Option[T any] []T + +func None[T any]() Option[T] { + return nil +} + +func Some[T any](v T) Option[T] { + return Option[T]{v} +} + +func FromPtr[T any](v *T) Option[T] { + if v == nil { + return None[T]() + } + return Some(*v) +} + +func FromNonDefault[T comparable](v T) Option[T] { + var zero T + if v == zero { + return None[T]() + } + return Some(v) +} + +func (o Option[T]) Has() bool { + return o != nil +} + +func (o Option[T]) Value() T { + var zero T + return o.ValueOrDefault(zero) +} + +func (o Option[T]) ValueOrDefault(v T) T { + if o.Has() { + return o[0] + } + return v +} diff --git a/modules/optional/option_test.go b/modules/optional/option_test.go new file mode 100644 index 0000000000..7ec345b6ba --- /dev/null +++ b/modules/optional/option_test.go @@ -0,0 +1,48 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package optional + +import ( + "testing" + + "code.gitea.io/gitea/modules/util" + + "github.com/stretchr/testify/assert" +) + +func TestOption(t *testing.T) { + var uninitialized Option[int] + assert.False(t, uninitialized.Has()) + assert.Equal(t, int(0), uninitialized.Value()) + assert.Equal(t, int(1), uninitialized.ValueOrDefault(1)) + + none := None[int]() + assert.False(t, none.Has()) + assert.Equal(t, int(0), none.Value()) + assert.Equal(t, int(1), none.ValueOrDefault(1)) + + some := Some[int](1) + assert.True(t, some.Has()) + assert.Equal(t, int(1), some.Value()) + assert.Equal(t, int(1), some.ValueOrDefault(2)) + + var ptr *int + assert.False(t, FromPtr(ptr).Has()) + + opt1 := FromPtr(util.ToPointer(1)) + assert.True(t, opt1.Has()) + assert.Equal(t, int(1), opt1.Value()) + + assert.False(t, FromNonDefault("").Has()) + + opt2 := FromNonDefault("test") + assert.True(t, opt2.Has()) + assert.Equal(t, "test", opt2.Value()) + + assert.False(t, FromNonDefault(0).Has()) + + opt3 := FromNonDefault(1) + assert.True(t, opt3.Has()) + assert.Equal(t, int(1), opt3.Value()) +} |