diff --git a/pkg/auth/session.go b/pkg/auth/session.go index f08dbf1..aedfc0e 100644 --- a/pkg/auth/session.go +++ b/pkg/auth/session.go @@ -70,33 +70,39 @@ type Credential struct { UserID UserID `json:"user_id"` AuthProviderType string `json:"auth_provider_type"` AuthProviderID *string `json:"auth_provider_id"` - DataRaw json.RawMessage `json:"data,omitempty"` - user provider.ProviderUser + DataRaw *json.RawMessage `json:"data,omitempty"` + user provider.ProviderUser `json:"-"` } func (cred *Credential) MarshalJSON() ([]byte, error) { - rm := map[string]interface{}{ - "id": cred.ID, - "user_id": cred.UserID, - "auth_provider_type": cred.user.ProviderType(), - "auth_provider_id": cred.user.ProviderID(), - } + type CredAlias Credential + nCd := (*CredAlias)(cred) providerData := cred.user.ProviderUserData() if providerData != nil { - rm["data"] = providerData + b, err := json.Marshal(providerData) + if err != nil { + return nil, err + } + + dr := json.RawMessage(b) + nCd.DataRaw = &dr } - return json.Marshal(rm) + return json.Marshal(nCd) } -func (ss *SessionStore) verifyAndGetCredential(tr *TokenRequest, r *http.Request) *Credential { - user, success := ss.verify(tr, r) +func (a *Authenticator) verifyAndGetCredential(tr *TokenRequest, r *http.Request) *Credential { + user, success := a.sessions.verify(tr, r) if !success { return nil } - return &Credential{user: user} + cred := &Credential{ + user: user, + } + + return cred } const defaultExpiration = 2 * time.Hour @@ -156,7 +162,7 @@ func (a *Authenticator) TokenHandler(c echo.Context) error { return c.JSON(http.StatusBadRequest, AuthError{Error: "invalid_request", Description: "invalid code"}) } - if cred := a.sessions.verifyAndGetCredential(rq, c.Request()); cred != nil { + if cred := a.verifyAndGetCredential(rq, c.Request()); cred != nil { // TODO: success user, err := a.getOrCreateUser(cred) if err != nil { diff --git a/pkg/auth/store.go b/pkg/auth/store.go index 0c9374c..157319e 100644 --- a/pkg/auth/store.go +++ b/pkg/auth/store.go @@ -4,6 +4,8 @@ import ( "encoding/json" "fmt" + "github.com/rs/zerolog/log" + "dynatron.me/x/blasphem/pkg/storage" ) @@ -31,6 +33,7 @@ func (a *Authenticator) newAuthStore(s storage.Store) (as *authStore, err error) for _, u := range as.Users { as.userMap[u.ID] = &u + log.Debug().Interface("user", u).Msg("user") } for _, c := range as.Credentials { @@ -41,9 +44,11 @@ func (a *Authenticator) newAuthStore(s storage.Store) (as *authStore, err error) pd := prov.NewCredData() - err := json.Unmarshal(c.DataRaw, pd) - if err != nil { - return nil, err + if c.DataRaw != nil { + err := json.Unmarshal(*c.DataRaw, pd) + if err != nil { + return nil, err + } } } diff --git a/pkg/auth/user.go b/pkg/auth/user.go index ded2f98..6913e83 100644 --- a/pkg/auth/user.go +++ b/pkg/auth/user.go @@ -1,8 +1,6 @@ package auth import ( - "errors" - "github.com/rs/zerolog/log" ) @@ -34,7 +32,7 @@ func (u *User) allowedToAuth() error { } func (a *Authenticator) getOrCreateUser(c *Credential) (*User, error) { - log.Debug().Interface("userdata", c.user.ProviderUserData()).Msg("getOrCreateUser") + log.Debug().Interface("userdata", c).Msg("getOrCreateUser") u := a.store.User(c.UserID) if u == nil { return nil, ErrInvalidAuth diff --git a/pkg/blas/blas.go b/pkg/blas/blas.go index 45e13b3..bd5cf00 100644 --- a/pkg/blas/blas.go +++ b/pkg/blas/blas.go @@ -20,6 +20,7 @@ type Blas struct { func (b *Blas) Shutdown(ctx context.Context) error { b.Bus.Shutdown() + b.Store.Shutdown() return ctx.Err() } @@ -44,7 +45,8 @@ func (b *Blas) ConfigDir() (cd string) { } func (b *Blas) openStore() error { - stor, err := storage.Open(os.DirFS(b.ConfigDir())) + // TODO: based on config, open filestore or db store + stor, err := storage.OpenFileStore(b.ConfigDir()) b.Store = stor return err } diff --git a/pkg/storage/storage.go b/pkg/storage/storage.go index 80e3231..b2d992e 100644 --- a/pkg/storage/storage.go +++ b/pkg/storage/storage.go @@ -2,57 +2,203 @@ package storage import ( "encoding/json" + "errors" "fmt" "io/fs" + "os" + "path" + "strings" + + "github.com/rs/zerolog/log" +) + +var ( + IndentStr = strings.Repeat(" ", 4) + + ErrNoSuchKey = errors.New("no such key in store") +) + +const ( + SecretMode os.FileMode = 0600 + DefaultMode os.FileMode = 0644 ) type Data interface { } -type Item struct { +type item struct { Version int `json:"version"` MinorVersion *int `json:"minor_version,omitempty"` Key string `json:"key"` - Data Data `json:"data"` + Data interface{} `json:"data"` + + fmode os.FileMode + dirty bool } -type store struct { +type Item interface { + Dirty() + IsDirty() bool + GetData() interface{} + SetData(interface{}) + ItemKey() string +} + +func (i *item) Dirty() { i.dirty = true } +func (i *item) IsDirty() bool { return i.dirty } +func (i *item) GetData() interface{} { return i.Data } +func (i *item) SetData(d interface{}) { i.Data = d; i.Dirty() } +func (i *item) ItemKey() string { return i.Key } + +func (it *item) mode() os.FileMode { + if it.fmode != 0 { + return it.fmode + } + + return SecretMode +} + +type fsStore struct { fs.FS + storeRoot string + s map[string]*item } type Store interface { + GetItem(key string, data interface{}) (Item, error) Get(key string, data interface{}) error + Put(key string, version, minorVersion int, secretMode bool, data interface{}) (Item, error) + FlushAll() []error + Flush(key string) error + Shutdown() } -func (s *store) Get(key string, data interface{}) error { - f, err := s.Open(key) +func (s *fsStore) persist(it *item) error { + f, err := os.OpenFile(path.Join(s.storeRoot, it.Key), os.O_WRONLY|os.O_CREATE, it.mode()) if err != nil { return err } defer f.Close() - item := Item{ - Data: data, - } - d := json.NewDecoder(f) - err = d.Decode(&item) - if err != nil { - return err + enc := json.NewEncoder(f) + enc.SetIndent("", IndentStr) + + err = enc.Encode(it) + if err == nil { + it.dirty = false } - if item.Key != key { - return fmt.Errorf("key mismatch '%s' != '%s'", item.Key, key) + return err +} + +func (s *fsStore) Dirty(key string) error { + it, has := s.s[key] + if !has { + return ErrNoSuchKey } + it.dirty = true + return nil } -func Open(dir fs.FS) (*store, error) { - stor, err := fs.Sub(dir, ".storage") +func (s *fsStore) Flush(key string) error { + it, exists := s.s[key] + if !exists { + return ErrNoSuchKey + } + + return s.persist(it) +} + +func (s *fsStore) FlushAll() []error { + var errs []error + for _, it := range s.s { + err := s.persist(it) + if err != nil { + errs = append(errs, fmt.Errorf("store key %s: %w", it.Key, err)) + } + } + + return errs +} + +func (s *fsStore) Shutdown() { + errs := s.FlushAll() + if errs != nil { + log.Error().Errs("errors", errs).Msg("errors persisting store") + } +} + +// Put puts an item into the store. +// NB: Any user of a previous item with this key will now have a dangling reference that will not be persisted. +// It is up to consumers to coordinate against this case! +func (s *fsStore) Put(key string, version, minorVersion int, secretMode bool, data interface{}) (Item, error) { + var mv *int + if minorVersion != 0 { + mv = &minorVersion + } + + mode := DefaultMode + + if secretMode { + mode = SecretMode + } + + it := &item{ + Version: version, + MinorVersion: mv, + Key: key, + Data: data, + + fmode: mode, + dirty: true, + } + + s.s[key] = it + return it, s.persist(it) +} + +func (s *fsStore) Get(key string, data interface{}) error { + _, err := s.GetItem(key, data) + + return err +} + +func (s *fsStore) GetItem(key string, data interface{}) (Item, error) { + f, err := s.Open(key) if err != nil { return nil, err } - return &store{stor}, nil + defer f.Close() + + item := &item{ + Data: data, + } + d := json.NewDecoder(f) + err = d.Decode(item) + if err != nil { + return nil, err + } + + if item.Key != key { + return nil, fmt.Errorf("key mismatch '%s' != '%s'", item.Key, key) + } + + s.s[key] = item + + return item, nil +} + +func OpenFileStore(configRoot string) (*fsStore, error) { + storeRoot := path.Join(configRoot, ".storage") + stor := os.DirFS(storeRoot) + + return &fsStore{ + FS: stor, + storeRoot: storeRoot, + s: make(map[string]*item), + }, nil }