From f7db80e4e298d7b4ef87c1b1216ccb1f0832fa0b Mon Sep 17 00:00:00 2001 From: Aaron L Date: Mon, 20 Feb 2017 14:28:38 -0800 Subject: [PATCH] Prototyping --- config.go | 38 +----- renderer.go | 15 +++ router.go | 5 +- storer.go | 344 ++++++++-------------------------------------------- 4 files changed, 72 insertions(+), 330 deletions(-) create mode 100644 renderer.go diff --git a/config.go b/config.go index 96c0c9e..bc63ef7 100644 --- a/config.go +++ b/config.go @@ -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 } diff --git a/renderer.go b/renderer.go new file mode 100644 index 0000000..7a635f0 --- /dev/null +++ b/renderer.go @@ -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) +} diff --git a/router.go b/router.go index 7e2d530..db83ef8 100644 --- a/router.go +++ b/router.go @@ -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 { diff --git a/storer.go b/storer.go index 7d9d4d0..5be20ca 100644 --- a/storer.go +++ b/storer.go @@ -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++ {