Compare commits
4 commits
c618197c54
...
86abb0b618
Author | SHA1 | Date | |
---|---|---|---|
86abb0b618 | |||
d34814f050 | |||
f3e17e149f | |||
ae00c1534d |
10 changed files with 343 additions and 159 deletions
|
@ -15,6 +15,13 @@ type cmdOptions interface {
|
||||||
Execute() error
|
Execute() error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func AppNamePtr() *string {
|
||||||
|
s := AppName
|
||||||
|
return &s
|
||||||
|
}
|
||||||
|
|
||||||
|
func IntPtr(i int) *int { return &i }
|
||||||
|
|
||||||
// RunE is a convenience function for use with cobra.
|
// RunE is a convenience function for use with cobra.
|
||||||
func RunE(c cmdOptions) func(cmd *cobra.Command, args []string) error {
|
func RunE(c cmdOptions) func(cmd *cobra.Command, args []string) error {
|
||||||
return func(cmd *cobra.Command, args []string) error {
|
return func(cmd *cobra.Command, args []string) error {
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
type (
|
type (
|
||||||
// PyTimeStamp is a timestamp that marshals to python-style timestamp strings (long nano).
|
// PyTimeStamp is a timestamp that marshals to python-style timestamp strings (long nano).
|
||||||
PyTimestamp time.Time
|
PyTimestamp time.Time
|
||||||
|
ClientID string
|
||||||
)
|
)
|
||||||
|
|
||||||
const PytTimeFormat = "2006-01-02T15:04:05.999999-07:00"
|
const PytTimeFormat = "2006-01-02T15:04:05.999999-07:00"
|
||||||
|
|
|
@ -24,7 +24,7 @@ var (
|
||||||
|
|
||||||
type Authenticator struct {
|
type Authenticator struct {
|
||||||
store AuthStore
|
store AuthStore
|
||||||
flows FlowStore
|
flows *AuthFlowManager
|
||||||
sessions AccessSessionStore
|
sessions AccessSessionStore
|
||||||
providers map[string]provider.AuthProvider
|
providers map[string]provider.AuthProvider
|
||||||
}
|
}
|
||||||
|
@ -58,7 +58,7 @@ func (a *Authenticator) InitAuth(s storage.Store) error {
|
||||||
a.providers[nProv.ProviderType()] = nProv
|
a.providers[nProv.ProviderType()] = nProv
|
||||||
}
|
}
|
||||||
|
|
||||||
a.flows = make(FlowStore)
|
a.flows = NewAuthFlowManager()
|
||||||
|
|
||||||
a.sessions.init()
|
a.sessions.init()
|
||||||
|
|
||||||
|
@ -91,19 +91,14 @@ func (a *Authenticator) ProvidersHandler(c echo.Context) error {
|
||||||
return c.JSON(http.StatusOK, providers)
|
return c.JSON(http.StatusOK, providers)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Authenticator) Check(f *Flow, req *http.Request, rm map[string]interface{}) (provider.ProviderUser, error) {
|
func (a *Authenticator) Check(f *LoginFlow, req *http.Request, rm map[string]interface{}) (provider.ProviderUser, error) {
|
||||||
cID, hasCID := rm["client_id"]
|
cID, hasCID := rm["client_id"]
|
||||||
cIDStr, cidIsStr := cID.(string)
|
cIDStr, cidIsStr := cID.(string)
|
||||||
if !hasCID || !cidIsStr || cIDStr == "" || cIDStr != string(f.request.ClientID) {
|
if !hasCID || !cidIsStr || cIDStr == "" || cIDStr != string(f.ClientID) {
|
||||||
return nil, ErrInvalidAuth
|
return nil, ErrInvalidAuth
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, h := range f.Handler {
|
p := a.Provider(f.Handler.String())
|
||||||
if h == nil {
|
|
||||||
return nil, ErrInvalidHandler
|
|
||||||
}
|
|
||||||
|
|
||||||
p := a.Provider(*h)
|
|
||||||
if p == nil {
|
if p == nil {
|
||||||
return nil, ErrInvalidAuth
|
return nil, ErrInvalidAuth
|
||||||
}
|
}
|
||||||
|
@ -114,7 +109,6 @@ func (a *Authenticator) Check(f *Flow, req *http.Request, rm map[string]interfac
|
||||||
log.Info().Interface("user", user.UserData()).Msg("Login success")
|
log.Info().Interface("user", user.UserData()).Msg("Login success")
|
||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return nil, ErrInvalidAuth
|
return nil, ErrInvalidAuth
|
||||||
}
|
}
|
||||||
|
|
193
pkg/auth/flow.go
193
pkg/auth/flow.go
|
@ -3,132 +3,95 @@ package auth
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/jinzhu/copier"
|
"github.com/jinzhu/copier"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
|
|
||||||
"dynatron.me/x/blasphem/internal/common"
|
"dynatron.me/x/blasphem/internal/common"
|
||||||
"dynatron.me/x/blasphem/internal/generate"
|
|
||||||
"dynatron.me/x/blasphem/pkg/auth/provider"
|
"dynatron.me/x/blasphem/pkg/auth/provider"
|
||||||
|
"dynatron.me/x/blasphem/pkg/flow"
|
||||||
)
|
)
|
||||||
|
|
||||||
type FlowStore map[FlowID]*Flow
|
type AuthFlowManager struct {
|
||||||
|
*flow.FlowManager
|
||||||
|
}
|
||||||
|
|
||||||
type FlowRequest struct {
|
type LoginFlow struct {
|
||||||
ClientID ClientID `json:"client_id"`
|
flow.FlowHandler
|
||||||
|
|
||||||
|
ClientID common.ClientID
|
||||||
|
FlowContext
|
||||||
|
}
|
||||||
|
|
||||||
|
type FlowContext struct {
|
||||||
|
IPAddr string
|
||||||
|
CredentialOnly bool
|
||||||
|
RedirectURI string
|
||||||
|
}
|
||||||
|
|
||||||
|
type LoginFlowRequest struct {
|
||||||
|
ClientID common.ClientID `json:"client_id"`
|
||||||
Handler []*string `json:"handler"`
|
Handler []*string `json:"handler"`
|
||||||
RedirectURI string `json:"redirect_uri"`
|
RedirectURI string `json:"redirect_uri"`
|
||||||
|
Type *string `json:"type"`
|
||||||
|
|
||||||
|
ip string `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FlowType string
|
func (r *LoginFlowRequest) FlowContext() FlowContext {
|
||||||
|
return FlowContext{
|
||||||
const (
|
IPAddr: r.ip,
|
||||||
TypeForm FlowType = "form"
|
RedirectURI: r.RedirectURI,
|
||||||
TypeCreateEntry FlowType = "create_entry"
|
CredentialOnly: r.Type != nil && *r.Type == "link_user",
|
||||||
)
|
|
||||||
|
|
||||||
type FlowID string
|
|
||||||
type Step string
|
|
||||||
|
|
||||||
const (
|
|
||||||
StepInit Step = "init"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Flow struct {
|
|
||||||
Type FlowType `json:"type"`
|
|
||||||
ID FlowID `json:"flow_id"`
|
|
||||||
Handler []*string `json:"handler"`
|
|
||||||
StepID *Step `json:"step_id,omitempty"`
|
|
||||||
Schema []provider.FlowSchemaItem `json:"data_schema"`
|
|
||||||
Errors interface{} `json:"errors"`
|
|
||||||
DescPlace *string `json:"description_placeholders"`
|
|
||||||
LastStep *string `json:"last_step"`
|
|
||||||
|
|
||||||
request *FlowRequest
|
|
||||||
ctime time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Flow) touch() {
|
|
||||||
f.ctime = time.Now()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fs FlowStore) register(f *Flow) {
|
|
||||||
fs.cull()
|
|
||||||
fs[f.ID] = f
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fs FlowStore) Remove(f *Flow) {
|
|
||||||
delete(fs, f.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
const cullAge = time.Minute * 30
|
|
||||||
|
|
||||||
func (fs FlowStore) cull() {
|
|
||||||
for k, v := range fs {
|
|
||||||
if time.Now().Sub(v.ctime) > cullAge {
|
|
||||||
delete(fs, k)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewAuthFlowManager() *AuthFlowManager {
|
||||||
|
return &AuthFlowManager{FlowManager: flow.NewFlowManager()}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fs FlowStore) Get(id FlowID) *Flow {
|
func (afm *AuthFlowManager) NewLoginFlow(req *LoginFlowRequest, prov provider.AuthProvider) *LoginFlow {
|
||||||
f, ok := fs[id]
|
lf := &LoginFlow{
|
||||||
if ok {
|
FlowHandler: flow.NewFlowHandlerBase(prov, prov.ProviderType()),
|
||||||
return f
|
ClientID: req.ClientID,
|
||||||
|
FlowContext: req.FlowContext(),
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
afm.Register(lf)
|
||||||
|
|
||||||
|
return lf
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Authenticator) NewFlow(r *FlowRequest) *Flow {
|
func (a *Authenticator) NewFlow(r *LoginFlowRequest) *flow.Result {
|
||||||
var sch []provider.FlowSchemaItem
|
var prov provider.AuthProvider
|
||||||
|
|
||||||
for _, h := range r.Handler {
|
for _, h := range r.Handler {
|
||||||
if h == nil {
|
if h == nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
if hand := a.Provider(*h); hand != nil {
|
prov = a.Provider(*h)
|
||||||
sch = hand.FlowSchema()
|
if prov != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if sch == nil {
|
if prov == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
flow := &Flow{
|
flow := a.flows.NewLoginFlow(r, prov)
|
||||||
Type: TypeForm,
|
|
||||||
ID: FlowID(generate.UUID()),
|
|
||||||
StepID: stepPtr(StepInit),
|
|
||||||
Schema: sch,
|
|
||||||
Handler: r.Handler,
|
|
||||||
Errors: []string{},
|
|
||||||
request: r,
|
|
||||||
}
|
|
||||||
flow.touch()
|
|
||||||
|
|
||||||
a.flows.register(flow)
|
return flow.ShowForm(nil)
|
||||||
|
|
||||||
return flow
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func stepPtr(s Step) *Step { return &s }
|
func (f *LoginFlow) redirect(c echo.Context) {
|
||||||
|
c.Request().Header.Set("Location", f.RedirectURI)
|
||||||
func (f *Flow) redirect(c echo.Context) {
|
|
||||||
c.Request().Header.Set("Location", f.request.RedirectURI)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Flow) progress(a *Authenticator, c echo.Context) error {
|
func (f *LoginFlow) progress(a *Authenticator, c echo.Context) error {
|
||||||
if f.StepID == nil {
|
switch f.Step() {
|
||||||
c.Logger().Error("stepID is nil")
|
case flow.StepInit:
|
||||||
return c.String(http.StatusInternalServerError, "No Step ID")
|
|
||||||
}
|
|
||||||
|
|
||||||
switch *f.StepID {
|
|
||||||
case StepInit:
|
|
||||||
rm := make(map[string]interface{})
|
rm := make(map[string]interface{})
|
||||||
|
|
||||||
err := c.Bind(&rm)
|
err := c.Bind(&rm)
|
||||||
|
@ -136,31 +99,21 @@ func (f *Flow) progress(a *Authenticator, c echo.Context) error {
|
||||||
return c.String(http.StatusBadRequest, err.Error())
|
return c.String(http.StatusBadRequest, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, si := range f.Schema {
|
err = f.Schema.CheckRequired(rm)
|
||||||
if si.Required {
|
if err != nil {
|
||||||
if _, ok := rm[si.Name]; !ok {
|
return c.JSON(http.StatusBadRequest, f.ShowForm([]string{err.Error()}))
|
||||||
return c.String(http.StatusBadRequest, "missing required param "+si.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := a.Check(f, c.Request(), rm)
|
user, err := a.Check(f, c.Request(), rm)
|
||||||
switch err {
|
switch err {
|
||||||
case nil:
|
case nil:
|
||||||
var finishedFlow struct {
|
finishedFlow := flow.Result{}
|
||||||
ID FlowID `json:"flow_id"`
|
|
||||||
Handler []*string `json:"handler"`
|
|
||||||
Result AccessTokenID `json:"result"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Type FlowType `json:"type"`
|
|
||||||
Version int `json:"version"`
|
|
||||||
}
|
|
||||||
|
|
||||||
a.flows.Remove(f)
|
a.flows.Remove(f)
|
||||||
copier.Copy(&finishedFlow, f)
|
copier.Copy(&finishedFlow, f)
|
||||||
finishedFlow.Type = TypeCreateEntry
|
finishedFlow.Type = flow.TypeCreateEntry
|
||||||
finishedFlow.Title = common.AppName
|
finishedFlow.Title = common.AppNamePtr()
|
||||||
finishedFlow.Version = 1
|
finishedFlow.Version = common.IntPtr(1)
|
||||||
finishedFlow.Result = a.NewAccessToken(c.Request(), user, f)
|
finishedFlow.Result = a.NewAccessToken(c.Request(), user)
|
||||||
|
|
||||||
f.redirect(c)
|
f.redirect(c)
|
||||||
|
|
||||||
|
@ -170,24 +123,26 @@ func (f *Flow) progress(a *Authenticator, c echo.Context) error {
|
||||||
case ErrInvalidAuth:
|
case ErrInvalidAuth:
|
||||||
fallthrough
|
fallthrough
|
||||||
default:
|
default:
|
||||||
f.Errors = map[string]interface{}{
|
return c.JSON(http.StatusOK, f.ShowForm(map[string]interface{}{
|
||||||
"base": "invalid_auth",
|
"base": "invalid_auth",
|
||||||
}
|
}))
|
||||||
return c.JSON(http.StatusOK, f)
|
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return c.String(http.StatusBadRequest, "unknown flow step")
|
return c.JSON(http.StatusOK, f.ShowForm(map[string]interface{}{
|
||||||
|
"base": "unknown_flow_step",
|
||||||
|
}))
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Authenticator) LoginFlowDeleteHandler(c echo.Context) error {
|
func (a *Authenticator) LoginFlowDeleteHandler(c echo.Context) error {
|
||||||
flowID := c.Param("flow_id")
|
flowID := flow.FlowID(c.Param("flow_id"))
|
||||||
|
|
||||||
if flowID == "" {
|
if flowID == "" {
|
||||||
return c.String(http.StatusBadRequest, "empty flow ID")
|
return c.String(http.StatusBadRequest, "empty flow ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(a.flows, FlowID(flowID))
|
a.flows.Delete(flowID)
|
||||||
|
|
||||||
return c.String(http.StatusOK, "deleted")
|
return c.String(http.StatusOK, "deleted")
|
||||||
}
|
}
|
||||||
|
@ -202,12 +157,14 @@ func setJSON(c echo.Context) {
|
||||||
func (a *Authenticator) BeginLoginFlowHandler(c echo.Context) error {
|
func (a *Authenticator) BeginLoginFlowHandler(c echo.Context) error {
|
||||||
setJSON(c)
|
setJSON(c)
|
||||||
|
|
||||||
var flowReq FlowRequest
|
var flowReq LoginFlowRequest
|
||||||
err := c.Bind(&flowReq)
|
err := c.Bind(&flowReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.String(http.StatusBadRequest, err.Error())
|
return c.String(http.StatusBadRequest, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
flowReq.ip = c.Request().RemoteAddr
|
||||||
|
|
||||||
resp := a.NewFlow(&flowReq)
|
resp := a.NewFlow(&flowReq)
|
||||||
|
|
||||||
if resp == nil {
|
if resp == nil {
|
||||||
|
@ -222,16 +179,10 @@ func (a *Authenticator) LoginFlowHandler(c echo.Context) error {
|
||||||
|
|
||||||
flowID := c.Param("flow_id")
|
flowID := c.Param("flow_id")
|
||||||
|
|
||||||
flow := a.flows.Get(FlowID(flowID))
|
flow := a.flows.Get(flow.FlowID(flowID))
|
||||||
if flow == nil {
|
if flow == nil {
|
||||||
return c.String(http.StatusNotFound, "no such flow")
|
return c.String(http.StatusNotFound, "no such flow")
|
||||||
}
|
}
|
||||||
|
|
||||||
if time.Now().Sub(flow.ctime) > cullAge {
|
return flow.(*LoginFlow).progress(a, c)
|
||||||
a.flows.Remove(flow)
|
|
||||||
|
|
||||||
return c.String(http.StatusGone, "flow timed out")
|
|
||||||
}
|
|
||||||
|
|
||||||
return flow.progress(a, c)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
|
||||||
"dynatron.me/x/blasphem/pkg/auth/provider"
|
"dynatron.me/x/blasphem/pkg/auth/provider"
|
||||||
|
"dynatron.me/x/blasphem/pkg/flow"
|
||||||
"dynatron.me/x/blasphem/pkg/storage"
|
"dynatron.me/x/blasphem/pkg/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -24,7 +25,7 @@ type HAUser struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (hau *HAUser) UserData() provider.ProviderUser {
|
func (hau *HAUser) UserData() provider.ProviderUser {
|
||||||
return &UserData{
|
return &UserData{ // strip secret
|
||||||
Username: hau.Username,
|
Username: hau.Username,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -44,6 +45,7 @@ func (h *HAUser) ProviderUserData() interface{} { return h.UserData() }
|
||||||
type HomeAssistantProvider struct {
|
type HomeAssistantProvider struct {
|
||||||
provider.AuthProviderBase `json:"-"`
|
provider.AuthProviderBase `json:"-"`
|
||||||
Users []HAUser `json:"users"`
|
Users []HAUser `json:"users"`
|
||||||
|
userMap map[string]*HAUser
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHAProvider(s storage.Store) (provider.AuthProvider, error) {
|
func NewHAProvider(s storage.Store) (provider.AuthProvider, error) {
|
||||||
|
@ -59,13 +61,25 @@ func NewHAProvider(s storage.Store) (provider.AuthProvider, error) {
|
||||||
return hap, err
|
return hap, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range hap.Users {
|
hap.userMap = make(map[string]*HAUser)
|
||||||
|
|
||||||
|
for i, u := range hap.Users {
|
||||||
hap.Users[i].AuthProvider = hap
|
hap.Users[i].AuthProvider = hap
|
||||||
|
hap.userMap[u.Username] = &hap.Users[i]
|
||||||
}
|
}
|
||||||
|
|
||||||
return hap, nil
|
return hap, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (hap *HomeAssistantProvider) Lookup(pu provider.ProviderUser) provider.ProviderUser {
|
||||||
|
u, has := hap.userMap[pu.(*HAUser).Username]
|
||||||
|
if !has {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
func (hap *HomeAssistantProvider) hashPass(p string) ([]byte, error) {
|
func (hap *HomeAssistantProvider) hashPass(p string) ([]byte, error) {
|
||||||
return bcrypt.GenerateFromPassword([]byte(p), bcrypt.DefaultCost)
|
return bcrypt.GenerateFromPassword([]byte(p), bcrypt.DefaultCost)
|
||||||
}
|
}
|
||||||
|
@ -110,11 +124,11 @@ func (hap *HomeAssistantProvider) ValidateCreds(r *http.Request, rm map[string]i
|
||||||
}
|
}
|
||||||
|
|
||||||
func (hap *HomeAssistantProvider) NewCredData() interface{} {
|
func (hap *HomeAssistantProvider) NewCredData() interface{} {
|
||||||
return &UserData{}
|
return &HAUser{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (hap *HomeAssistantProvider) FlowSchema() []provider.FlowSchemaItem {
|
func (hap *HomeAssistantProvider) FlowSchema() flow.Schema {
|
||||||
return []provider.FlowSchemaItem{
|
return []flow.SchemaItem{
|
||||||
{
|
{
|
||||||
Type: "string",
|
Type: "string",
|
||||||
Name: "username",
|
Name: "username",
|
||||||
|
|
|
@ -3,6 +3,7 @@ package provider
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"dynatron.me/x/blasphem/pkg/flow"
|
||||||
"dynatron.me/x/blasphem/pkg/storage"
|
"dynatron.me/x/blasphem/pkg/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -13,9 +14,10 @@ var Providers = make(map[string]Constructor)
|
||||||
type AuthProvider interface { // TODO: this should include stepping
|
type AuthProvider interface { // TODO: this should include stepping
|
||||||
AuthProviderMetadata
|
AuthProviderMetadata
|
||||||
ProviderBase() AuthProviderBase
|
ProviderBase() AuthProviderBase
|
||||||
FlowSchema() []FlowSchemaItem
|
FlowSchema() flow.Schema
|
||||||
NewCredData() interface{}
|
NewCredData() interface{}
|
||||||
ValidateCreds(r *http.Request, reqMap map[string]interface{}) (user ProviderUser, success bool)
|
ValidateCreds(r *http.Request, reqMap map[string]interface{}) (user ProviderUser, success bool)
|
||||||
|
Lookup(ProviderUser) ProviderUser
|
||||||
}
|
}
|
||||||
|
|
||||||
func Register(providerName string, f func(storage.Store) (AuthProvider, error)) {
|
func Register(providerName string, f func(storage.Store) (AuthProvider, error)) {
|
||||||
|
@ -23,6 +25,7 @@ func Register(providerName string, f func(storage.Store) (AuthProvider, error))
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProviderUser interface {
|
type ProviderUser interface {
|
||||||
|
// TODO: make sure this is sane with all the ProviderUser and UserData type stuff
|
||||||
UserData() ProviderUser
|
UserData() ProviderUser
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,9 +45,3 @@ func (bp *AuthProviderBase) ProviderName() string { return bp.Name }
|
||||||
func (bp *AuthProviderBase) ProviderID() *string { return bp.ID }
|
func (bp *AuthProviderBase) ProviderID() *string { return bp.ID }
|
||||||
func (bp *AuthProviderBase) ProviderType() string { return bp.Type }
|
func (bp *AuthProviderBase) ProviderType() string { return bp.Type }
|
||||||
func (bp *AuthProviderBase) ProviderBase() AuthProviderBase { return *bp }
|
func (bp *AuthProviderBase) ProviderBase() AuthProviderBase { return *bp }
|
||||||
|
|
||||||
type FlowSchemaItem struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Required bool `json:"required"`
|
|
||||||
}
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"dynatron.me/x/blasphem/pkg/auth/provider"
|
"dynatron.me/x/blasphem/pkg/auth/provider"
|
||||||
|
"dynatron.me/x/blasphem/pkg/flow"
|
||||||
"dynatron.me/x/blasphem/pkg/storage"
|
"dynatron.me/x/blasphem/pkg/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -46,6 +47,10 @@ func New(s storage.Store) (provider.AuthProvider, error) {
|
||||||
return hap, nil
|
return hap, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (tnp *TrustedNetworksProvider) Lookup(pu provider.ProviderUser) provider.ProviderUser {
|
||||||
|
return pu
|
||||||
|
}
|
||||||
|
|
||||||
func (hap *TrustedNetworksProvider) ValidateCreds(r *http.Request, rm map[string]interface{}) (provider.ProviderUser, bool) {
|
func (hap *TrustedNetworksProvider) ValidateCreds(r *http.Request, rm map[string]interface{}) (provider.ProviderUser, bool) {
|
||||||
/*
|
/*
|
||||||
if req.RemoteAddr in allowed then do the thing
|
if req.RemoteAddr in allowed then do the thing
|
||||||
|
@ -57,8 +62,8 @@ func (hap *TrustedNetworksProvider) NewCredData() interface{} {
|
||||||
return &UserData{}
|
return &UserData{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (hap *TrustedNetworksProvider) FlowSchema() []provider.FlowSchemaItem {
|
func (hap *TrustedNetworksProvider) FlowSchema() flow.Schema {
|
||||||
return []provider.FlowSchemaItem{
|
return []flow.SchemaItem{
|
||||||
{
|
{
|
||||||
Type: "string",
|
Type: "string",
|
||||||
Name: "username",
|
Name: "username",
|
||||||
|
|
|
@ -124,16 +124,14 @@ func (a *Authenticator) verifyAndGetCredential(tr *TokenRequest, r *http.Request
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
cred := &Credential{
|
cred := a.store.Credential(user)
|
||||||
user: user,
|
|
||||||
}
|
|
||||||
|
|
||||||
return cred
|
return cred
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultExpiration = 15 * time.Minute
|
const defaultExpiration = 15 * time.Minute
|
||||||
|
|
||||||
func (a *Authenticator) NewAccessToken(r *http.Request, user provider.ProviderUser, f *Flow) AccessTokenID {
|
func (a *Authenticator) NewAccessToken(r *http.Request, user provider.ProviderUser) AccessTokenID {
|
||||||
id := AccessTokenID(generate.UUID())
|
id := AccessTokenID(generate.UUID())
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
|
||||||
|
@ -158,7 +156,7 @@ const (
|
||||||
GTRefreshToken GrantType = "refresh_token"
|
GTRefreshToken GrantType = "refresh_token"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ClientID string
|
type ClientID common.ClientID
|
||||||
|
|
||||||
func (c *ClientID) IsValid() bool {
|
func (c *ClientID) IsValid() bool {
|
||||||
// TODO: || !indieauth.VerifyClientID(rq.ClientID)?
|
// TODO: || !indieauth.VerifyClientID(rq.ClientID)?
|
||||||
|
|
|
@ -14,6 +14,7 @@ const (
|
||||||
|
|
||||||
type AuthStore interface {
|
type AuthStore interface {
|
||||||
User(UserID) *User
|
User(UserID) *User
|
||||||
|
Credential(provider.ProviderUser) *Credential
|
||||||
}
|
}
|
||||||
|
|
||||||
type authStore struct {
|
type authStore struct {
|
||||||
|
@ -23,6 +24,16 @@ type authStore struct {
|
||||||
Refresh []RefreshToken `json:"refresh_tokens"`
|
Refresh []RefreshToken `json:"refresh_tokens"`
|
||||||
|
|
||||||
userMap map[UserID]*User
|
userMap map[UserID]*User
|
||||||
|
providerUsers map[provider.ProviderUser]*Credential
|
||||||
|
}
|
||||||
|
|
||||||
|
func (as *authStore) Credential(p provider.ProviderUser) *Credential {
|
||||||
|
c, have := as.providerUsers[p]
|
||||||
|
if !have {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Authenticator) newAuthStore(s storage.Store) (as *authStore, err error) {
|
func (a *Authenticator) newAuthStore(s storage.Store) (as *authStore, err error) {
|
||||||
|
@ -30,6 +41,7 @@ func (a *Authenticator) newAuthStore(s storage.Store) (as *authStore, err error)
|
||||||
err = s.Get(AuthStoreKey, as)
|
err = s.Get(AuthStoreKey, as)
|
||||||
|
|
||||||
as.userMap = make(map[UserID]*User)
|
as.userMap = make(map[UserID]*User)
|
||||||
|
as.providerUsers = make(map[provider.ProviderUser]*Credential)
|
||||||
|
|
||||||
for _, u := range as.Users {
|
for _, u := range as.Users {
|
||||||
as.userMap[u.ID] = &u
|
as.userMap[u.ID] = &u
|
||||||
|
@ -49,7 +61,11 @@ func (a *Authenticator) newAuthStore(s storage.Store) (as *authStore, err error)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
c.user = pd.(provider.ProviderUser)
|
c.user = prov.Lookup(pd.(provider.ProviderUser))
|
||||||
|
if c.user == nil {
|
||||||
|
return nil, fmt.Errorf("cannot find user in provider %s", prov.ProviderName())
|
||||||
|
}
|
||||||
|
as.providerUsers[c.user] = &c
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
201
pkg/flow/flow.go
Normal file
201
pkg/flow/flow.go
Normal file
|
@ -0,0 +1,201 @@
|
||||||
|
// flow is the data entry flow.
|
||||||
|
package flow
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"dynatron.me/x/blasphem/internal/generate"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
ResultType string
|
||||||
|
FlowID string
|
||||||
|
Step string
|
||||||
|
HandlerKey string
|
||||||
|
Errors interface{}
|
||||||
|
|
||||||
|
Context interface{}
|
||||||
|
|
||||||
|
FlowStore map[FlowID]Handler
|
||||||
|
|
||||||
|
FlowManager struct {
|
||||||
|
flows FlowStore
|
||||||
|
}
|
||||||
|
|
||||||
|
Result struct {
|
||||||
|
Type ResultType `json:"type"`
|
||||||
|
ID FlowID `json:"flow_id"`
|
||||||
|
Handler []*HandlerKey `json:"handler"`
|
||||||
|
Title *string `json:"title,omitempty"`
|
||||||
|
Data map[string]interface{} `json:"data,omitempty"`
|
||||||
|
StepID *Step `json:"step_id,omitempty"`
|
||||||
|
Schema []SchemaItem `json:"data_schema"`
|
||||||
|
Extra *string `json:"extra,omitempty"`
|
||||||
|
Required *bool `json:"required,omitempty"`
|
||||||
|
Errors interface{} `json:"errors"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
DescPlace *string `json:"description_placeholders"`
|
||||||
|
URL *string `json:"url,omitempty"`
|
||||||
|
Reason *string `json:"reason,omitempty"`
|
||||||
|
Context *string `json:"context,omitempty"`
|
||||||
|
Result interface{} `json:"result,omitempty"`
|
||||||
|
LastStep *string `json:"last_step"`
|
||||||
|
Options map[string]interface{} `json:"options,omitempty"`
|
||||||
|
Version *int `json:"version,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
SchemaItem struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Required bool `json:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
Schema []SchemaItem
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Schemer interface {
|
||||||
|
FlowSchema() Schema
|
||||||
|
}
|
||||||
|
|
||||||
|
Handler interface {
|
||||||
|
Base() FlowHandler
|
||||||
|
FlowID() FlowID
|
||||||
|
|
||||||
|
flowCtime() time.Time
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
StepInit Step = "init"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (fs *Schema) CheckRequired(rm map[string]interface{}) error {
|
||||||
|
for _, si := range *fs {
|
||||||
|
if si.Required {
|
||||||
|
if _, ok := rm[si.Name]; !ok {
|
||||||
|
return fmt.Errorf("missing required param %s", si.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFlowManager() *FlowManager {
|
||||||
|
return &FlowManager{
|
||||||
|
flows: make(FlowStore),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func stepPtr(s Step) *Step { return &s }
|
||||||
|
|
||||||
|
type FlowHandler struct {
|
||||||
|
ID FlowID // ID is the FlowID
|
||||||
|
Handler HandlerKey // Handler key
|
||||||
|
Context Context // flow Context
|
||||||
|
Schema Schema
|
||||||
|
|
||||||
|
// curStep is the current step set by the flow manager
|
||||||
|
curStep Step
|
||||||
|
|
||||||
|
ctime time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FlowHandler) Step() Step { return f.curStep }
|
||||||
|
|
||||||
|
func (f *FlowHandler) Base() FlowHandler { return *f }
|
||||||
|
|
||||||
|
func (f *FlowHandler) FlowID() FlowID {
|
||||||
|
return f.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FlowHandler) flowCtime() time.Time { return f.ctime }
|
||||||
|
|
||||||
|
func NewFlowHandlerBase(sch Schemer, hand string) FlowHandler {
|
||||||
|
return FlowHandler{
|
||||||
|
ID: FlowID(generate.UUID()),
|
||||||
|
Handler: HandlerKey(hand),
|
||||||
|
Schema: sch.FlowSchema(),
|
||||||
|
|
||||||
|
curStep: StepInit,
|
||||||
|
ctime: time.Now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hk *HandlerKey) String() string {
|
||||||
|
return string(*hk)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fm *FlowHandler) Handlers() []*HandlerKey {
|
||||||
|
return []*HandlerKey{&fm.Handler, nil}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resultErrs(e Errors) Errors {
|
||||||
|
if e == nil {
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fm *FlowHandler) ShowForm(errs Errors) *Result {
|
||||||
|
res := &Result{
|
||||||
|
Type: TypeForm,
|
||||||
|
ID: fm.ID,
|
||||||
|
StepID: stepPtr(fm.curStep),
|
||||||
|
Schema: fm.Schema,
|
||||||
|
Handler: fm.Handlers(),
|
||||||
|
Errors: resultErrs(errs),
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fm *FlowManager) Delete(id FlowID) {
|
||||||
|
delete(fm.flows, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
TypeForm ResultType = "form"
|
||||||
|
TypeCreateEntry ResultType = "create_entry"
|
||||||
|
TypeAbort ResultType = "abort"
|
||||||
|
TypeExternalStep ResultType = "external"
|
||||||
|
TypeExternalStepDone ResultType = "external_done"
|
||||||
|
TypeShowProgress ResultType = "progress"
|
||||||
|
TypeShowProgressDone ResultType = "progress_done"
|
||||||
|
TypeMenu ResultType = "menu"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (f *FlowHandler) touch() {
|
||||||
|
f.ctime = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fm *FlowManager) Register(f Handler) {
|
||||||
|
fm.flows.cull()
|
||||||
|
fm.flows[f.FlowID()] = f
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *FlowManager) Remove(f Handler) {
|
||||||
|
delete(fs.flows, f.FlowID())
|
||||||
|
}
|
||||||
|
|
||||||
|
const cullAge = time.Minute * 30
|
||||||
|
|
||||||
|
func (fs FlowStore) cull() {
|
||||||
|
for k, v := range fs {
|
||||||
|
if time.Now().Sub(v.flowCtime()) > cullAge {
|
||||||
|
delete(fs, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *FlowManager) Get(id FlowID) Handler {
|
||||||
|
f, ok := fs.flows[id]
|
||||||
|
if ok {
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
Loading…
Reference in a new issue