1
0
mirror of https://github.com/volatiletech/authboss.git synced 2024-11-24 08:42:17 +02:00

Prototyping

This commit is contained in:
Aaron L 2017-02-20 14:28:38 -08:00
parent bc55489bc6
commit f7db80e4e2
4 changed files with 72 additions and 330 deletions

View File

@ -99,25 +99,8 @@ type Config struct {
XSRFMaker XSRF
// Storer is the interface through which Authboss accesses the web apps database.
Storer Storer
// StoreMaker is an alternative to defining Storer directly, which facilitates creating
// a Storer on demand from the current http request. Unless you have an exceedingly unusual
// special requirement, defining Storer directly is the preferred pattern; literally the only
// known use case at the time of this property being added is Google App Engine, which requires
// the current context as an argument to its datastore API methods. To avoid passing StoreMaker
// an expired request object, where relevant, calls to this function will never be spun off as
// goroutines.
StoreMaker StoreMaker
// OAuth2Storer is a different kind of storer only meant for OAuth2.
OAuth2Storer OAuth2Storer
// OAuth2StoreMaker is an alternative to defining OAuth2Storer directly, which facilitates creating
// a OAuth2Storer on demand from the current http request. Unless you have an exceedingly unusual
// special requirement, defining OAuth2Storer directly is the preferred pattern; literally the only
// known use case at the time of this property being added is Google App Engine, which requires
// the current context as an argument to its datastore API methods. To avoid passing OAuth2StoreMaker
// an expired request object, where relevant, calls to this function will never be spun off as
// goroutines.
OAuth2StoreMaker OAuth2StoreMaker
StoreLoader Storer
// CookieStoreMaker must be defined to provide an interface capapable of storing cookies
// for the given response, and reading them from the request.
CookieStoreMaker CookieStoreMaker
@ -127,25 +110,10 @@ type Config struct {
// LogWriter is written to when errors occur, as well as on startup to show which modules are loaded
// and which routes they registered. By default writes to io.Discard.
LogWriter io.Writer
// LogWriteMaker is an alternative to defining LogWriter directly, which facilitates creating
// a LogWriter on demand from the current http request. Unless you have an exceedingly unusual
// special requirement, defining LogWriter directly is the preferred pattern; literally the only
// known use case at the time of this property being added is Google App Engine, which requires
// the current context as an argument to its logging API methods. To avoid passing LogWriteMaker
// an expired request object, where relevant, calls to this function will never be spun off as
// goroutines.
LogWriteMaker LogWriteMaker
// Mailer is the mailer being used to send e-mails out. Authboss defines two loggers for use
// LogMailer and SMTPMailer, the default is a LogMailer to io.Discard.
Mailer Mailer
// MailMaker is an alternative to defining Mailer directly, which facilitates creating
// a Mailer on demand from the current http request. Unless you have an exceedingly unusual
// special requirement, defining Mailer directly is the preferred pattern; literally the only
// known use case at the time of this property being added is Google App Engine, which requires
// the current context as an argument to its mail API methods. To avoid passing MailMaker
// an expired request object, where relevant, calls to this function will never be spun off as
// goroutines.
MailMaker MailMaker
// ContextProvider provides a context for a given request
ContextProvider func(*http.Request) context.Context
}

15
renderer.go Normal file
View File

@ -0,0 +1,15 @@
package authboss
import "context"
// RenderLoader is an object that understands how to load display templates.
// It's possible that Init() is a no-op if the responses are JSON or anything
// else.
type RenderLoader interface {
Init(names string) (Renderer, error)
}
// Renderer is a type that can render a given template with some data.
type Renderer interface {
Render(ctx context.Context, data HTMLData) ([]byte, error)
}

View File

@ -8,11 +8,8 @@ import (
"strings"
)
// HandlerFunc augments http.HandlerFunc with a context and error handling.
type HandlerFunc func(*Context, http.ResponseWriter, *http.Request) error
// RouteTable is a routing table from a path to a handlerfunc.
type RouteTable map[string]HandlerFunc
type RouteTable map[string]http.HandlerFunc
// NewRouter returns a router to be mounted at some mountpoint.
func (a *Authboss) NewRouter() http.Handler {

344
storer.go
View File

@ -2,16 +2,10 @@ package authboss
import (
"bytes"
"database/sql"
"database/sql/driver"
"context"
"errors"
"fmt"
"net/http"
"reflect"
"strconv"
"strings"
"time"
"unicode"
)
// Data store constants for attribute names.
@ -35,34 +29,64 @@ var (
ErrUserNotFound = errors.New("User not found")
// ErrTokenNotFound should be returned from UseToken when the record is not found.
ErrTokenNotFound = errors.New("Token not found")
// ErrUserFound should be retruned from Create when the primaryID of the record is found.
// ErrUserFound should be returned from Create when the primaryID of the record is found.
ErrUserFound = errors.New("User found")
)
// StorageOptions is a map depicting the things a module must be able to store.
type StorageOptions map[string]DataType
// Storer must be implemented in order to store the user's attributes somewhere.
// The type of store is up to the developer implementing it, and all it has to
// do is be able to store several simple types.
type Storer interface {
// Put is for storing the attributes passed in using the key. This is an
// update only method and should not store if it does not find the key.
Put(key string, attr Attributes) error
// Get is for retrieving attributes for a given key. The return value
// must be a struct that contains all fields with the correct types as shown
// by attrMeta. If the key is not found in the data store simply
// return nil, ErrUserNotFound.
Get(key string) (interface{}, error)
// StoreLoader represents the data store that's capable of loading users
// and giving them a context with which to store themselves.
type StoreLoader interface {
// Load will be passed the PrimaryID and return pre-loaded storer (meaning
// Storer.Load will not be called)
Load(ctx context.Context, key string) (Storer, error)
}
// OAuth2Storer is a replacement (or addition) to the Storer interface.
// It allows users to be stored and fetched via a uid/provider combination.
// Storer represents a user that also knows how to put himself into the db.
// It has functions for each piece of data it requires.
// Note that you should only persist data once Save() has been called.
type Storer interface {
PutEmail(ctx context.Context, email string) error
PutUsername(ctx context.Context, username string) error
PutPassword(ctx context.Context, password string) error
GetEmail(ctx context.Context) (email string, err error)
GetUsername(ctx context.Context) (username string, err error)
GetPassword(ctx context.Context) (password string, err error)
// Save the state
Save(ctx context.Context) error
// Load the state based on the properties that have been given (typically
// an e-mail/username).
Load(ctx context.Context) error
}
type ArbitraryStorer interface {
Storer
// PutArbitrary allows arbitrary fields defined by the authboss library
// consumer to add fields to the user registration piece.
PutArbitrary(ctx context.Context) error
// GetArbitrary is used only to display the arbitrary data back to the user
// when the form is reset.
GetArbitrary(ctx context.Context) (arbitrary map[string]string, err error)
}
// OAuth2Storer allows reading and writing values
type OAuth2Storer interface {
// PutOAuth creates or updates an existing record (unlike Storer.Put)
// because in the OAuth flow there is no separate create/update.
PutOAuth(uid, provider string, attr Attributes) error
GetOAuth(uid, provider string) (interface{}, error)
Storer
PutUID(ctx context.Context, uid string) error
PutProvider(ctx context.Context, provider string) error
PutToken(ctx context.Context, token string) error
PutRefreshToken(ctx context.Context, refreshToken string) error
PutExpiry(ctx context.Context, expiry time.Duration) error
GetUID(ctx context.Context) (uid string, err error)
GetProvider(ctx context.Context) (provider string, err error)
GetToken(ctx context.Context) (token string, err error)
GetRefreshToken(ctx context.Context) (refreshToken string, err error)
GetExpiry(ctx context.Context) (expiry time.Duration, err error)
}
// DataType represents the various types that clients must be able to store.
@ -95,268 +119,6 @@ func (d DataType) String() string {
return ""
}
// AttributeMeta stores type information for attributes.
type AttributeMeta map[string]DataType
// Names returns the names of all the attributes.
func (a AttributeMeta) Names() []string {
names := make([]string, len(a))
i := 0
for n := range a {
names[i] = n
i++
}
return names
}
// Attributes is just a key-value mapping of data.
type Attributes map[string]interface{}
// Attributes converts the post form values into an attributes map.
func AttributesFromRequest(r *http.Request) (Attributes, error) {
attr := make(Attributes)
if err := r.ParseForm(); err != nil {
return nil, err
}
for name, values := range r.Form {
if len(values) == 0 {
continue
}
val := values[0]
if len(val) == 0 {
continue
}
switch {
case strings.HasSuffix(name, "_int"):
integer, err := strconv.Atoi(val)
if err != nil {
return nil, fmt.Errorf("%q (%q): could not be converted to an integer: %v", name, val, err)
}
attr[strings.TrimRight(name, "_int")] = integer
case strings.HasSuffix(name, "_date"):
date, err := time.Parse(time.RFC3339, val)
if err != nil {
return nil, fmt.Errorf("%q (%q): could not be converted to a datetime: %v", name, val, err)
}
attr[strings.TrimRight(name, "_date")] = date.UTC()
default:
attr[name] = val
}
}
return attr, nil
}
// Names returns the names of all the attributes.
func (a Attributes) Names() []string {
names := make([]string, len(a))
i := 0
for n := range a {
names[i] = n
i++
}
return names
}
// String returns a single value as a string
func (a Attributes) String(key string) (string, bool) {
inter, ok := a[key]
if !ok {
return "", false
}
val, ok := inter.(string)
return val, ok
}
// Int64 returns a single value as a int64
func (a Attributes) Int64(key string) (int64, bool) {
inter, ok := a[key]
if !ok {
return 0, false
}
val, ok := inter.(int64)
return val, ok
}
// Bool returns a single value as a bool.
func (a Attributes) Bool(key string) (val bool, ok bool) {
var inter interface{}
inter, ok = a[key]
if !ok {
return val, ok
}
val, ok = inter.(bool)
return val, ok
}
// DateTime returns a single value as a time.Time
func (a Attributes) DateTime(key string) (time.Time, bool) {
inter, ok := a[key]
if !ok {
var time time.Time
return time, false
}
val, ok := inter.(time.Time)
return val, ok
}
// StringErr returns a single value as a string
func (a Attributes) StringErr(key string) (val string, err error) {
inter, ok := a[key]
if !ok {
return "", AttributeErr{Name: key}
}
val, ok = inter.(string)
if !ok {
return val, NewAttributeErr(key, String, inter)
}
return val, nil
}
// Int64Err returns a single value as a int
func (a Attributes) Int64Err(key string) (val int64, err error) {
inter, ok := a[key]
if !ok {
return val, AttributeErr{Name: key}
}
val, ok = inter.(int64)
if !ok {
return val, NewAttributeErr(key, Integer, inter)
}
return val, nil
}
// BoolErr returns a single value as a bool.
func (a Attributes) BoolErr(key string) (val bool, err error) {
inter, ok := a[key]
if !ok {
return val, AttributeErr{Name: key}
}
val, ok = inter.(bool)
if !ok {
return val, NewAttributeErr(key, Integer, inter)
}
return val, nil
}
// DateTimeErr returns a single value as a time.Time
func (a Attributes) DateTimeErr(key string) (val time.Time, err error) {
inter, ok := a[key]
if !ok {
return val, AttributeErr{Name: key}
}
val, ok = inter.(time.Time)
if !ok {
return val, NewAttributeErr(key, DateTime, inter)
}
return val, nil
}
// Bind the data in the attributes to the given struct. This means the
// struct creator must have read the documentation and decided what fields
// will be needed ahead of time. Ignore missing ignores attributes for
// which a struct attribute equivalent can not be found.
func (a Attributes) Bind(strct interface{}, ignoreMissing bool) error {
structType := reflect.TypeOf(strct)
if structType.Kind() != reflect.Ptr {
return errors.New("Bind: Must pass in a struct pointer.")
}
structVal := reflect.ValueOf(strct).Elem()
structType = structVal.Type()
for k, v := range a {
k = underToCamel(k)
if _, has := structType.FieldByName(k); !has && ignoreMissing {
continue
} else if !has {
return fmt.Errorf("Bind: Struct was missing %s field, type: %v", k, reflect.TypeOf(v).String())
}
field := structVal.FieldByName(k)
if !field.CanSet() {
return fmt.Errorf("Bind: Found field %s, but was not writeable", k)
}
fieldType := field.Type()
fieldPtr := field.Addr()
if _, ok := fieldPtr.Interface().(sql.Scanner); ok {
method := fieldPtr.MethodByName("Scan")
if !method.IsValid() {
return errors.New("Bind: Was a scanner without a Scan method")
}
rvals := method.Call([]reflect.Value{reflect.ValueOf(v)})
if err, ok := rvals[0].Interface().(error); ok && err != nil {
return err
}
continue
}
if valType := reflect.TypeOf(v); fieldType != valType {
return fmt.Errorf("Bind: Field %s's type should be %s but was %s", k, valType, fieldType)
}
field.Set(reflect.ValueOf(v))
}
return nil
}
// StoreMaker is used to create a storer from an http request.
type StoreMaker func(http.ResponseWriter, *http.Request) Storer
// OAuth2StoreMaker is used to create an oauth2 storer from an http request.
type OAuth2StoreMaker func(http.ResponseWriter, *http.Request) OAuth2Storer
// Unbind is the opposite of Bind, taking a struct in and producing a list of attributes.
func Unbind(intf interface{}) Attributes {
structValue := reflect.ValueOf(intf)
if structValue.Kind() == reflect.Ptr {
structValue = structValue.Elem()
}
structType := structValue.Type()
attr := make(Attributes)
for i := 0; i < structValue.NumField(); i++ {
field := structValue.Field(i)
name := structType.Field(i).Name
if unicode.IsLower(rune(name[0])) {
continue // Unexported
}
name = camelToUnder(name)
fieldPtr := field.Addr()
if _, ok := fieldPtr.Interface().(driver.Valuer); ok {
method := fieldPtr.MethodByName("Value")
if !method.IsValid() {
panic("Unbind: Was a valuer without a Value method")
}
rvals := method.Call([]reflect.Value{})
if err, ok := rvals[1].Interface().(error); ok && err != nil {
panic(fmt.Errorf("Unbind: Failed to get value out of Valuer: %s, %v", name, err))
}
attr[name] = rvals[0].Interface()
continue
}
attr[name] = field.Interface()
}
return attr
}
func camelToUnder(in string) string {
out := bytes.Buffer{}
for i := 0; i < len(in); i++ {