1
0
mirror of https://github.com/volatiletech/authboss.git synced 2025-02-09 13:47:09 +02:00

Add a way to read values and validate

In order to support multiple different types of requests, there needed
to be an interface to be able to read values from a request, and
subsequently validate them to return any errors.

So we've adjusted the Validator interface to no longer validate a
request but instead validate the object it lives on.

And we've created a new BodyReader interface.
This commit is contained in:
Aaron L 2018-02-04 18:30:39 -08:00
parent 7119b3a90e
commit 386133a84b
8 changed files with 242 additions and 70 deletions

View File

@ -15,6 +15,7 @@ var blankRegex = regexp.MustCompile(`^\s*$`)
type Rules struct {
// FieldName is the name of the field this is intended to validate.
FieldName string
// MatchError describes the MustMatch regexp to a user.
Required bool
MatchError string
@ -27,11 +28,6 @@ type Rules struct {
AllowWhitespace bool
}
// Field names the field this ruleset applies to.
func (r Rules) Field() string {
return r.FieldName
}
// Errors returns an array of errors for each validation error that
// is present in the given string. Returns nil if there are no errors.
func (r Rules) Errors(toValidate string) authboss.ErrorList {

View File

@ -2,37 +2,42 @@ package defaults
import (
"fmt"
"net/http"
"github.com/volatiletech/authboss"
)
// HTTPFormValidator validates HTTP post type inputs
type HTTPFormValidator struct {
Ruleset []authboss.FieldValidator
Values map[string]string
Ruleset []Rules
ConfirmFields []string
}
// Validate validates a request using the given ruleset.
func (h HTTPFormValidator) Validate(r *http.Request) authboss.ErrorList {
func (h HTTPFormValidator) Validate() []error {
var errList authboss.ErrorList
for _, fieldValidator := range h.Ruleset {
field := fieldValidator.Field()
for _, rule := range h.Ruleset {
field := rule.FieldName
val := r.FormValue(field)
if errs := fieldValidator.Errors(val); len(errs) > 0 {
val := h.Values[field]
if errs := rule.Errors(val); len(errs) > 0 {
errList = append(errList, errs...)
}
}
if l := len(h.ConfirmFields); l != 0 && l%2 != 0 {
panic("HTTPFormValidator given an odd number of confirm fields")
}
for i := 0; i < len(h.ConfirmFields)-1; i += 2 {
main := r.FormValue(h.ConfirmFields[i])
main := h.Values[h.ConfirmFields[i]]
if len(main) == 0 {
continue
}
confirm := r.FormValue(h.ConfirmFields[i+1])
confirm := h.Values[h.ConfirmFields[i+1]]
if len(confirm) == 0 || main != confirm {
errList = append(errList, FieldError{h.ConfirmFields[i+1], fmt.Errorf("Does not match %s", h.ConfirmFields[i])})
}

View File

@ -3,39 +3,36 @@ package defaults
import (
"testing"
"github.com/pkg/errors"
"github.com/volatiletech/authboss"
"github.com/volatiletech/authboss/internal/mocks"
)
func TestValidate(t *testing.T) {
t.Parallel()
req := mocks.Request("POST", "username", "john", "email", "john@john.com")
validator := HTTPFormValidator{
Ruleset: []authboss.FieldValidator{
mocks.FieldValidator{
Values: map[string]string{
"username": "john",
"email": "john@john.com",
},
Ruleset: []Rules{
Rules{
FieldName: "username",
Errs: authboss.ErrorList{FieldError{"username", errors.New("must be longer than 4")}},
MinLength: 5,
},
mocks.FieldValidator{
Rules{
FieldName: "missing_field",
Errs: authboss.ErrorList{FieldError{"missing_field", errors.New("Expected field to exist")}},
},
mocks.FieldValidator{
FieldName: "email", Errs: nil,
Required: true,
},
},
}
errList := validator.Validate(req)
errList := authboss.ErrorList(validator.Validate())
errs := errList.Map()
if errs["username"][0] != "must be longer than 4" {
if errs["username"][0] != "Must be at least 5 characters" {
t.Error("Expected a different error for username:", errs["username"][0])
}
if errs["missing_field"][0] != "Expected field to exist" {
if errs["missing_field"][0] != "Cannot be blank" {
t.Error("Expected a different error for missing_field:", errs["missing_field"][0])
}
if _, ok := errs["email"]; ok {
@ -47,17 +44,23 @@ func TestValidate_Confirm(t *testing.T) {
t.Parallel()
validator := HTTPFormValidator{
ConfirmFields: []string{"username", "confirmUsername"},
Values: map[string]string{
"username": "john",
"confirm_username": "johnny",
},
ConfirmFields: []string{"username", "confirm_username"},
}
req := mocks.Request("POST", "username", "john", "confirmUsername", "johnny")
errs := validator.Validate(req).Map()
if errs["confirmUsername"][0] != "Does not match username" {
t.Error("Expected a different error for confirmUsername:", errs["confirmUsername"][0])
mapped := authboss.ErrorList(validator.Validate()).Map()
if mapped["confirm_username"][0] != "Does not match username" {
t.Error("Expected a different error for confirmUsername:", mapped["confirmUsername"][0])
}
req = mocks.Request("POST", "username", "john", "confirmUsername", "john")
errs = validator.Validate(req).Map()
validator.Values = map[string]string{
"username": "john",
"confirm_username": "john",
}
errs := authboss.ErrorList(validator.Validate())
if len(errs) != 0 {
t.Error("Expected no errors:", errs)
}
@ -66,9 +69,19 @@ func TestValidate_Confirm(t *testing.T) {
ConfirmFields: []string{"username"},
}
req = mocks.Request("POST", "username", "john", "confirmUsername", "john")
errs = validator.Validate(req).Map()
paniced := false
defer func() {
if r := recover(); r != nil {
paniced = true
}
}()
errs = authboss.ErrorList(validator.Validate())
if len(errs) != 0 {
t.Error("Expected no errors:", errs)
}
if !paniced {
t.Error("Want a panic due to bad confirm fields slice")
}
}

129
defaults/values.go Normal file
View File

@ -0,0 +1,129 @@
package defaults
import (
"net/http"
"net/url"
"regexp"
"github.com/pkg/errors"
"github.com/volatiletech/authboss"
)
// FormValue types
const (
FormValueEmail = "email"
FormValuePassword = "password"
FormValueUsername = "username"
)
// UserValues from the login form
type UserValues struct {
HTTPFormValidator
PID string
Password string
}
// GetPID from the values
func (u UserValues) GetPID() string {
return u.PID
}
// GetPassword from the values
func (u UserValues) GetPassword() string {
return u.Password
}
// HTTPFormReader reads forms from various pages and decodes
// them.
type HTTPFormReader struct {
UseUsername bool
Rulesets map[string][]Rules
}
// NewHTTPFormReader creates a form reader with default validation rules
// for each page.
func NewHTTPFormReader(useUsernameNotEmail bool) *HTTPFormReader {
var pid string
var pidRules Rules
if useUsernameNotEmail {
pid = "username"
pidRules = Rules{
FieldName: pid, Required: true,
MatchError: "Usernames must only start with letters, and contain letters and numbers",
MustMatch: regexp.MustCompile(`(?i)[a-z][a-z0-9]?`),
}
} else {
pid = "email"
pidRules = Rules{
FieldName: pid, Required: true,
MatchError: "Must be a valid e-mail address",
MustMatch: regexp.MustCompile(`.*@.*\.[a-z]{1,}`),
}
}
passwordRule := Rules{
FieldName: "password",
MinLength: 8,
MinNumeric: 1,
MinSymbols: 1,
MinUpper: 1,
MinLower: 1,
}
return &HTTPFormReader{
UseUsername: useUsernameNotEmail,
Rulesets: map[string][]Rules{
"login": []Rules{pidRules, passwordRule},
},
}
}
// Read the form pages
func (h HTTPFormReader) Read(page string, r *http.Request) (authboss.Validator, error) {
if err := r.ParseForm(); err != nil {
return nil, errors.Wrapf(err, "failed to parse form on page: %s", page)
}
rules := h.Rulesets[page]
values := URLValuesToMap(r.Form)
switch page {
case "login":
var pid string
if h.UseUsername {
pid = values[FormValueUsername]
} else {
pid = values[FormValueEmail]
}
validator := HTTPFormValidator{
Values: values,
Ruleset: rules,
ConfirmFields: []string{FormValuePassword, "confirm_" + FormValuePassword},
}
password := values[FormValuePassword]
return UserValues{
HTTPFormValidator: validator,
PID: pid,
Password: password,
}, nil
default:
return nil, errors.Errorf("failed to parse unknown page's form: %s", page)
}
}
// URLValuesToMap helps create a map from url.Values
func URLValuesToMap(form url.Values) map[string]string {
values := make(map[string]string)
for k, v := range form {
if len(v) != 0 {
values[k] = v[0]
}
}
return values
}

28
defaults/values_test.go Normal file
View File

@ -0,0 +1,28 @@
package defaults
import (
"testing"
"github.com/volatiletech/authboss"
"github.com/volatiletech/authboss/internal/mocks"
)
func TestHTTPFormReader(t *testing.T) {
t.Parallel()
h := NewHTTPFormReader(false)
r := mocks.Request("POST", "email", "john@john.john", "password", "flowers")
validator, err := h.Read("login", r)
if err != nil {
t.Error(err)
}
uv := validator.(authboss.UserValuer)
if "john@john.john" != uv.GetPID() {
t.Error("wrong e-mail:", uv.GetPID())
}
if "flowers" != uv.GetPassword() {
t.Error("wrong password:", uv.GetPassword())
}
}

View File

@ -349,25 +349,3 @@ func NewAfterCallback() *AfterCallback {
return &m
}
// FieldValidator mock
type FieldValidator struct {
FieldName string
Errs []error
Ruleset []string
}
// Field being validated
func (f FieldValidator) Field() string {
return f.FieldName
}
// Errors list
func (f FieldValidator) Errors(in string) []error {
return f.Errs
}
// Rules present
func (f FieldValidator) Rules() []string {
return f.Ruleset
}

View File

@ -12,20 +12,14 @@ const (
// Validator takes a form name and a set of inputs and returns any validation errors
// for the inputs.
type Validator interface {
// Validate inputs from the named form
Validate(name string, fieldValues map[string]string) []error
}
// FieldValidator is anything that can validate a string and provide a list of errors
// and describe its set of rules.
type FieldValidator interface {
Field() string
Errors(in string) []error
Rules() []string
// Validate makes the type validate itself and return
// a list of validation errors.
Validate() []error
}
// FieldError describes an error on a field
type FieldError interface {
error
Name() string
Err() error
}

29
values.go Normal file
View File

@ -0,0 +1,29 @@
package authboss
import "net/http"
// BodyReader reads data from the request
// and returns it in an abstract form.
// Typically used to decode JSON responses
// or Url Encoded request bodies.
//
// The first parameter is the page that this request
// was made on so we can tell what kind of JSON object
// or form was present as well as create the proper
// validation mechanisms.
//
// A typical example of this is taking the request
// and turning it into a JSON struct that knows how
// to validate itself and return certain fields.
type BodyReader interface {
Read(page string, r *http.Request) (Validator, error)
}
// UserValuer gets a string from a map-like data structure
// Typically a decoded JSON or form auth request
type UserValuer interface {
Validator
GetPID() string
GetPassword() string
}