Add a password policy for distillery users
This commit is contained in:
parent
ab9998881b
commit
6f257bd27f
9 changed files with 185 additions and 76 deletions
|
|
@ -18,7 +18,6 @@ var passwordTemplate = static.AssetsUser.MustParseShared("password.html", passwo
|
|||
|
||||
var (
|
||||
errPasswordsNotIdentical = errors.New("passwords are not identical")
|
||||
errPasswordIsEmpty = errors.New("password is empty")
|
||||
errCredentialsIncorrect = errors.New("credentials are not correct")
|
||||
errPasswordSetFailure = errors.New("error saving new password")
|
||||
errTOTPSetFailure = errors.New("unable to disable totp")
|
||||
|
|
@ -47,10 +46,6 @@ func (panel *UserPanel) routePassword(ctx context.Context) http.Handler {
|
|||
return struct{}{}, errPasswordsNotIdentical
|
||||
}
|
||||
|
||||
if new == "" {
|
||||
return struct{}{}, errPasswordIsEmpty
|
||||
}
|
||||
|
||||
user, err := panel.Dependencies.Auth.UserOf(r)
|
||||
if err != nil {
|
||||
return struct{}{}, err
|
||||
|
|
@ -62,6 +57,14 @@ func (panel *UserPanel) routePassword(ctx context.Context) http.Handler {
|
|||
return struct{}{}, errCredentialsIncorrect
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
err := user.CheckPasswordPolicy(new)
|
||||
if err != nil {
|
||||
return struct{}{}, err
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
err := user.SetPassword(r.Context(), []byte(new))
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -6,9 +6,11 @@ import (
|
|||
"encoding/base64"
|
||||
"fmt"
|
||||
"image/png"
|
||||
"strings"
|
||||
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/models"
|
||||
"github.com/FAU-CDI/wisski-distillery/pkg/password"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/pquerna/otp"
|
||||
"github.com/pquerna/otp/totp"
|
||||
|
|
@ -232,9 +234,49 @@ func (au *AuthUser) UnsetPassword(ctx context.Context) error {
|
|||
return au.Save(ctx)
|
||||
}
|
||||
|
||||
var ErrNoUser = errors.New("user is nil")
|
||||
var ErrUserDisabled = errors.New("user is disabled")
|
||||
var ErrUserBlank = errors.New("user has no password set")
|
||||
const MinPasswordLength = 8
|
||||
|
||||
var (
|
||||
ErrPolicyBlank = errors.New("password is blank")
|
||||
ErrPolicyTooShort = errors.New(fmt.Sprintf("password is too short: minimum length %d", MinPasswordLength))
|
||||
ErrPolicyKnown = errors.New("password is on the list of known passwords")
|
||||
ErrPolicyUsername = errors.New("password may not be identical to username")
|
||||
)
|
||||
|
||||
// CheckPasswordPolicy checks if the given password would pass the password policy.
|
||||
//
|
||||
// The password policy checks that the password has a minimum length of [MinPasswordLength]
|
||||
// and that it is not a common password.
|
||||
// It also checks that password and username are not identical.
|
||||
func (auth *Auth) CheckPasswordPolicy(candidate string, username string) error {
|
||||
if candidate == "" {
|
||||
return ErrPolicyBlank
|
||||
}
|
||||
|
||||
if strings.EqualFold(candidate, username) {
|
||||
return ErrPolicyUsername
|
||||
}
|
||||
|
||||
if len(candidate) < MinPasswordLength {
|
||||
return ErrPolicyTooShort
|
||||
}
|
||||
|
||||
if err := password.CheckCommonPassword(func(common string) (bool, error) { return common == candidate, nil }); err != nil {
|
||||
return ErrPolicyKnown
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (au *AuthUser) CheckPasswordPolicy(candidate string) error {
|
||||
return au.auth.CheckPasswordPolicy(candidate, au.User.User)
|
||||
}
|
||||
|
||||
var (
|
||||
ErrNoUser = errors.New("user is nil")
|
||||
ErrUserDisabled = errors.New("user is disabled")
|
||||
ErrUserBlank = errors.New("user has no password set")
|
||||
)
|
||||
|
||||
// CheckPassword checks if this user can login with the provided password.
|
||||
// Returns nil on success, an error otherwise.
|
||||
|
|
|
|||
|
|
@ -14,9 +14,20 @@
|
|||
{{ end }}
|
||||
|
||||
{{ define "content" }}
|
||||
|
||||
<div class="pure-u-1">
|
||||
<div class="padding">
|
||||
<div class="overflow">
|
||||
|
||||
{{ $E := .Error }}
|
||||
{{ if not (eq $E "") }}
|
||||
<div class="pure-form-group">
|
||||
<p class="error-message">
|
||||
{{ $E }}
|
||||
</p>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
<table class="pure-table pure-table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
_ "embed"
|
||||
|
||||
|
|
@ -24,14 +25,15 @@ var userTemplate = static.AssetsAdmin.MustParseShared(
|
|||
|
||||
type userContext struct {
|
||||
custom.BaseContext
|
||||
httpx.FormContext
|
||||
|
||||
Error string
|
||||
Users []*auth.AuthUser
|
||||
}
|
||||
|
||||
func (admin *Admin) users(r *http.Request) (uc userContext, err error) {
|
||||
admin.Dependencies.Custom.Update(&uc, r)
|
||||
|
||||
uc.Error = r.URL.Query().Get("error")
|
||||
uc.Users, err = admin.Dependencies.Auth.Users(r.Context())
|
||||
return
|
||||
}
|
||||
|
|
@ -78,6 +80,12 @@ func (admin *Admin) createUser(ctx context.Context) http.Handler {
|
|||
return cu, errCreateInvalidPassword
|
||||
}
|
||||
|
||||
// check the password policy
|
||||
err = admin.Dependencies.Auth.CheckPasswordPolicy(cu.Passsword, cu.User)
|
||||
if err != nil {
|
||||
return cu, err
|
||||
}
|
||||
|
||||
return cu, nil
|
||||
},
|
||||
|
||||
|
|
@ -144,7 +152,7 @@ func (admin *Admin) useraction(ctx context.Context, name string, action func(r *
|
|||
|
||||
if err := action(r, user); err != nil {
|
||||
logger.Err(err).Str("action", name).Msg("failed to act on user")
|
||||
httpx.HTMLInterceptor.Fallback.ServeHTTP(w, r)
|
||||
http.Redirect(w, r, "/admin/users/?error="+url.QueryEscape(err.Error()), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -185,6 +193,11 @@ func (admin *Admin) usersPasswordHandler(ctx context.Context) http.Handler {
|
|||
if password == "" {
|
||||
return httpx.ErrBadRequest
|
||||
}
|
||||
// check the password policy
|
||||
err := user.CheckPasswordPolicy(password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return user.SetPassword(r.Context(), []byte(password))
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue