forked from forgejo/forgejo
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.
This commit is contained in:
parent
b4513f48ce
commit
f8b471ace1
42 changed files with 1383 additions and 1068 deletions
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
}
|
||||
|
||||
return count > 0, nil
|
||||
if count > 0 {
|
||||
return ErrIsPwned
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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/
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue