WIP: providers and user storage

This commit is contained in:
Daniel Ponte 2022-11-12 15:56:17 -05:00
parent 0358eeac53
commit 90825fa01b
7 changed files with 179 additions and 57 deletions

View file

@ -10,6 +10,8 @@ import (
"github.com/labstack/echo/v4"
"github.com/rs/zerolog/log"
"dynatron.me/x/blasphem/pkg/auth/provider"
"dynatron.me/x/blasphem/pkg/auth/provider/hass"
"dynatron.me/x/blasphem/pkg/frontend"
"dynatron.me/x/blasphem/pkg/storage"
)
@ -23,7 +25,7 @@ type Authenticator struct {
store AuthStore
flows FlowStore
sessions SessionStore
providers map[string]AuthProvider
providers map[string]provider.AuthProvider
}
type AuthError struct {
@ -42,26 +44,32 @@ func (a *Authenticator) InstallRoutes(e *echo.Echo) {
loginFlow := authG.Group("/login_flow") // TODO: add IP address affinity middleware
loginFlow.POST("/:flow_id", a.LoginFlowHandler)
loginFlow.DELETE("/:flow_id", a.LoginFlowDeleteHandler)
}
func (a *Authenticator) InitAuth(s storage.Store) error {
a.flows = make(FlowStore)
a.sessions.init()
hap, err := NewHAProvider(s)
hap, err := hass.NewHAProvider(s)
if err != nil {
return err
}
// XXX: yuck. use init with a registry or something
a.providers = map[string]AuthProvider{
a.providers = map[string]provider.AuthProvider{
hap.ProviderType(): hap,
}
a.store, err = a.newAuthStore(s)
if err != nil {
return err
}
return nil
}
func (a *Authenticator) Provider(name string) AuthProvider {
func (a *Authenticator) Provider(name string) provider.AuthProvider {
p, ok := a.providers[name]
if !ok {
return nil
@ -70,42 +78,18 @@ func (a *Authenticator) Provider(name string) AuthProvider {
return p
}
type AuthProvider interface { // TODO: this should include stepping
ProviderName() string
ProviderID() *string
ProviderType() string
ProviderBase() AuthProviderBase
FlowSchema() []FlowSchemaItem
ValidateCreds(reqMap map[string]interface{}) (user ProviderUser, success bool)
}
type ProviderUser interface {
ProviderUsername() string
}
type AuthProviderBase struct {
Name string `json:"name"`
ID *string `json:"id"`
Type string `json:"type"`
}
func (bp *AuthProviderBase) ProviderName() string { return bp.Name }
func (bp *AuthProviderBase) ProviderID() *string { return bp.ID }
func (bp *AuthProviderBase) ProviderType() string { return bp.Type }
func (bp *AuthProviderBase) ProviderBase() AuthProviderBase { return *bp }
var HomeAssistant = "homeassistant"
// TODO: make this configurable
func (a *Authenticator) ProvidersHandler(c echo.Context) error {
providers := []AuthProviderBase{
providers := []provider.AuthProviderBase{
a.Provider(HomeAssistant).ProviderBase(),
}
return c.JSON(http.StatusOK, providers)
}
func (a *Authenticator) Check(f *Flow, rm map[string]interface{}) (ProviderUser, error) {
func (a *Authenticator) Check(f *Flow, rm map[string]interface{}) (provider.ProviderUser, error) {
cID, hasCID := rm["client_id"]
cIDStr, cidIsStr := cID.(string)
if !hasCID || !cidIsStr || cIDStr == "" || cIDStr != string(f.request.ClientID) {
@ -125,7 +109,7 @@ func (a *Authenticator) Check(f *Flow, rm map[string]interface{}) (ProviderUser,
user, success := p.ValidateCreds(rm)
if success {
log.Info().Str("user", user.ProviderUsername()).Msg("Login success")
log.Info().Interface("user", user.ProviderUserData()).Msg("Login success")
return user, nil
}
}

View file

@ -9,6 +9,7 @@ import (
"github.com/labstack/echo/v4"
"dynatron.me/x/blasphem/internal/common"
"dynatron.me/x/blasphem/pkg/auth/provider"
)
type FlowStore map[FlowID]*Flow
@ -19,12 +20,6 @@ type FlowRequest struct {
RedirectURI string `json:"redirect_uri"`
}
type FlowSchemaItem struct {
Type string `json:"type"`
Name string `json:"name"`
Required bool `json:"required"`
}
type FlowType string
const (
@ -44,7 +39,7 @@ type Flow struct {
ID FlowID `json:"flow_id"`
Handler []*string `json:"handler"`
StepID *Step `json:"step_id,omitempty"`
Schema []FlowSchemaItem `json:"data_schema"`
Schema []provider.FlowSchemaItem `json:"data_schema"`
Errors interface{} `json:"errors"`
DescPlace *string `json:"description_placeholders"`
LastStep *string `json:"last_step"`
@ -86,7 +81,7 @@ func (fs FlowStore) Get(id FlowID) *Flow {
}
func (a *Authenticator) NewFlow(r *FlowRequest) *Flow {
var sch []FlowSchemaItem
var sch []provider.FlowSchemaItem
for _, h := range r.Handler {
if h == nil {

View file

@ -1,4 +1,4 @@
package auth
package hass
import (
"encoding/base64"
@ -6,6 +6,7 @@ import (
"github.com/rs/zerolog/log"
"golang.org/x/crypto/bcrypt"
"dynatron.me/x/blasphem/pkg/auth/provider"
"dynatron.me/x/blasphem/pkg/storage"
)
@ -16,18 +17,32 @@ const (
type HAUser struct {
Password string `json:"password"`
Username string `json:"username"`
provider.AuthProvider `json:"-"`
}
func (h *HAUser) ProviderUsername() string { return h.Username }
func (hau *HAUser) UserData() interface{} {
return UserData{
Username: hau.Username,
}
}
type UserData struct {
Username string `json:"username"`
}
const HomeAssistant = "homeassistant"
func (h *HAUser) ProviderUserData() interface{} { return h.UserData() }
type HomeAssistantProvider struct {
AuthProviderBase `json:"-"`
provider.AuthProviderBase `json:"-"`
Users []HAUser `json:"users"`
}
func NewHAProvider(s storage.Store) (*HomeAssistantProvider, error) {
hap := &HomeAssistantProvider{
AuthProviderBase: AuthProviderBase{
AuthProviderBase: provider.AuthProviderBase{
Name: "Home Assistant Local",
Type: HomeAssistant,
},
@ -38,6 +53,10 @@ func NewHAProvider(s storage.Store) (*HomeAssistantProvider, error) {
return hap, err
}
for i := range hap.Users {
hap.Users[i].AuthProvider = hap
}
return hap, nil
}
@ -45,7 +64,7 @@ func (hap *HomeAssistantProvider) hashPass(p string) ([]byte, error) {
return bcrypt.GenerateFromPassword([]byte(p), bcrypt.DefaultCost)
}
func (hap *HomeAssistantProvider) ValidateCreds(rm map[string]interface{}) (ProviderUser, bool) {
func (hap *HomeAssistantProvider) ValidateCreds(rm map[string]interface{}) (provider.ProviderUser, bool) {
usernameE, hasU := rm["username"]
passwordE, hasP := rm["password"]
username, unStr := usernameE.(string)
@ -84,8 +103,12 @@ func (hap *HomeAssistantProvider) ValidateCreds(rm map[string]interface{}) (Prov
return nil, false
}
func (hap *HomeAssistantProvider) FlowSchema() []FlowSchemaItem {
return []FlowSchemaItem{
func (hap *HomeAssistantProvider) NewCredData() interface{} {
return &UserData{}
}
func (hap *HomeAssistantProvider) FlowSchema() []provider.FlowSchemaItem {
return []provider.FlowSchemaItem{
{
Type: "string",
Name: "username",

View file

@ -0,0 +1,39 @@
package provider
type AuthProvider interface { // TODO: this should include stepping
AuthProviderMetadata
ProviderBase() AuthProviderBase
FlowSchema() []FlowSchemaItem
NewCredData() interface{}
ValidateCreds(reqMap map[string]interface{}) (user ProviderUser, success bool)
}
type ProviderUser interface {
AuthProviderMetadata
ProviderUserData() interface{}
}
type AuthProviderBase struct {
Name string `json:"name"`
ID *string `json:"id"`
Type string `json:"type"`
}
type AuthProviderMetadata interface {
ProviderName() string
ProviderID() *string
ProviderType() string
}
func (bp *AuthProviderBase) ProviderName() string { return bp.Name }
func (bp *AuthProviderBase) ProviderID() *string { return bp.ID }
func (bp *AuthProviderBase) ProviderType() string { return bp.Type }
func (bp *AuthProviderBase) ProviderBase() AuthProviderBase { return *bp }
type FlowSchemaItem struct {
Type string `json:"type"`
Name string `json:"name"`
Required bool `json:"required"`
}

View file

@ -1,10 +1,13 @@
package auth
import (
"encoding/json"
"net/http"
"time"
"github.com/labstack/echo/v4"
"dynatron.me/x/blasphem/pkg/auth/provider"
)
type SessionStore struct {
@ -25,7 +28,7 @@ type Token struct { // TODO: jwt bro
Expires time.Time
Addr string
user ProviderUser `json:"-"`
user provider.ProviderUser `json:"-"`
}
func (ss *SessionStore) init() {
@ -49,7 +52,7 @@ func (ss *SessionStore) register(t *Token) {
ss.s[t.ID] = t
}
func (ss *SessionStore) verify(tr *TokenRequest, r *http.Request) (ProviderUser, bool) {
func (ss *SessionStore) verify(tr *TokenRequest, r *http.Request) (provider.ProviderUser, bool) {
if t, hasToken := ss.s[tr.Code]; hasToken {
// TODO: JWT
if t.Expires.After(time.Now()) {
@ -63,7 +66,28 @@ func (ss *SessionStore) verify(tr *TokenRequest, r *http.Request) (ProviderUser,
}
type Credential struct {
user ProviderUser
ID CredID `json:"id"`
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
}
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(),
}
providerData := cred.user.ProviderUserData()
if providerData != nil {
rm["data"] = providerData
}
return json.Marshal(rm)
}
func (ss *SessionStore) verifyAndGetCredential(tr *TokenRequest, r *http.Request) *Credential {
@ -77,7 +101,7 @@ func (ss *SessionStore) verifyAndGetCredential(tr *TokenRequest, r *http.Request
const defaultExpiration = 2 * time.Hour
func (a *Authenticator) NewToken(r *http.Request, user ProviderUser, f *Flow) TokenID {
func (a *Authenticator) NewToken(r *http.Request, user provider.ProviderUser, f *Flow) TokenID {
id := TokenID(genUUID())
t := &Token{

51
pkg/auth/store.go Normal file
View file

@ -0,0 +1,51 @@
package auth
import (
"fmt"
"encoding/json"
"dynatron.me/x/blasphem/pkg/storage"
)
const (
AuthStoreKey = "auth"
)
type AuthStore interface {
}
type authStore struct {
Users []User `json:"users"`
Groups interface {} `json:"groups"`
Credentials []Credential `json:"credentials"`
userMap map[UserID]*User
}
func (a *Authenticator) newAuthStore(s storage.Store) (as *authStore, err error) {
as = &authStore{}
err = s.Get(AuthStoreKey, as)
as.userMap = make(map[UserID]*User)
for _, u := range as.Users {
as.userMap[u.ID] = &u
}
for _, c := range as.Credentials {
prov := a.Provider(c.AuthProviderType)
if prov == nil {
return nil, fmt.Errorf("no such provider %s", c.AuthProviderType)
}
pd := prov.NewCredData()
err := json.Unmarshal(c.DataRaw, pd)
if err != nil {
return nil, err
}
}
return
}

View file

@ -6,17 +6,23 @@ import (
"github.com/rs/zerolog/log"
)
const (
AuthKey = "auth"
)
type UserID string
type GroupID string
type CredID string
type User struct {
Username string
ID UserID `json:"id"`
GroupIDs []GroupID `json:"group_ids"`
Data interface{} `json:"data,omitempty"`
UserMetadata
}
type UserMetadata struct {
Active bool
Active bool `json:"is_active"`
Owner bool `json:"is_owner"`
LocalOnly bool `json:"local_only"`
SystemGenerated bool `json:"system_generated"`
Name string `json:"name"`
}
func (u *User) allowedToAuth() error {
@ -28,7 +34,7 @@ func (u *User) allowedToAuth() error {
}
func (a *Authenticator) getOrCreateUser(c *Credential) (*User, error) {
log.Debug().Str("user", c.user.ProviderUsername()).Msg("getOrCreateUser")
log.Debug().Interface("userdata", c.user.ProviderUserData()).Msg("getOrCreateUser")
panic("not implemented")
return &User{}, nil
}