Merge pull request #326 from openziti/v0.4.0_password_requirements

Enhanced password requirements and relevant ui changes (#167)
This commit is contained in:
Michael Quigley 2023-05-23 14:10:49 -04:00 committed by GitHub
commit ee17f2f3d3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 726 additions and 78 deletions

View File

@ -10,6 +10,8 @@ FEATURE: New limits implementation based on the new metrics infrastructure (http
FEATURE: The invite mechanism has been reworked to improve user experience. The configuration has been moved to the `admin` stanza of the controller configuration and now includes a boolean flag indicating whether or not the instance allows new invitations to be created, and also includes contact details for requesting a new invite. These values are used by the `zrok invite` command to provide a smoother end-user invite experience https://github.com/openziti/zrok/issues/229) FEATURE: The invite mechanism has been reworked to improve user experience. The configuration has been moved to the `admin` stanza of the controller configuration and now includes a boolean flag indicating whether or not the instance allows new invitations to be created, and also includes contact details for requesting a new invite. These values are used by the `zrok invite` command to provide a smoother end-user invite experience https://github.com/openziti/zrok/issues/229)
FEATURE: New password strength checking rules and configuration. See the example configuration file (`etc/ctrl.yml`) for details about how to configure the strength checking rules (https://github.com/openziti/zrok/issues/167)
CHANGE: The controller configuration version bumps from `v: 2` to `v: 3` to support all of the new `v0.4` functionality. See the [example ctrl.yml](etc/ctrl.yml) for details on the new configuration. CHANGE: The controller configuration version bumps from `v: 2` to `v: 3` to support all of the new `v0.4` functionality. See the [example ctrl.yml](etc/ctrl.yml) for details on the new configuration.
CHANGE: The underlying database store now utilizes a `deleted` flag on all tables to implement "soft deletes". This was necessary for the new metrics infrastructure, where we need to account for metrics data that arrived after the lifetime of a share or environment; and also we're going to need this for limits, where we need to see historical information about activity in the past (https://github.com/openziti/zrok/issues/262) CHANGE: The underlying database store now utilizes a `deleted` flag on all tables to implement "soft deletes". This was necessary for the new metrics infrastructure, where we need to account for metrics data that arrived after the lifetime of a share or environment; and also we're going to need this for limits, where we need to see historical information about activity in the past (https://github.com/openziti/zrok/issues/262)

View File

@ -1,12 +1,13 @@
package config package config
import ( import (
"time"
"github.com/openziti/zrok/controller/emailUi" "github.com/openziti/zrok/controller/emailUi"
"github.com/openziti/zrok/controller/env" "github.com/openziti/zrok/controller/env"
"github.com/openziti/zrok/controller/limits" "github.com/openziti/zrok/controller/limits"
"github.com/openziti/zrok/controller/metrics" "github.com/openziti/zrok/controller/metrics"
"github.com/openziti/zrok/controller/zrokEdgeSdk" "github.com/openziti/zrok/controller/zrokEdgeSdk"
"time"
"github.com/michaelquigley/cf" "github.com/michaelquigley/cf"
"github.com/openziti/zrok/controller/store" "github.com/openziti/zrok/controller/store"
@ -24,6 +25,7 @@ type Config struct {
Limits *limits.Config Limits *limits.Config
Maintenance *MaintenanceConfig Maintenance *MaintenanceConfig
Metrics *metrics.Config Metrics *metrics.Config
Passwords *PasswordsConfig
Registration *RegistrationConfig Registration *RegistrationConfig
ResetPassword *ResetPasswordConfig ResetPassword *ResetPasswordConfig
Store *store.Config Store *store.Config
@ -31,11 +33,11 @@ type Config struct {
} }
type AdminConfig struct { type AdminConfig struct {
Secrets []string `cf:"+secret"`
TouLink string
InvitesOpen bool InvitesOpen bool
InviteTokenStrategy string InviteTokenStrategy string
InviteTokenContact string InviteTokenContact string
Secrets []string `cf:"+secret"`
TouLink string
} }
type EndpointConfig struct { type EndpointConfig struct {
@ -43,6 +45,19 @@ type EndpointConfig struct {
Port int Port int
} }
type MaintenanceConfig struct {
ResetPassword *ResetPasswordMaintenanceConfig
Registration *RegistrationMaintenanceConfig
}
type PasswordsConfig struct {
Length int
RequireCapital bool
RequireNumeric bool
RequireSpecial bool
ValidSpecialCharacters string
}
type RegistrationConfig struct { type RegistrationConfig struct {
RegistrationUrlTemplate string RegistrationUrlTemplate string
} }
@ -51,11 +66,6 @@ type ResetPasswordConfig struct {
ResetUrlTemplate string ResetUrlTemplate string
} }
type MaintenanceConfig struct {
ResetPassword *ResetPasswordMaintenanceConfig
Registration *RegistrationMaintenanceConfig
}
type RegistrationMaintenanceConfig struct { type RegistrationMaintenanceConfig struct {
ExpirationTimeout time.Duration ExpirationTimeout time.Duration
CheckFrequency time.Duration CheckFrequency time.Duration
@ -83,6 +93,13 @@ func DefaultConfig() *Config {
BatchLimit: 500, BatchLimit: 500,
}, },
}, },
Passwords: &PasswordsConfig{
Length: 8,
RequireCapital: true,
RequireNumeric: true,
RequireSpecial: true,
ValidSpecialCharacters: `!@$&*_-., "#%'()+/:;<=>?[\]^{|}~`,
},
} }
} }

View File

@ -27,6 +27,15 @@ func (ch *configurationHandler) Handle(_ metadata.ConfigurationParams) middlewar
if cfg.Admin != nil { if cfg.Admin != nil {
data.TouLink = cfg.Admin.TouLink data.TouLink = cfg.Admin.TouLink
data.InviteTokenContact = cfg.Admin.InviteTokenContact data.InviteTokenContact = cfg.Admin.InviteTokenContact
if cfg.Passwords != nil {
data.PasswordRequirements = &rest_model_zrok.PasswordRequirements{
Length: int64(cfg.Passwords.Length),
RequireCapital: cfg.Passwords.RequireCapital,
RequireNumeric: cfg.Passwords.RequireNumeric,
RequireSpecial: cfg.Passwords.RequireSpecial,
ValidSpecialCharacters: cfg.Passwords.ValidSpecialCharacters,
}
}
} }
return metadata.NewConfigurationOK().WithPayload(data) return metadata.NewConfigurationOK().WithPayload(data)
} }

View File

@ -2,6 +2,7 @@ package controller
import ( import (
"context" "context"
"github.com/openziti/zrok/controller/config" "github.com/openziti/zrok/controller/config"
"github.com/openziti/zrok/controller/limits" "github.com/openziti/zrok/controller/limits"
"github.com/openziti/zrok/controller/metrics" "github.com/openziti/zrok/controller/metrics"
@ -34,8 +35,8 @@ func Run(inCfg *config.Config) error {
api.KeyAuth = newZrokAuthenticator(cfg).authenticate api.KeyAuth = newZrokAuthenticator(cfg).authenticate
api.AccountInviteHandler = newInviteHandler(cfg) api.AccountInviteHandler = newInviteHandler(cfg)
api.AccountLoginHandler = account.LoginHandlerFunc(loginHandler) api.AccountLoginHandler = account.LoginHandlerFunc(loginHandler)
api.AccountRegisterHandler = newRegisterHandler() api.AccountRegisterHandler = newRegisterHandler(cfg)
api.AccountResetPasswordHandler = newResetPasswordHandler() api.AccountResetPasswordHandler = newResetPasswordHandler(cfg)
api.AccountResetPasswordRequestHandler = newResetPasswordRequestHandler() api.AccountResetPasswordRequestHandler = newResetPasswordRequestHandler()
api.AccountVerifyHandler = newVerifyHandler() api.AccountVerifyHandler = newVerifyHandler()
api.AdminCreateFrontendHandler = newCreateFrontendHandler() api.AdminCreateFrontendHandler = newCreateFrontendHandler()

View File

@ -2,16 +2,21 @@ package controller
import ( import (
"github.com/go-openapi/runtime/middleware" "github.com/go-openapi/runtime/middleware"
"github.com/openziti/zrok/controller/config"
"github.com/openziti/zrok/controller/store" "github.com/openziti/zrok/controller/store"
"github.com/openziti/zrok/rest_model_zrok" "github.com/openziti/zrok/rest_model_zrok"
"github.com/openziti/zrok/rest_server_zrok/operations/account" "github.com/openziti/zrok/rest_server_zrok/operations/account"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
type registerHandler struct{} type registerHandler struct {
cfg *config.Config
}
func newRegisterHandler() *registerHandler { func newRegisterHandler(cfg *config.Config) *registerHandler {
return &registerHandler{} return &registerHandler{
cfg: cfg,
}
} }
func (h *registerHandler) Handle(params account.RegisterParams) middleware.Responder { func (h *registerHandler) Handle(params account.RegisterParams) middleware.Responder {
if params.Body == nil || params.Body.Token == "" || params.Body.Password == "" { if params.Body == nil || params.Body.Token == "" || params.Body.Password == "" {
@ -38,6 +43,12 @@ func (h *registerHandler) Handle(params account.RegisterParams) middleware.Respo
logrus.Errorf("error creating token for request '%v' (%v): %v", params.Body.Token, ar.Email, err) logrus.Errorf("error creating token for request '%v' (%v): %v", params.Body.Token, ar.Email, err)
return account.NewRegisterInternalServerError() return account.NewRegisterInternalServerError()
} }
if err := validatePassword(h.cfg, params.Body.Password); err != nil {
logrus.Errorf("password not valid for request '%v', (%v): %v", params.Body.Token, ar.Email, err)
return account.NewRegisterUnprocessableEntity().WithPayload(rest_model_zrok.ErrorMessage(err.Error()))
}
hpwd, err := hashPassword(params.Body.Password) hpwd, err := hashPassword(params.Body.Password)
if err != nil { if err != nil {
logrus.Errorf("error hashing password for request '%v' (%v): %v", params.Body.Token, ar.Email, err) logrus.Errorf("error hashing password for request '%v' (%v): %v", params.Body.Token, ar.Email, err)

View File

@ -2,14 +2,20 @@ package controller
import ( import (
"github.com/go-openapi/runtime/middleware" "github.com/go-openapi/runtime/middleware"
"github.com/openziti/zrok/controller/config"
"github.com/openziti/zrok/rest_model_zrok"
"github.com/openziti/zrok/rest_server_zrok/operations/account" "github.com/openziti/zrok/rest_server_zrok/operations/account"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
type resetPasswordHandler struct{} type resetPasswordHandler struct {
cfg *config.Config
}
func newResetPasswordHandler() *resetPasswordHandler { func newResetPasswordHandler(cfg *config.Config) *resetPasswordHandler {
return &resetPasswordHandler{} return &resetPasswordHandler{
cfg: cfg,
}
} }
func (handler *resetPasswordHandler) Handle(params account.ResetPasswordParams) middleware.Responder { func (handler *resetPasswordHandler) Handle(params account.ResetPasswordParams) middleware.Responder {
@ -41,6 +47,12 @@ func (handler *resetPasswordHandler) Handle(params account.ResetPasswordParams)
logrus.Errorf("account '%v' for '%v' deleted", a.Email, a.Token) logrus.Errorf("account '%v' for '%v' deleted", a.Email, a.Token)
return account.NewResetPasswordNotFound() return account.NewResetPasswordNotFound()
} }
if err := validatePassword(handler.cfg, params.Body.Password); err != nil {
logrus.Errorf("password not valid for request '%v', (%v): %v", params.Body.Token, a.Email, err)
return account.NewResetPasswordUnprocessableEntity().WithPayload(rest_model_zrok.ErrorMessage(err.Error()))
}
hpwd, err := hashPassword(params.Body.Password) hpwd, err := hashPassword(params.Body.Password)
if err != nil { if err != nil {
logrus.Errorf("error hashing password for '%v' (%v): %v", params.Body.Token, a.Email, err) logrus.Errorf("error hashing password for '%v' (%v): %v", params.Body.Token, a.Email, err)

View File

@ -1,13 +1,16 @@
package controller package controller
import ( import (
"fmt"
"net/http"
"strings"
"unicode"
errors2 "github.com/go-openapi/errors" errors2 "github.com/go-openapi/errors"
"github.com/jaevor/go-nanoid" "github.com/jaevor/go-nanoid"
"github.com/openziti/zrok/controller/config" "github.com/openziti/zrok/controller/config"
"github.com/openziti/zrok/rest_model_zrok" "github.com/openziti/zrok/rest_model_zrok"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"net/http"
"strings"
) )
type zrokAuthenticator struct { type zrokAuthenticator struct {
@ -87,3 +90,43 @@ func realRemoteAddress(req *http.Request) string {
func proxyUrl(shrToken, template string) string { func proxyUrl(shrToken, template string) string {
return strings.Replace(template, "{token}", shrToken, -1) return strings.Replace(template, "{token}", shrToken, -1)
} }
func validatePassword(cfg *config.Config, password string) error {
if cfg.Passwords.Length > len(password) {
return fmt.Errorf("password length: expected (%d), got (%d)", cfg.Passwords.Length, len(password))
}
if cfg.Passwords.RequireCapital {
if !hasCapital(password) {
return fmt.Errorf("password requires capital, found none")
}
}
if cfg.Passwords.RequireNumeric {
if !hasNumeric(password) {
return fmt.Errorf("password requires numeric, found none")
}
}
if cfg.Passwords.RequireSpecial {
if !strings.ContainsAny(password, cfg.Passwords.ValidSpecialCharacters) {
return fmt.Errorf("password requires special character, found none")
}
}
return nil
}
func hasCapital(check string) bool {
for _, c := range check {
if unicode.IsUpper(c) {
return true
}
}
return false
}
func hasNumeric(check string) bool {
for _, c := range check {
if unicode.IsDigit(c) {
return true
}
}
return false
}

View File

@ -140,6 +140,16 @@ metrics:
org: zrok org: zrok
token: "<INFLUX TOKEN>" token: "<INFLUX TOKEN>"
# Configure password requirements for user accounts.
#
#passwords:
# length: 8
# require_capital: true
# require_numeric: true
# require_special: true
# # Denote which characters satisfy the `require_special` requirement. Note the need to escape specific characters.
# valid_special_characters: "\"\\`'~!@#$%^&*()[],./"
# Configure the generated URL for the registration email. The registration token will be appended to this URL. # Configure the generated URL for the registration email. The registration token will be appended to this URL.
# #
registration: registration:

View File

@ -35,6 +35,12 @@ func (o *RegisterReader) ReadResponse(response runtime.ClientResponse, consumer
return nil, err return nil, err
} }
return nil, result return nil, result
case 422:
result := NewRegisterUnprocessableEntity()
if err := result.readResponse(response, consumer, o.formats); err != nil {
return nil, err
}
return nil, result
case 500: case 500:
result := NewRegisterInternalServerError() result := NewRegisterInternalServerError()
if err := result.readResponse(response, consumer, o.formats); err != nil { if err := result.readResponse(response, consumer, o.formats); err != nil {
@ -160,6 +166,67 @@ func (o *RegisterNotFound) readResponse(response runtime.ClientResponse, consume
return nil return nil
} }
// NewRegisterUnprocessableEntity creates a RegisterUnprocessableEntity with default headers values
func NewRegisterUnprocessableEntity() *RegisterUnprocessableEntity {
return &RegisterUnprocessableEntity{}
}
/*
RegisterUnprocessableEntity describes a response with status code 422, with default header values.
password validation failure
*/
type RegisterUnprocessableEntity struct {
Payload rest_model_zrok.ErrorMessage
}
// IsSuccess returns true when this register unprocessable entity response has a 2xx status code
func (o *RegisterUnprocessableEntity) IsSuccess() bool {
return false
}
// IsRedirect returns true when this register unprocessable entity response has a 3xx status code
func (o *RegisterUnprocessableEntity) IsRedirect() bool {
return false
}
// IsClientError returns true when this register unprocessable entity response has a 4xx status code
func (o *RegisterUnprocessableEntity) IsClientError() bool {
return true
}
// IsServerError returns true when this register unprocessable entity response has a 5xx status code
func (o *RegisterUnprocessableEntity) IsServerError() bool {
return false
}
// IsCode returns true when this register unprocessable entity response a status code equal to that given
func (o *RegisterUnprocessableEntity) IsCode(code int) bool {
return code == 422
}
func (o *RegisterUnprocessableEntity) Error() string {
return fmt.Sprintf("[POST /register][%d] registerUnprocessableEntity %+v", 422, o.Payload)
}
func (o *RegisterUnprocessableEntity) String() string {
return fmt.Sprintf("[POST /register][%d] registerUnprocessableEntity %+v", 422, o.Payload)
}
func (o *RegisterUnprocessableEntity) GetPayload() rest_model_zrok.ErrorMessage {
return o.Payload
}
func (o *RegisterUnprocessableEntity) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {
// response payload
if err := consumer.Consume(response.Body(), &o.Payload); err != nil && err != io.EOF {
return err
}
return nil
}
// NewRegisterInternalServerError creates a RegisterInternalServerError with default headers values // NewRegisterInternalServerError creates a RegisterInternalServerError with default headers values
func NewRegisterInternalServerError() *RegisterInternalServerError { func NewRegisterInternalServerError() *RegisterInternalServerError {
return &RegisterInternalServerError{} return &RegisterInternalServerError{}

View File

@ -7,9 +7,12 @@ package account
import ( import (
"fmt" "fmt"
"io"
"github.com/go-openapi/runtime" "github.com/go-openapi/runtime"
"github.com/go-openapi/strfmt" "github.com/go-openapi/strfmt"
"github.com/openziti/zrok/rest_model_zrok"
) )
// ResetPasswordReader is a Reader for the ResetPassword structure. // ResetPasswordReader is a Reader for the ResetPassword structure.
@ -32,6 +35,12 @@ func (o *ResetPasswordReader) ReadResponse(response runtime.ClientResponse, cons
return nil, err return nil, err
} }
return nil, result return nil, result
case 422:
result := NewResetPasswordUnprocessableEntity()
if err := result.readResponse(response, consumer, o.formats); err != nil {
return nil, err
}
return nil, result
case 500: case 500:
result := NewResetPasswordInternalServerError() result := NewResetPasswordInternalServerError()
if err := result.readResponse(response, consumer, o.formats); err != nil { if err := result.readResponse(response, consumer, o.formats); err != nil {
@ -145,6 +154,67 @@ func (o *ResetPasswordNotFound) readResponse(response runtime.ClientResponse, co
return nil return nil
} }
// NewResetPasswordUnprocessableEntity creates a ResetPasswordUnprocessableEntity with default headers values
func NewResetPasswordUnprocessableEntity() *ResetPasswordUnprocessableEntity {
return &ResetPasswordUnprocessableEntity{}
}
/*
ResetPasswordUnprocessableEntity describes a response with status code 422, with default header values.
password validation failure
*/
type ResetPasswordUnprocessableEntity struct {
Payload rest_model_zrok.ErrorMessage
}
// IsSuccess returns true when this reset password unprocessable entity response has a 2xx status code
func (o *ResetPasswordUnprocessableEntity) IsSuccess() bool {
return false
}
// IsRedirect returns true when this reset password unprocessable entity response has a 3xx status code
func (o *ResetPasswordUnprocessableEntity) IsRedirect() bool {
return false
}
// IsClientError returns true when this reset password unprocessable entity response has a 4xx status code
func (o *ResetPasswordUnprocessableEntity) IsClientError() bool {
return true
}
// IsServerError returns true when this reset password unprocessable entity response has a 5xx status code
func (o *ResetPasswordUnprocessableEntity) IsServerError() bool {
return false
}
// IsCode returns true when this reset password unprocessable entity response a status code equal to that given
func (o *ResetPasswordUnprocessableEntity) IsCode(code int) bool {
return code == 422
}
func (o *ResetPasswordUnprocessableEntity) Error() string {
return fmt.Sprintf("[POST /resetPassword][%d] resetPasswordUnprocessableEntity %+v", 422, o.Payload)
}
func (o *ResetPasswordUnprocessableEntity) String() string {
return fmt.Sprintf("[POST /resetPassword][%d] resetPasswordUnprocessableEntity %+v", 422, o.Payload)
}
func (o *ResetPasswordUnprocessableEntity) GetPayload() rest_model_zrok.ErrorMessage {
return o.Payload
}
func (o *ResetPasswordUnprocessableEntity) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {
// response payload
if err := consumer.Consume(response.Body(), &o.Payload); err != nil && err != io.EOF {
return err
}
return nil
}
// NewResetPasswordInternalServerError creates a ResetPasswordInternalServerError with default headers values // NewResetPasswordInternalServerError creates a ResetPasswordInternalServerError with default headers values
func NewResetPasswordInternalServerError() *ResetPasswordInternalServerError { func NewResetPasswordInternalServerError() *ResetPasswordInternalServerError {
return &ResetPasswordInternalServerError{} return &ResetPasswordInternalServerError{}

View File

@ -8,6 +8,7 @@ package rest_model_zrok
import ( import (
"context" "context"
"github.com/go-openapi/errors"
"github.com/go-openapi/strfmt" "github.com/go-openapi/strfmt"
"github.com/go-openapi/swag" "github.com/go-openapi/swag"
) )
@ -23,6 +24,9 @@ type Configuration struct {
// invites open // invites open
InvitesOpen bool `json:"invitesOpen,omitempty"` InvitesOpen bool `json:"invitesOpen,omitempty"`
// password requirements
PasswordRequirements *PasswordRequirements `json:"passwordRequirements,omitempty"`
// requires invite token // requires invite token
RequiresInviteToken bool `json:"requiresInviteToken,omitempty"` RequiresInviteToken bool `json:"requiresInviteToken,omitempty"`
@ -35,11 +39,64 @@ type Configuration struct {
// Validate validates this configuration // Validate validates this configuration
func (m *Configuration) Validate(formats strfmt.Registry) error { func (m *Configuration) Validate(formats strfmt.Registry) error {
var res []error
if err := m.validatePasswordRequirements(formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil return nil
} }
// ContextValidate validates this configuration based on context it is used func (m *Configuration) validatePasswordRequirements(formats strfmt.Registry) error {
if swag.IsZero(m.PasswordRequirements) { // not required
return nil
}
if m.PasswordRequirements != nil {
if err := m.PasswordRequirements.Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("passwordRequirements")
} else if ce, ok := err.(*errors.CompositeError); ok {
return ce.ValidateName("passwordRequirements")
}
return err
}
}
return nil
}
// ContextValidate validate this configuration based on the context it is used
func (m *Configuration) ContextValidate(ctx context.Context, formats strfmt.Registry) error { func (m *Configuration) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
var res []error
if err := m.contextValidatePasswordRequirements(ctx, formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *Configuration) contextValidatePasswordRequirements(ctx context.Context, formats strfmt.Registry) error {
if m.PasswordRequirements != nil {
if err := m.PasswordRequirements.ContextValidate(ctx, formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("passwordRequirements")
} else if ce, ok := err.(*errors.CompositeError); ok {
return ce.ValidateName("passwordRequirements")
}
return err
}
}
return nil return nil
} }

View File

@ -0,0 +1,62 @@
// Code generated by go-swagger; DO NOT EDIT.
package rest_model_zrok
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"context"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
)
// PasswordRequirements password requirements
//
// swagger:model passwordRequirements
type PasswordRequirements struct {
// length
Length int64 `json:"length,omitempty"`
// require capital
RequireCapital bool `json:"requireCapital,omitempty"`
// require numeric
RequireNumeric bool `json:"requireNumeric,omitempty"`
// require special
RequireSpecial bool `json:"requireSpecial,omitempty"`
// valid special characters
ValidSpecialCharacters string `json:"validSpecialCharacters,omitempty"`
}
// Validate validates this password requirements
func (m *PasswordRequirements) Validate(formats strfmt.Registry) error {
return nil
}
// ContextValidate validates this password requirements based on context it is used
func (m *PasswordRequirements) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
return nil
}
// MarshalBinary interface implementation
func (m *PasswordRequirements) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *PasswordRequirements) UnmarshalBinary(b []byte) error {
var res PasswordRequirements
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@ -754,6 +754,12 @@ func init() {
"404": { "404": {
"description": "request not found" "description": "request not found"
}, },
"422": {
"description": "password validation failure",
"schema": {
"$ref": "#/definitions/errorMessage"
}
},
"500": { "500": {
"description": "internal server error" "description": "internal server error"
} }
@ -782,6 +788,12 @@ func init() {
"404": { "404": {
"description": "request not found" "description": "request not found"
}, },
"422": {
"description": "password validation failure",
"schema": {
"$ref": "#/definitions/errorMessage"
}
},
"500": { "500": {
"description": "internal server error" "description": "internal server error"
} }
@ -1062,6 +1074,9 @@ func init() {
"invitesOpen": { "invitesOpen": {
"type": "boolean" "type": "boolean"
}, },
"passwordRequirements": {
"$ref": "#/definitions/passwordRequirements"
},
"requiresInviteToken": { "requiresInviteToken": {
"type": "boolean" "type": "boolean"
}, },
@ -1295,6 +1310,26 @@ func init() {
} }
} }
}, },
"passwordRequirements": {
"type": "object",
"properties": {
"length": {
"type": "integer"
},
"requireCapital": {
"type": "boolean"
},
"requireNumeric": {
"type": "boolean"
},
"requireSpecial": {
"type": "boolean"
},
"validSpecialCharacters": {
"type": "string"
}
}
},
"principal": { "principal": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -2315,6 +2350,12 @@ func init() {
"404": { "404": {
"description": "request not found" "description": "request not found"
}, },
"422": {
"description": "password validation failure",
"schema": {
"$ref": "#/definitions/errorMessage"
}
},
"500": { "500": {
"description": "internal server error" "description": "internal server error"
} }
@ -2343,6 +2384,12 @@ func init() {
"404": { "404": {
"description": "request not found" "description": "request not found"
}, },
"422": {
"description": "password validation failure",
"schema": {
"$ref": "#/definitions/errorMessage"
}
},
"500": { "500": {
"description": "internal server error" "description": "internal server error"
} }
@ -2623,6 +2670,9 @@ func init() {
"invitesOpen": { "invitesOpen": {
"type": "boolean" "type": "boolean"
}, },
"passwordRequirements": {
"$ref": "#/definitions/passwordRequirements"
},
"requiresInviteToken": { "requiresInviteToken": {
"type": "boolean" "type": "boolean"
}, },
@ -2856,6 +2906,26 @@ func init() {
} }
} }
}, },
"passwordRequirements": {
"type": "object",
"properties": {
"length": {
"type": "integer"
},
"requireCapital": {
"type": "boolean"
},
"requireNumeric": {
"type": "boolean"
},
"requireSpecial": {
"type": "boolean"
},
"validSpecialCharacters": {
"type": "string"
}
}
},
"principal": { "principal": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@ -83,6 +83,49 @@ func (o *RegisterNotFound) WriteResponse(rw http.ResponseWriter, producer runtim
rw.WriteHeader(404) rw.WriteHeader(404)
} }
// RegisterUnprocessableEntityCode is the HTTP code returned for type RegisterUnprocessableEntity
const RegisterUnprocessableEntityCode int = 422
/*
RegisterUnprocessableEntity password validation failure
swagger:response registerUnprocessableEntity
*/
type RegisterUnprocessableEntity struct {
/*
In: Body
*/
Payload rest_model_zrok.ErrorMessage `json:"body,omitempty"`
}
// NewRegisterUnprocessableEntity creates RegisterUnprocessableEntity with default headers values
func NewRegisterUnprocessableEntity() *RegisterUnprocessableEntity {
return &RegisterUnprocessableEntity{}
}
// WithPayload adds the payload to the register unprocessable entity response
func (o *RegisterUnprocessableEntity) WithPayload(payload rest_model_zrok.ErrorMessage) *RegisterUnprocessableEntity {
o.Payload = payload
return o
}
// SetPayload sets the payload to the register unprocessable entity response
func (o *RegisterUnprocessableEntity) SetPayload(payload rest_model_zrok.ErrorMessage) {
o.Payload = payload
}
// WriteResponse to the client
func (o *RegisterUnprocessableEntity) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
rw.WriteHeader(422)
payload := o.Payload
if err := producer.Produce(rw, payload); err != nil {
panic(err) // let the recovery middleware deal with this
}
}
// RegisterInternalServerErrorCode is the HTTP code returned for type RegisterInternalServerError // RegisterInternalServerErrorCode is the HTTP code returned for type RegisterInternalServerError
const RegisterInternalServerErrorCode int = 500 const RegisterInternalServerErrorCode int = 500

View File

@ -9,6 +9,8 @@ import (
"net/http" "net/http"
"github.com/go-openapi/runtime" "github.com/go-openapi/runtime"
"github.com/openziti/zrok/rest_model_zrok"
) )
// ResetPasswordOKCode is the HTTP code returned for type ResetPasswordOK // ResetPasswordOKCode is the HTTP code returned for type ResetPasswordOK
@ -61,6 +63,49 @@ func (o *ResetPasswordNotFound) WriteResponse(rw http.ResponseWriter, producer r
rw.WriteHeader(404) rw.WriteHeader(404)
} }
// ResetPasswordUnprocessableEntityCode is the HTTP code returned for type ResetPasswordUnprocessableEntity
const ResetPasswordUnprocessableEntityCode int = 422
/*
ResetPasswordUnprocessableEntity password validation failure
swagger:response resetPasswordUnprocessableEntity
*/
type ResetPasswordUnprocessableEntity struct {
/*
In: Body
*/
Payload rest_model_zrok.ErrorMessage `json:"body,omitempty"`
}
// NewResetPasswordUnprocessableEntity creates ResetPasswordUnprocessableEntity with default headers values
func NewResetPasswordUnprocessableEntity() *ResetPasswordUnprocessableEntity {
return &ResetPasswordUnprocessableEntity{}
}
// WithPayload adds the payload to the reset password unprocessable entity response
func (o *ResetPasswordUnprocessableEntity) WithPayload(payload rest_model_zrok.ErrorMessage) *ResetPasswordUnprocessableEntity {
o.Payload = payload
return o
}
// SetPayload sets the payload to the reset password unprocessable entity response
func (o *ResetPasswordUnprocessableEntity) SetPayload(payload rest_model_zrok.ErrorMessage) {
o.Payload = payload
}
// WriteResponse to the client
func (o *ResetPasswordUnprocessableEntity) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
rw.WriteHeader(422)
payload := o.Payload
if err := producer.Produce(rw, payload); err != nil {
panic(err) // let the recovery middleware deal with this
}
}
// ResetPasswordInternalServerErrorCode is the HTTP code returned for type ResetPasswordInternalServerError // ResetPasswordInternalServerErrorCode is the HTTP code returned for type ResetPasswordInternalServerError
const ResetPasswordInternalServerErrorCode int = 500 const ResetPasswordInternalServerErrorCode int = 500

View File

@ -72,6 +72,10 @@ paths:
$ref: "#/definitions/registerResponse" $ref: "#/definitions/registerResponse"
404: 404:
description: request not found description: request not found
422:
description: password validation failure
schema:
$ref: '#/definitions/errorMessage'
500: 500:
description: internal server error description: internal server error
@ -90,6 +94,10 @@ paths:
description: password reset description: password reset
404: 404:
description: request not found description: request not found
422:
description: password validation failure
schema:
$ref: '#/definitions/errorMessage'
500: 500:
description: internal server error description: internal server error
@ -678,6 +686,8 @@ definitions:
type: boolean type: boolean
inviteTokenContact: inviteTokenContact:
type: string type: string
passwordRequirements:
$ref: "#/definitions/passwordRequirements"
createFrontendRequest: createFrontendRequest:
type: object type: object
@ -840,6 +850,20 @@ definitions:
type: array type: array
items: items:
$ref: "#/definitions/environmentAndResources" $ref: "#/definitions/environmentAndResources"
passwordRequirements:
type: object
properties:
length:
type: integer
requireCapital:
type: boolean
requireNumeric:
type: boolean
requireSpecial:
type: boolean
validSpecialCharacters:
type: string
principal: principal:
type: object type: object

View File

@ -34,6 +34,7 @@
* @property {boolean} invitesOpen * @property {boolean} invitesOpen
* @property {boolean} requiresInviteToken * @property {boolean} requiresInviteToken
* @property {string} inviteTokenContact * @property {string} inviteTokenContact
* @property {module:types.passwordRequirements} passwordRequirements
*/ */
/** /**
@ -166,6 +167,17 @@
* @property {module:types.environmentAndResources[]} environments * @property {module:types.environmentAndResources[]} environments
*/ */
/**
* @typedef passwordRequirements
* @memberof module:types
*
* @property {number} length
* @property {boolean} requireCapital
* @property {boolean} requireNumeric
* @property {boolean} requireSpecial
* @property {string} validSpecialCharacters
*/
/** /**
* @typedef principal * @typedef principal
* @memberof module:types * @memberof module:types

View File

@ -0,0 +1,99 @@
import React, {useEffect, useState, Fragment} from "react";
import {Button, Container, Form, Row} from "react-bootstrap";
const PasswordForm = (props) => {
const [password, setPassword] = useState('');
const [confirm, setConfirm] = useState('');
const passwordMismatchMessage = <h2 className={"errorMessage"}>Entered passwords do not match!</h2>
const passwordTooShortMessage = <h2 className={"errorMessage"}>Entered password too short! ({props.passwordLength} characters, minimum)</h2>
const passwordRequiresCapitalMessage = <h2 className={"errorMessage"}>Entered password requires a capital letter!</h2>
const passwordRequiresNumericMessage = <h2 className={"errorMessage"}>Entered password requires a digit!</h2>
const passwordRequiresSpecialMessage = <h2 className={"errorMessage"}>Entered password requires a special character! ({props.passwordValidSpecialCharacters.split("").join(" ")})</h2>
useEffect(() => {
if (confirm === "" && password === "") {
return
}
if (password.length < props.passwordLength) {
props.setMessage(passwordTooShortMessage)
return;
}
if (props.passwordRequireCapital && !/[A-Z]/.test(password)) {
props.setMessage(passwordRequiresCapitalMessage)
return;
}
if (props.passwordRequireNumeric && !/\d/.test(password)) {
props.setMessage(passwordRequiresNumericMessage)
return;
}
if (props.passwordRequireSpecial) {
if (!props.passwordValidSpecialCharacters.split("").some(v => password.includes(v))) {
props.setMessage(passwordRequiresSpecialMessage)
return;
}
}
if (confirm !== password) {
props.setMessage(passwordMismatchMessage)
return;
}
props.setParentPassword(password)
}, [password, confirm])
return (
<Fragment>
{
(props.passwordLength > 0 || props.passwordRequireCapital || props.passwordRequireNumeric || props.passwordRequireSpecial) &&
<h2>Password Requirements</h2>
}
{
props.passwordLength > 0 &&
<Row>Minimum Length of {props.passwordLength} </Row>
}
{
props.passwordRequireCapital &&
<Row>Requires at least 1 Capital Letter</Row>
}
{
props.passwordRequireNumeric &&
<Row>Requires at least 1 Digit</Row>
}
{
props.passwordRequireSpecial &&
<Fragment>
<Row>Requires at least 1 Special Character</Row>
<Row>{props.passwordValidSpecialCharacters.split("").join(" ")}</Row>
</Fragment>
}
<Container className={"fullscreen-body"}>
<Form.Group controlId={"password"}>
<Form.Control
type={"password"}
placeholder={"Set Password"}
onChange={t => { props.setMessage(null); setPassword(t.target.value);}}
value={password}
/>
</Form.Group>
<Form.Group controlId={"confirm"}>
<Form.Control
type={"password"}
placeholder={"Confirm Password"}
onChange={t => { props.setMessage(null); setConfirm(t.target.value);}}
value={confirm}
/>
</Form.Group>
</Container>
</Fragment>
)
};
PasswordForm.defaultProps = {
passwordLength: 0,
passwordRequireCapital: false,
passwordRequireNumeric: false,
passwordRequireSpecial: false,
passwordValidSpecialCharacters: ""
}
export default PasswordForm;

View File

@ -3,26 +3,33 @@ import * as account from "../api/account";
import * as metadata from "../api/metadata" import * as metadata from "../api/metadata"
import Success from "./Success"; import Success from "./Success";
import {Button, Container, Form, Row} from "react-bootstrap"; import {Button, Container, Form, Row} from "react-bootstrap";
import PasswordForm from "../components/password";
const SetPasswordForm = (props) => { const SetPasswordForm = (props) => {
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
const [confirm, setConfirm] = useState('');
const [message, setMessage] = useState(); const [message, setMessage] = useState();
const [authToken, setAuthToken] = useState(''); const [authToken, setAuthToken] = useState('');
const [complete, setComplete] = useState(false); const [complete, setComplete] = useState(false);
const [tou, setTou] = useState(); const [tou, setTou] = useState();
const [passwordLength, setPasswordLength] = useState(10);
const [passwordRequireCapital, setPasswordRequireCapital] = useState(true);
const [passwordRequireNumeric, setPasswordRequireNumeric] = useState(true);
const [passwordRequireSpecial, setPasswordRequireSpecial] = useState(true);
const [passwordValidSpecialCharacters, setPasswordValidSpecialCharacters] = useState("");
const passwordMismatchMessage = <h2 className={"errorMessage"}>Entered passwords do not match!</h2>
const passwordTooShortMessage = <h2 className={"errorMessage"}>Entered password too short! (4 characters, minimum)</h2>
const registerFailed = <h2 className={"errorMessage"}>Account creation failed!</h2> const registerFailed = <h2 className={"errorMessage"}>Account creation failed!</h2>
useEffect(() => { useEffect(() => {
metadata.configuration().then(resp => { metadata.configuration().then(resp => {
console.log(resp)
if(!resp.error) { if(!resp.error) {
if (resp.data.touLink !== null && resp.data.touLink.trim() !== "") { if (resp.data.touLink !== null && resp.data.touLink.trim() !== "") {
setTou(resp.data.touLink) setTou(resp.data.touLink)
} }
setPasswordLength(resp.data.passwordRequirements.length)
setPasswordRequireCapital(resp.data.passwordRequirements.requireCapital)
setPasswordRequireNumeric(resp.data.passwordRequirements.requireNumeric)
setPasswordRequireSpecial(resp.data.passwordRequirements.requireSpecial)
setPasswordValidSpecialCharacters(resp.data.passwordRequirements.validSpecialCharacters)
} }
}).catch(err => { }).catch(err => {
console.log("err", err); console.log("err", err);
@ -31,14 +38,7 @@ const SetPasswordForm = (props) => {
const handleSubmit = async e => { const handleSubmit = async e => {
e.preventDefault(); e.preventDefault();
if(confirm.length < 4) { if (password !== undefined && password !== "") {
setMessage(passwordTooShortMessage);
return;
}
if(confirm !== password) {
setMessage(passwordMismatchMessage);
return;
}
account.register({body: {"token": props.token, "password": password}}) account.register({body: {"token": props.token, "password": password}})
.then(resp => { .then(resp => {
if(!resp.error) { if(!resp.error) {
@ -54,6 +54,7 @@ const SetPasswordForm = (props) => {
console.log("resp", resp); console.log("resp", resp);
setMessage(registerFailed); setMessage(registerFailed);
}); });
}
}; };
if(!complete) { if(!complete) {
@ -72,23 +73,14 @@ const SetPasswordForm = (props) => {
<Container className={"fullscreen-form"}> <Container className={"fullscreen-form"}>
<Row> <Row>
<Form onSubmit={handleSubmit}> <Form onSubmit={handleSubmit}>
<Form.Group controlId={"password"}> <PasswordForm
<Form.Control setMessage={setMessage}
type={"password"} passwordLength={passwordLength}
placeholder={"Set Password"} passwordRequireCapital={passwordRequireCapital}
onChange={t => { setMessage(null); setPassword(t.target.value); }} passwordRequireNumeric={passwordRequireNumeric}
value={password} passwordRequireSpecial={passwordRequireSpecial}
/> passwordValidSpecialCharacters={passwordValidSpecialCharacters}
</Form.Group> setParentPassword={setPassword}/>
<Form.Group controlId={"confirm"}>
<Form.Control
type={"password"}
placeholder={"Confirm Password"}
onChange={t => { setMessage(null); setConfirm(t.target.value); }}
value={confirm}
/>
</Form.Group>
<Button variant={"light"} type={"submit"}>Register Account</Button> <Button variant={"light"} type={"submit"}>Register Account</Button>
</Form> </Form>
</Row> </Row>

View File

@ -1,29 +1,39 @@
import {useState} from "react"; import {useEffect, useState} from "react";
import * as account from '../api/account'; import * as account from '../api/account';
import * as metadata from "../api/metadata"
import {Button, Container, Form, Row} from "react-bootstrap"; import {Button, Container, Form, Row} from "react-bootstrap";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import PasswordForm from "../components/password";
const SetNewPassword = (props) => { const SetNewPassword = (props) => {
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
const [confirm, setConfirm] = useState('');
const [message, setMessage] = useState(); const [message, setMessage] = useState();
const [complete, setComplete] = useState(false); const [complete, setComplete] = useState(false);
const [passwordLength, setPasswordLength] = useState(10);
const passwordMismatchMessage = <h2 className={"errorMessage"}>Entered passwords do not match!</h2> const [passwordRequireCapital, setPasswordRequireCapital] = useState(true);
const passwordTooShortMessage = <h2 className={"errorMessage"}>Entered password too short! (4 characters, minimum)</h2> const [passwordRequireNumeric, setPasswordRequireNumeric] = useState(true);
const [passwordRequireSpecial, setPasswordRequireSpecial] = useState(true);
const [passwordValidSpecialCharacters, setPasswordValidSpecialCharacters] = useState("");
const errorMessage = <h2 className={"errorMessage"}>Reset Password Failed!</h2>; const errorMessage = <h2 className={"errorMessage"}>Reset Password Failed!</h2>;
useEffect(() => {
metadata.configuration().then(resp => {
if(!resp.error) {
setPasswordLength(resp.data.passwordRequirements.length)
setPasswordRequireCapital(resp.data.passwordRequirements.requireCapital)
setPasswordRequireNumeric(resp.data.passwordRequirements.requireNumeric)
setPasswordRequireSpecial(resp.data.passwordRequirements.requireSpecial)
setPasswordValidSpecialCharacters(resp.data.passwordRequirements.validSpecialCharacters)
}
}).catch(err => {
console.log("err", err);
});
}, [])
const handleSubmit = async e => { const handleSubmit = async e => {
e.preventDefault(); e.preventDefault();
if(confirm.length < 4) { if (password !== undefined && password !== "") {
setMessage(passwordTooShortMessage);
return;
}
if(confirm !== password) {
setMessage(passwordMismatchMessage);
return;
}
account.resetPassword({body: {"token": props.token, "password": password}}) account.resetPassword({body: {"token": props.token, "password": password}})
.then(resp => { .then(resp => {
if(!resp.error) { if(!resp.error) {
@ -36,6 +46,7 @@ const SetNewPassword = (props) => {
.catch(resp => { .catch(resp => {
setMessage(errorMessage); setMessage(errorMessage);
}) })
}
} }
if(!complete) { if(!complete) {
@ -51,23 +62,14 @@ const SetNewPassword = (props) => {
<Container className={"fullscreen-form"}> <Container className={"fullscreen-form"}>
<Row> <Row>
<Form onSubmit={handleSubmit}> <Form onSubmit={handleSubmit}>
<Form.Group controlId={"password"}> <PasswordForm
<Form.Control setMessage={setMessage}
type={"password"} passwordLength={passwordLength}
placeholder={"Set Password"} passwordRequireCapital={passwordRequireCapital}
onChange={t => { setMessage(null); setPassword(t.target.value); }} passwordRequireNumeric={passwordRequireNumeric}
value={password} passwordRequireSpecial={passwordRequireSpecial}
/> passwordValidSpecialCharacters={passwordValidSpecialCharacters}
</Form.Group> setParentPassword={setPassword}/>
<Form.Group controlId={"confirm"}>
<Form.Control
type={"password"}
placeholder={"Confirm Password"}
onChange={t => { setMessage(null); setConfirm(t.target.value); }}
value={confirm}
/>
</Form.Group>
<Button variant={"light"} type={"submit"}>Reset Password</Button> <Button variant={"light"} type={"submit"}>Reset Password</Button>
</Form> </Form>