diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f2793b..12e388b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,20 @@ -Changelog -========= +# Changelog -## 2015-08-02 Change the way Bind/Unbind works +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [2.0.0] - 2018-01-?? +### Added +### Changed +### Deprecated +### Removed +### Fixed +### Security + +## 2015-08-02 +### Changed This change is potentially breaking, it did break the sample since the supporting struct was wrong for the data we were using. **Lock:** The documentation was updated to reflect that the struct value for AttemptNumber is indeed an int64. @@ -11,6 +24,7 @@ and make them into a map. Now the field list will contain all types found in the the type in the attribute map matches what's in the struct before assignment. ## 2015-04-01 Refactor for Multi-tenancy +### Changed This breaking change allows multiple sites running off the same code base to each use different configurations of Authboss. To migrate your code simply use authboss.New() to get an instance of Authboss and all the old things that used to be in the authboss package are now there. See [this commit to the sample](https://github.com/volatiletech/authboss-sample/commit/eea55fc3b03855d4e9fb63577d72ce8ff0cd4079) diff --git a/auth/auth.go b/auth/auth.go index 1acb961..20d77e7 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -13,9 +13,6 @@ import ( ) const ( - methodGET = "GET" - methodPOST = "POST" - tplLogin = "login.html.tpl" ) @@ -26,7 +23,6 @@ func init() { // Auth module type Auth struct { *authboss.Authboss - templates response.Templates } // Initialize module diff --git a/auth/auth_test.go b/auth/auth_test.go index 2ef2c2a..6846f97 100644 --- a/auth/auth_test.go +++ b/auth/auth_test.go @@ -21,7 +21,7 @@ func testSetup() (a *Auth, s *mocks.MockStorer) { ab := authboss.New() ab.LogWriter = ioutil.Discard ab.Layout = template.Must(template.New("").Parse(`{{template "authboss" .}}`)) - ab.Storer = s + ab.Storage.Server = s ab.XSRFName = "xsrf" ab.XSRFMaker = func(_ http.ResponseWriter, _ *http.Request) string { return "xsrfvalue" @@ -293,7 +293,7 @@ func TestAuth_validateCredentials(t *testing.T) { ab := authboss.New() storer := mocks.NewMockStorer() - ab.Storer = storer + ab.Storage.Server = storer ctx := ab.NewContext() storer.Users["john"] = authboss.Attributes{"password": "$2a$10$pgFsuQwdhwOdZp/v52dvHeEi53ZaI7dGmtwK4bAzGGN5A4nT6doqm"} diff --git a/authboss.go b/authboss.go index a284c21..aa41e90 100644 --- a/authboss.go +++ b/authboss.go @@ -11,34 +11,37 @@ import "github.com/pkg/errors" // Authboss contains a configuration and other details for running. type Authboss struct { Config - loadedModules map[string]bool - - viewRenderer Renderer - mailRenderer Renderer + loadedModules map[string]Moduler } // New makes a new instance of authboss with a default // configuration. func New() *Authboss { ab := &Authboss{} + ab.loadedModules = make(map[string]Moduler) + ab.Config.Defaults() return ab } // Init authboss, modules, renderers -func (a *Authboss) Init() error { - //TODO(aarondl): Figure the template names out along with new "module" loading. - views := []string{"all"} - - var err error - a.viewRenderer, err = a.Config.ViewLoader.Init(views) - if err != nil { - return errors.Wrap(err, "failed to load the view renderer") +func (a *Authboss) Init(modulesToLoad ...string) error { + if len(modulesToLoad) == 0 { + modulesToLoad = RegisteredModules() } - a.mailRenderer, err = a.Config.MailViewLoader.Init(views) - if err != nil { - return errors.Wrap(err, "failed to load the mail view renderer") + for _, name := range modulesToLoad { + mod, ok := registeredModules[name] + if !ok { + return errors.Errorf("module %s was supposed to be loaded but is not registered", name) + } + + a.loadedModules[name] = mod + + // Initialize the module + if err := mod.Init(a); err != nil { + return errors.Wrapf(err, "failed to init module: %s", name) + } } return nil diff --git a/authboss_test.go b/authboss_test.go index b0382be..d3c8d6e 100644 --- a/authboss_test.go +++ b/authboss_test.go @@ -1,7 +1,6 @@ package authboss import ( - "io/ioutil" "testing" ) @@ -9,9 +8,6 @@ func TestAuthBossInit(t *testing.T) { t.Parallel() ab := New() - ab.LogWriter = ioutil.Discard - ab.ViewLoader = mockRenderLoader{} - ab.MailViewLoader = mockRenderLoader{} err := ab.Init() if err != nil { t.Error("Unexpected error:", err) diff --git a/client_state.go b/client_state.go index 193d4e1..a487595 100644 --- a/client_state.go +++ b/client_state.go @@ -95,8 +95,8 @@ func (a *Authboss) NewResponse(w http.ResponseWriter, r *http.Request) *ClientSt // LoadClientState loads the state from sessions and cookies into the request context func (a *Authboss) LoadClientState(w http.ResponseWriter, r *http.Request) (*http.Request, error) { - if a.SessionStateStorer != nil { - state, err := a.SessionStateStorer.ReadState(w, r) + if a.Storage.SessionState != nil { + state, err := a.Storage.SessionState.ReadState(w, r) if err != nil { return nil, err } else if state == nil { @@ -106,8 +106,8 @@ func (a *Authboss) LoadClientState(w http.ResponseWriter, r *http.Request) (*htt ctx := context.WithValue(r.Context(), ctxKeySessionState, state) r = r.WithContext(ctx) } - if a.CookieStateStorer != nil { - state, err := a.CookieStateStorer.ReadState(w, r) + if a.Storage.CookieState != nil { + state, err := a.Storage.CookieState.ReadState(w, r) if err != nil { return nil, err } else if state == nil { @@ -184,14 +184,14 @@ func (c *ClientStateResponseWriter) putClientState() error { cookie = cookieStateIntf.(ClientState) } - if c.ab.SessionStateStorer != nil { - err := c.ab.SessionStateStorer.WriteState(c, session, c.sessionStateEvents) + if c.ab.Storage.SessionState != nil { + err := c.ab.Storage.SessionState.WriteState(c, session, c.sessionStateEvents) if err != nil { return err } } - if c.ab.CookieStateStorer != nil { - err := c.ab.CookieStateStorer.WriteState(c, cookie, c.cookieStateEvents) + if c.ab.Storage.CookieState != nil { + err := c.ab.Storage.CookieState.WriteState(c, cookie, c.cookieStateEvents) if err != nil { return err } diff --git a/client_state_test.go b/client_state_test.go index 3f9d565..9a1dc51 100644 --- a/client_state_test.go +++ b/client_state_test.go @@ -12,8 +12,8 @@ func TestStateGet(t *testing.T) { t.Parallel() ab := New() - ab.SessionStateStorer = newMockClientStateRW("one", "two") - ab.CookieStateStorer = newMockClientStateRW("three", "four") + ab.Storage.SessionState = newMockClientStateRW("one", "two") + ab.Storage.CookieState = newMockClientStateRW("three", "four") r := httptest.NewRequest("GET", "/", nil) w := ab.NewResponse(httptest.NewRecorder(), r) @@ -36,7 +36,7 @@ func TestStateResponseWriterDoubleWritePanic(t *testing.T) { t.Parallel() ab := New() - ab.SessionStateStorer = newMockClientStateRW("one", "two") + ab.Storage.SessionState = newMockClientStateRW("one", "two") r := httptest.NewRequest("GET", "/", nil) w := ab.NewResponse(httptest.NewRecorder(), r) @@ -58,8 +58,8 @@ func TestStateResponseWriterLastSecondWriteWithPrevious(t *testing.T) { t.Parallel() ab := New() - ab.SessionStateStorer = newMockClientStateRW("one", "two") - ab.CookieStateStorer = newMockClientStateRW("three", "four") + ab.Storage.SessionState = newMockClientStateRW("one", "two") + ab.Storage.CookieState = newMockClientStateRW("three", "four") r := httptest.NewRequest("GET", "/", nil) var w http.ResponseWriter = httptest.NewRecorder() @@ -85,7 +85,7 @@ func TestStateResponseWriterLastSecondWriteHeader(t *testing.T) { t.Parallel() ab := New() - ab.SessionStateStorer = newMockClientStateRW() + ab.Storage.SessionState = newMockClientStateRW() r := httptest.NewRequest("GET", "/", nil) w := ab.NewResponse(httptest.NewRecorder(), r) @@ -103,7 +103,7 @@ func TestStateResponseWriterLastSecondWriteWrite(t *testing.T) { t.Parallel() ab := New() - ab.SessionStateStorer = newMockClientStateRW() + ab.Storage.SessionState = newMockClientStateRW() r := httptest.NewRequest("GET", "/", nil) w := ab.NewResponse(httptest.NewRecorder(), r) @@ -155,7 +155,7 @@ func TestFlashClearer(t *testing.T) { t.Parallel() ab := New() - ab.SessionStateStorer = newMockClientStateRW(FlashSuccessKey, "a", FlashErrorKey, "b") + ab.Storage.SessionState = newMockClientStateRW(FlashSuccessKey, "a", FlashErrorKey, "b") r := httptest.NewRequest("GET", "/", nil) w := ab.NewResponse(httptest.NewRecorder(), r) diff --git a/config.go b/config.go index 588a158..c175d05 100644 --- a/config.go +++ b/config.go @@ -7,90 +7,107 @@ import ( // Config holds all the configuration for both authboss and it's modules. type Config struct { - // MountPath is the path to mount authboss's routes at (eg /auth). - MountPath string - // ViewsPath is the path to search for overridden templates. - ViewsPath string - // RootURL is the scheme+host+port of the web application (eg https://www.happiness.com:8080) for url generation. No trailing slash. - RootURL string - // BCryptCost is the cost of the bcrypt password hashing function. - BCryptCost int + Paths struct { + // Mount is the path to mount authboss's routes at (eg /auth). + Mount string - // PrimaryID is the primary identifier of the user. Set to one of: - // authboss.StoreEmail, authboss.StoreUsername (StoreEmail is default) - PrimaryID string + // AuthLoginOK is the redirect path after a successful authentication. + AuthLoginOK string + // AuthLoginFail is the redirect path after a failed authentication. + AuthLoginFail string + // AuthLogoutOK is the redirect path after a log out. + AuthLogoutOK string - // ViewLoader loads the templates for the application. - ViewLoader RenderLoader - // MailViewLoader loads the templates for mail. If this is nil, it will - // fall back to using the Renderer created from the ViewLoader instead. - MailViewLoader RenderLoader + // RecoverOK is the redirect path after a successful recovery of a password. + RecoverOK string - // OAuth2Providers lists all providers that can be used. See - // OAuthProvider documentation for more details. - OAuth2Providers map[string]OAuth2Provider + // RegisterOK is the redirect path after a successful registration. + RegisterOK string - // AuthLoginOKPath is the redirect path after a successful authentication. - AuthLoginOKPath string - // AuthLoginFailPath is the redirect path after a failed authentication. - AuthLoginFailPath string - // AuthLogoutOKPath is the redirect path after a log out. - AuthLogoutOKPath string + // RootURL is the scheme+host+port of the web application (eg https://www.happiness.com:8080) for url generation. No trailing slash. + RootURL string + } - // RecoverOKPath is the redirect path after a successful recovery of a password. - RecoverOKPath string - // RecoverTokenDuration controls how long a token sent via email for password - // recovery is valid for. - RecoverTokenDuration time.Duration + Modules struct { + // BCryptCost is the cost of the bcrypt password hashing function. + BCryptCost int - // RegisterOKPath is the redirect path after a successful registration. - RegisterOKPath string + // OAuth2Providers lists all providers that can be used. See + // OAuthProvider documentation for more details. + OAuth2Providers map[string]OAuth2Provider - // Policies control validation of form fields and are automatically run - // against form posts that include the fields. - Policies []Validator - // ConfirmFields are fields that are supposed to be submitted with confirmation - // fields alongside them, passwords, emails etc. - ConfirmFields []string - // PreserveFields are fields used with registration that are to be rendered when - // post fails. - PreserveFields []string + // PreserveFields are fields used with registration that are to be rendered when + // post fails. + PreserveFields []string - // ExpireAfter controls the time an account is idle before being logged out - // by the ExpireMiddleware. - ExpireAfter time.Duration + // ExpireAfter controls the time an account is idle before being logged out + // by the ExpireMiddleware. + ExpireAfter time.Duration - // LockAfter this many tries. - LockAfter int - // LockWindow is the waiting time before the number of attemps are reset. - LockWindow time.Duration - // LockDuration is how long an account is locked for. - LockDuration time.Duration + // RecoverTokenDuration controls how long a token sent via email for password + // recovery is valid for. + RecoverTokenDuration time.Duration - // EmailFrom is the email address authboss e-mails come from. - EmailFrom string - // EmailSubjectPrefix is used to add something to the front of the authboss - // email subjects. - EmailSubjectPrefix string + // LockAfter this many tries. + LockAfter int + // LockWindow is the waiting time before the number of attemps are reset. + LockWindow time.Duration + // LockDuration is how long an account is locked for. + LockDuration time.Duration + } - // Storer is the interface through which Authboss accesses the web apps database - // for user operations. - Storer ServerStorer + Mail struct { + // From is the email address authboss e-mails come from. + From string + // SubjectPrefix is used to add something to the front of the authboss + // email subjects. + SubjectPrefix string + } - // CookieStateStorer must be defined to provide an interface capapable of - // storing cookies for the given response, and reading them from the request. - CookieStateStorer ClientStateReadWriter - // SessionStateStorer must be defined to provide an interface capable of - // storing session-only values for the given response, and reading them - // from the request. - SessionStateStorer ClientStateReadWriter - // 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 - // 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 + Storage struct { + // Storer is the interface through which Authboss accesses the web apps database + // for user operations. + Server ServerStorer + + // CookieState must be defined to provide an interface capapable of + // storing cookies for the given response, and reading them from the request. + CookieState ClientStateReadWriter + // SessionState must be defined to provide an interface capable of + // storing session-only values for the given response, and reading them + // from the request. + SessionState ClientStateReadWriter + } + + Core struct { + // Router is the entity that controls all routing to authboss routes + // modules will register their routes with it. + Router Router + + // Responder takes a generic response from a controller and prepares + // the response, uses a renderer to create the body, and replies to the + // http request. + Responder HTTPResponder + + // Redirector can redirect a response, similar to Responder but responsible + // only for redirection. + Redirector HTTPRedirector + + // Validator helps validate an http request, it's given a name that describes + // the form it's validating so that conditional logic may be applied. + Validator Validator + + // ViewRenderer loads the templates for the application. + ViewRenderer Renderer + // MailRenderer loads the templates for mail. If this is nil, it will + // fall back to using the Renderer created from the ViewLoader instead. + MailRenderer Renderer + + // Mailer is the mailer being used to send e-mails out via smtp + Mailer Mailer + + // LogWriter is written to when errors occur + LogWriter io.Writer + } } // Defaults sets the configuration's default values. diff --git a/confirm/confirm_test.go b/confirm/confirm_test.go index e9e3db2..1ab99e2 100644 --- a/confirm/confirm_test.go +++ b/confirm/confirm_test.go @@ -19,7 +19,7 @@ import ( func setup() *Confirm { ab := authboss.New() - ab.Storer = mocks.NewMockStorer() + ab.Storage.Server = mocks.NewMockStorer() ab.LayoutHTMLEmail = template.Must(template.New("").Parse(`email ^_^`)) ab.LayoutTextEmail = template.Must(template.New("").Parse(`email`)) diff --git a/context.go b/context.go index 7009ee9..8a4666f 100644 --- a/context.go +++ b/context.go @@ -76,7 +76,7 @@ func (a *Authboss) CurrentUserP(w http.ResponseWriter, r *http.Request) User { } func (a *Authboss) currentUser(ctx context.Context, pid string) (User, error) { - user, err := a.Storer.Load(ctx, pid) + user, err := a.Storage.Server.Load(ctx, pid) if err != nil { return nil, err } diff --git a/context_test.go b/context_test.go index 54bfcb7..c3016ef 100644 --- a/context_test.go +++ b/context_test.go @@ -17,8 +17,8 @@ func loadClientStateP(ab *Authboss, w http.ResponseWriter, r *http.Request) *htt func testSetupContext() (*Authboss, *http.Request) { ab := New() - ab.SessionStateStorer = newMockClientStateRW(SessionKey, "george-pid") - ab.Storer = mockServerStorer{ + ab.Storage.SessionState = newMockClientStateRW(SessionKey, "george-pid") + ab.Storage.Server = mockServerStorer{ "george-pid": mockUser{Email: "george-pid", Password: "unreadable"}, } r := loadClientStateP(ab, nil, httptest.NewRequest("GET", "/", nil)) @@ -39,8 +39,8 @@ func testSetupContextCached() (*Authboss, mockUser, *http.Request) { func testSetupContextPanic() *Authboss { ab := New() - ab.SessionStateStorer = newMockClientStateRW(SessionKey, "george-pid") - ab.Storer = mockServerStorer{} + ab.Storage.SessionState = newMockClientStateRW(SessionKey, "george-pid") + ab.Storage.Server = mockServerStorer{} return ab } @@ -80,7 +80,7 @@ func TestCurrentUserIDP(t *testing.T) { ab := testSetupContextPanic() // Overwrite the setup functions state storer - ab.SessionStateStorer = newMockClientStateRW() + ab.Storage.SessionState = newMockClientStateRW() defer func() { if recover().(error) != ErrUserNotFound { @@ -101,7 +101,7 @@ func TestCurrentUser(t *testing.T) { t.Error(err) } - if got, err := user.GetEmail(context.TODO()); err != nil { + if got, err := user.GetPID(context.TODO()); err != nil { t.Error(err) } else if got != "george-pid" { t.Error("got:", got) @@ -118,7 +118,7 @@ func TestCurrentUserContext(t *testing.T) { t.Error(err) } - if got, err := user.GetEmail(context.TODO()); err != nil { + if got, err := user.GetPID(context.TODO()); err != nil { t.Error(err) } else if got != "george-pid" { t.Error("got:", got) @@ -198,7 +198,7 @@ func TestLoadCurrentUser(t *testing.T) { t.Error(err) } - if got, err := user.GetEmail(context.TODO()); err != nil { + if got, err := user.GetPID(context.TODO()); err != nil { t.Error(err) } else if got != "george-pid" { t.Error("got:", got) diff --git a/defaults/responder_test.go b/defaults/responder_test.go index c829e89..71508fe 100644 --- a/defaults/responder_test.go +++ b/defaults/responder_test.go @@ -16,6 +16,10 @@ type testRenderer struct { Callback func(context.Context, string, authboss.HTMLData) ([]byte, string, error) } +func (t testRenderer) Load(names ...string) error { + return nil +} + func (t testRenderer) Render(ctx context.Context, name string, data authboss.HTMLData) ([]byte, string, error) { return t.Callback(ctx, name, data) } @@ -189,8 +193,8 @@ func TestResponseRedirectNonAPI(t *testing.T) { w := httptest.NewRecorder() ab := authboss.New() - ab.Config.SessionStateStorer = mocks.NewClientRW() - ab.Config.CookieStateStorer = mocks.NewClientRW() + ab.Config.Storage.SessionState = mocks.NewClientRW() + ab.Config.Storage.CookieState = mocks.NewClientRW() aw := ab.NewResponse(w, r) ro := authboss.RedirectOptions{ @@ -228,8 +232,8 @@ func TestResponseRedirectNonAPIFollowRedir(t *testing.T) { w := httptest.NewRecorder() ab := authboss.New() - ab.Config.SessionStateStorer = mocks.NewClientRW() - ab.Config.CookieStateStorer = mocks.NewClientRW() + ab.Config.Storage.SessionState = mocks.NewClientRW() + ab.Config.Storage.CookieState = mocks.NewClientRW() aw := ab.NewResponse(w, r) ro := authboss.RedirectOptions{ diff --git a/expire.go b/expire.go index 4edf5cf..a0c696a 100644 --- a/expire.go +++ b/expire.go @@ -61,7 +61,7 @@ func (a *Authboss) ExpireMiddleware(next http.Handler) http.Handler { // below it. func (m expireMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) { if _, ok := GetSession(r, SessionKey); ok { - ttl := timeToExpiry(r, m.ab.ExpireAfter) + ttl := timeToExpiry(r, m.ab.Modules.ExpireAfter) if ttl == 0 { DelSession(w, SessionKey) DelSession(w, SessionLastAction) diff --git a/expire_test.go b/expire_test.go index 8b6d24a..d8e624b 100644 --- a/expire_test.go +++ b/expire_test.go @@ -10,7 +10,7 @@ import ( func TestExpireIsExpired(t *testing.T) { ab := New() - ab.SessionStateStorer = newMockClientStateRW( + ab.Storage.SessionState = newMockClientStateRW( SessionKey, "username", SessionLastAction, time.Now().UTC().Format(time.RFC3339), ) @@ -26,7 +26,7 @@ func TestExpireIsExpired(t *testing.T) { // No t.Parallel() - Also must be after refreshExpiry() call nowTime = func() time.Time { - return time.Now().UTC().Add(ab.ExpireAfter * 2) + return time.Now().UTC().Add(ab.Modules.ExpireAfter * 2) } defer func() { nowTime = time.Now @@ -72,8 +72,8 @@ func TestExpireIsExpired(t *testing.T) { func TestExpireNotExpired(t *testing.T) { ab := New() - ab.Config.ExpireAfter = time.Hour - ab.SessionStateStorer = newMockClientStateRW( + ab.Config.Modules.ExpireAfter = time.Hour + ab.Storage.SessionState = newMockClientStateRW( SessionKey, "username", SessionLastAction, time.Now().UTC().Format(time.RFC3339), ) @@ -90,7 +90,7 @@ func TestExpireNotExpired(t *testing.T) { } // No t.Parallel() - Also must be after refreshExpiry() call - newTime := time.Now().UTC().Add(ab.ExpireAfter / 2) + newTime := time.Now().UTC().Add(ab.Modules.ExpireAfter / 2) nowTime = func() time.Time { return newTime } diff --git a/views.go b/html_data.go similarity index 100% rename from views.go rename to html_data.go diff --git a/views_test.go b/html_data_test.go similarity index 100% rename from views_test.go rename to html_data_test.go diff --git a/internal/mocks/mocks.go b/internal/mocks/mocks.go index 75cc78f..c1d76fc 100644 --- a/internal/mocks/mocks.go +++ b/internal/mocks/mocks.go @@ -31,7 +31,7 @@ type User struct { } func (m User) GetUsername(context.Context) (string, error) { return m.Username, nil } -func (m User) GetEmail(context.Context) (string, error) { return m.Email, nil } +func (m User) GetPID(context.Context) (string, error) { return m.Email, nil } func (m User) GetPassword(context.Context) (string, error) { return m.Password, nil } func (m User) GetRecoverToken(context.Context) (string, error) { return m.RecoverToken, nil } func (m User) GetRecoverTokenExpiry(context.Context) (time.Time, error) { diff --git a/lock/lock_test.go b/lock/lock_test.go index 02bd463..3a719d0 100644 --- a/lock/lock_test.go +++ b/lock/lock_test.go @@ -58,7 +58,7 @@ func TestAfterAuth(t *testing.T) { } storer := mocks.NewMockStorer() - ab.Storer = storer + ab.Storage.Server = storer ctx.User = authboss.Attributes{ab.PrimaryID: "john@john.com"} if err := lock.afterAuth(ctx); err != nil { @@ -81,7 +81,7 @@ func TestAfterAuthFail_Lock(t *testing.T) { ctx := ab.NewContext() storer := mocks.NewMockStorer() - ab.Storer = storer + ab.Storage.Server = storer lock := Lock{ab} ab.LockWindow = 30 * time.Minute ab.LockDuration = 30 * time.Minute @@ -133,7 +133,7 @@ func TestAfterAuthFail_Reset(t *testing.T) { storer := mocks.NewMockStorer() lock := Lock{ab} ab.LockWindow = 30 * time.Minute - ab.Storer = storer + ab.Storage.Server = storer old = time.Now().UTC().Add(-time.Hour) @@ -175,7 +175,7 @@ func TestLock(t *testing.T) { ab := authboss.New() storer := mocks.NewMockStorer() - ab.Storer = storer + ab.Storage.Server = storer lock := Lock{ab} email := "john@john.com" @@ -199,7 +199,7 @@ func TestUnlock(t *testing.T) { ab := authboss.New() storer := mocks.NewMockStorer() - ab.Storer = storer + ab.Storage.Server = storer lock := Lock{ab} ab.LockWindow = 1 * time.Hour diff --git a/mocks_test.go b/mocks_test.go index f25a752..74e571d 100644 --- a/mocks_test.go +++ b/mocks_test.go @@ -29,7 +29,7 @@ func (m mockServerStorer) Load(ctx context.Context, key string) (User, error) { } func (m mockServerStorer) Save(ctx context.Context, user User) error { - e, err := user.GetEmail(ctx) + e, err := user.GetPID(ctx) if err != nil { panic(err) } @@ -39,7 +39,7 @@ func (m mockServerStorer) Save(ctx context.Context, user User) error { return nil } -func (m mockUser) PutEmail(ctx context.Context, email string) error { +func (m mockUser) PutPID(ctx context.Context, email string) error { m.Email = email return nil } @@ -53,7 +53,7 @@ func (m mockUser) PutPassword(ctx context.Context, password string) error { return nil } -func (m mockUser) GetEmail(ctx context.Context) (email string, err error) { +func (m mockUser) GetPID(ctx context.Context) (email string, err error) { return m.Email, nil } @@ -153,16 +153,14 @@ func newMockAPIRequest(postKeyValues ...string) *http.Request { return req } -type mockRenderLoader struct{} - -func (m mockRenderLoader) Init(names []string) (Renderer, error) { - return mockRenderer{}, nil -} - type mockRenderer struct { expectName string } +func (m mockRenderer) Load(names ...string) error { + return nil +} + func (m mockRenderer) Render(ctx context.Context, name string, data HTMLData) ([]byte, string, error) { if len(m.expectName) != 0 && m.expectName != name { panic(fmt.Sprintf("want template name: %s, but got: %s", m.expectName, name)) diff --git a/module.go b/module.go new file mode 100644 index 0000000..fd6fff8 --- /dev/null +++ b/module.go @@ -0,0 +1,45 @@ +package authboss + +var registeredModules = make(map[string]Moduler) + +// Moduler should be implemented by all the authboss modules. +type Moduler interface { + // Init the module + Init(*Authboss) error +} + +// RegisterModule with the core providing all the necessary information to +// integrate into authboss. +func RegisterModule(name string, m Moduler) { + registeredModules[name] = m +} + +// RegisteredModules returns a list of modules that are currently registered. +func RegisteredModules() []string { + mods := make([]string, len(registeredModules)) + i := 0 + for k := range registeredModules { + mods[i] = k + i++ + } + + return mods +} + +// LoadedModules returns a list of modules that are currently loaded. +func (a *Authboss) LoadedModules() []string { + mods := make([]string, len(a.loadedModules)) + i := 0 + for k := range a.loadedModules { + mods[i] = k + i++ + } + + return mods +} + +// IsLoaded checks if a specific module is loaded. +func (a *Authboss) IsLoaded(mod string) bool { + _, ok := a.loadedModules[mod] + return ok +} diff --git a/module_test.go b/module_test.go new file mode 100644 index 0000000..430d1c8 --- /dev/null +++ b/module_test.go @@ -0,0 +1,70 @@ +package authboss + +import ( + "net/http" + "testing" +) + +const ( + testModName = "testmodule" +) + +var ( + testMod = &testModule{} +) + +func init() { + RegisterModule(testModName, testMod) +} + +type testModule struct { +} + +func testHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("testhandler", "test") +} + +func (t *testModule) Init(a *Authboss) error { return nil } + +func TestRegister(t *testing.T) { + t.Parallel() + + // RegisterModule called by init() + if _, ok := registeredModules[testModName]; !ok { + t.Error("Expected module to be saved.") + } +} + +func TestLoadedModules(t *testing.T) { + t.Parallel() + + // RegisterModule called by init() + registered := RegisteredModules() + if len(registered) != 1 { + t.Error("Expected only a single module to be loaded.") + } else { + found := false + for _, name := range registered { + if name == testModName { + found = true + break + } + } + if !found { + t.Error("It should have found the module:", registered) + } + } +} + +func TestIsLoaded(t *testing.T) { + t.Parallel() + + ab := New() + if err := ab.Init(); err != nil { + t.Error(err) + } + + if loaded := ab.LoadedModules(); len(loaded) == 0 || loaded[0] != testModName { + t.Error("Loaded modules wrong:", loaded) + } +} diff --git a/recover/recover_test.go b/recover/recover_test.go index 8da6485..98e4505 100644 --- a/recover/recover_test.go +++ b/recover/recover_test.go @@ -29,7 +29,7 @@ func testSetup() (r *Recover, s *mocks.MockStorer, l *bytes.Buffer) { ab.Layout = template.Must(template.New("").Parse(`{{template "authboss" .}}`)) ab.LayoutHTMLEmail = template.Must(template.New("").Parse(`{{template "authboss" .}}`)) ab.LayoutTextEmail = template.Must(template.New("").Parse(`{{template "authboss" .}}`)) - ab.Storer = s + ab.Storage.Server = s ab.XSRFName = "xsrf" ab.XSRFMaker = func(_ http.ResponseWriter, _ *http.Request) string { return "xsrfvalue" diff --git a/register/register_test.go b/register/register_test.go index ddd82d5..32db906 100644 --- a/register/register_test.go +++ b/register/register_test.go @@ -22,7 +22,7 @@ func setup() *Register { return "xsrfvalue" } ab.ConfirmFields = []string{"password", "confirm_password"} - ab.Storer = mocks.NewMockStorer() + ab.Storage.Server = mocks.NewMockStorer() reg := Register{} if err := reg.Initialize(ab); err != nil { @@ -34,7 +34,7 @@ func setup() *Register { func TestRegister(t *testing.T) { ab := authboss.New() - ab.Storer = mocks.NewMockStorer() + ab.Storage.Server = mocks.NewMockStorer() r := Register{} if err := r.Initialize(ab); err != nil { t.Error(err) diff --git a/remember/remember_test.go b/remember/remember_test.go index 31340d7..547749e 100644 --- a/remember/remember_test.go +++ b/remember/remember_test.go @@ -19,13 +19,13 @@ func TestInitialize(t *testing.T) { t.Error("Expected error about token storers.") } - ab.Storer = mocks.MockFailStorer{} + ab.Storage.Server = mocks.MockFailStorer{} err = r.Initialize(ab) if err == nil { t.Error("Expected error about token storers.") } - ab.Storer = mocks.NewMockStorer() + ab.Storage.Server = mocks.NewMockStorer() err = r.Initialize(ab) if err != nil { t.Error("Unexpected error:", err) diff --git a/renderer.go b/renderer.go index a9a3f05..98212ae 100644 --- a/renderer.go +++ b/renderer.go @@ -2,14 +2,11 @@ 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 { + // Load the given templates, will most likely be called multiple times + Load(name ...string) error + + // Render the given template Render(ctx context.Context, name string, data HTMLData) (output []byte, contentType string, err error) } diff --git a/response.go b/response.go index 92878c4..77c97eb 100644 --- a/response.go +++ b/response.go @@ -48,10 +48,10 @@ type HTTPResponder interface { Respond(w http.ResponseWriter, r *http.Request, code int, templateName string, data HTMLData) error } -// Redirector redirects http requests to a different url (must handle both json and html) +// HTTPRedirector redirects http requests to a different url (must handle both json and html) // When an authboss controller wants to redirect a user to a different path, it will use // this interface. -type Redirector interface { +type HTTPRedirector interface { Redirect(w http.ResponseWriter, r *http.Request, ro RedirectOptions) error } @@ -60,7 +60,7 @@ func (a *Authboss) Email(w http.ResponseWriter, r *http.Request, email Email, ro ctx := r.Context() if len(ro.HTMLTemplate) != 0 { - htmlBody, _, err := a.mailRenderer.Render(ctx, ro.HTMLTemplate, ro.Data) + htmlBody, _, err := a.Core.MailRenderer.Render(ctx, ro.HTMLTemplate, ro.Data) if err != nil { return errors.Wrap(err, "failed to render e-mail html body") } @@ -68,12 +68,12 @@ func (a *Authboss) Email(w http.ResponseWriter, r *http.Request, email Email, ro } if len(ro.TextTemplate) != 0 { - textBody, _, err := a.mailRenderer.Render(ctx, ro.TextTemplate, ro.Data) + textBody, _, err := a.Core.MailRenderer.Render(ctx, ro.TextTemplate, ro.Data) if err != nil { return errors.Wrap(err, "failed to render e-mail text body") } email.TextBody = string(textBody) } - return a.Mailer.Send(ctx, email) + return a.Core.Mailer.Send(ctx, email) } diff --git a/storage.go b/storage.go index 0fd1f08..5626d11 100644 --- a/storage.go +++ b/storage.go @@ -45,13 +45,14 @@ type ServerStorer interface { // User has functions for each piece of data it requires. // Data should not be persisted on each function call. +// User has a PID (primary ID) that is used on the site as +// a single unique identifier to any given user (very typically e-mail +// or username). type User interface { - PutEmail(ctx context.Context, email string) error - PutUsername(ctx context.Context, username string) error + PutPID(ctx context.Context, pid 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) + GetPID(ctx context.Context) (pid string, err error) GetPassword(ctx context.Context) (password string, err error) } diff --git a/xsrf.go b/xsrf.go deleted file mode 100644 index d011a37..0000000 --- a/xsrf.go +++ /dev/null @@ -1,6 +0,0 @@ -package authboss - -import "net/http" - -// XSRF returns a token that should be written to forms to prevent xsrf attacks. -type XSRF func(http.ResponseWriter, *http.Request) (token string)