WIP: add casbin to access manager, allow to persist identities

This commit is contained in:
Ingo Oppermann
2023-02-06 17:07:20 +01:00
parent 8f1ff2d1a2
commit 11e55fc2c7
12 changed files with 919 additions and 95 deletions

View File

@@ -383,12 +383,50 @@ func (a *api) start() error {
a.sessions = sessions a.sessions = sessions
} }
iam, err := iam.NewIAM() {
if err != nil { superuser := iam.User{
return fmt.Errorf("iam: %w", err) Name: cfg.API.Auth.Username,
} Superuser: true,
Auth: iam.UserAuth{
API: iam.UserAuthAPI{
Userpass: iam.UserAuthPassword{
Enable: cfg.API.Auth.Enable,
Password: cfg.API.Auth.Password,
},
Auth0: iam.UserAuthAPIAuth0{
Enable: cfg.API.Auth.Auth0.Enable,
User: cfg.API.Auth.Auth0.Tenants[0].Users[0],
Tenant: iam.Auth0Tenant{
Domain: cfg.API.Auth.Auth0.Tenants[0].Domain,
Audience: cfg.API.Auth.Auth0.Tenants[0].Audience,
ClientID: cfg.API.Auth.Auth0.Tenants[0].ClientID,
},
},
},
Services: iam.UserAuthServices{
Basic: iam.UserAuthPassword{
Enable: cfg.Storage.Memory.Auth.Enable,
Password: cfg.Storage.Memory.Auth.Password,
},
Token: cfg.RTMP.Token,
},
},
}
a.iam = iam fs, err := fs.NewRootedDiskFilesystem(fs.RootedDiskConfig{
Root: filepath.Join(cfg.DB.Dir, "iam"),
})
if err != nil {
return err
}
iam, err := iam.NewIAM(fs, superuser)
if err != nil {
return fmt.Errorf("iam: %w", err)
}
a.iam = iam
}
diskfs, err := fs.NewRootedDiskFilesystem(fs.RootedDiskConfig{ diskfs, err := fs.NewRootedDiskFilesystem(fs.RootedDiskConfig{
Root: cfg.Storage.Disk.Dir, Root: cfg.Storage.Disk.Dir,
@@ -639,7 +677,7 @@ func (a *api) start() error {
return fmt.Errorf("unable to create JWT provider: %w", err) return fmt.Errorf("unable to create JWT provider: %w", err)
} }
if validator, err := jwt.NewLocalValidator(cfg.API.Auth.Username, cfg.API.Auth.Password); err == nil { if validator, err := jwt.NewLocalValidator(a.iam); err == nil {
if err := httpjwt.AddValidator(app.Name, validator); err != nil { if err := httpjwt.AddValidator(app.Name, validator); err != nil {
return fmt.Errorf("unable to add local JWT validator: %w", err) return fmt.Errorf("unable to add local JWT validator: %w", err)
} }
@@ -649,7 +687,7 @@ func (a *api) start() error {
if cfg.API.Auth.Auth0.Enable { if cfg.API.Auth.Auth0.Enable {
for _, t := range cfg.API.Auth.Auth0.Tenants { for _, t := range cfg.API.Auth.Auth0.Tenants {
if validator, err := jwt.NewAuth0Validator(t.Domain, t.Audience, t.ClientID, t.Users); err == nil { if validator, err := jwt.NewAuth0Validator(a.iam); err == nil {
if err := httpjwt.AddValidator("https://"+t.Domain+"/", validator); err != nil { if err := httpjwt.AddValidator("https://"+t.Domain+"/", validator); err != nil {
return fmt.Errorf("unable to add Auth0 JWT validator: %w", err) return fmt.Errorf("unable to add Auth0 JWT validator: %w", err)
} }
@@ -1303,6 +1341,10 @@ func (a *api) stop() {
return return
} }
if a.iam != nil {
a.iam.Close()
}
// Stop JWT authentication // Stop JWT authentication
if a.httpjwt != nil { if a.httpjwt != nil {
a.httpjwt.ClearValidators() a.httpjwt.ClearValidators()

View File

@@ -46,8 +46,8 @@ func (v *localValidator) Validate(c echo.Context) (bool, string, error) {
return false, "", nil return false, "", nil
} }
identity := v.iam.GetIdentity(login.Username) identity, err := v.iam.GetIdentity(login.Username)
if identity == nil { if err != nil {
return true, "", fmt.Errorf("invalid username or password") return true, "", fmt.Errorf("invalid username or password")
} }
@@ -55,7 +55,7 @@ func (v *localValidator) Validate(c echo.Context) (bool, string, error) {
return true, "", fmt.Errorf("invalid username or password") return true, "", fmt.Errorf("invalid username or password")
} }
return true, login.Username, nil return true, identity.Name(), nil
} }
func (v *localValidator) Cancel() {} func (v *localValidator) Cancel() {}
@@ -109,8 +109,8 @@ func (v *auth0Validator) Validate(c echo.Context) (bool, string, error) {
} }
} }
identity := v.iam.GetIdentityByAuth0(subject) identity, err := v.iam.GetIdentityByAuth0(subject)
if identity == nil { if err != nil {
return true, "", fmt.Errorf("invalid token") return true, "", fmt.Errorf("invalid token")
} }
@@ -118,7 +118,7 @@ func (v *auth0Validator) Validate(c echo.Context) (bool, string, error) {
return true, "", fmt.Errorf("invalid token") return true, "", fmt.Errorf("invalid token")
} }
return true, subject, nil return true, identity.Name(), nil
} }
func (v *auth0Validator) Cancel() {} func (v *auth0Validator) Cancel() {}

View File

@@ -1,5 +1,12 @@
package iam package iam
import (
"github.com/datarhei/core/v16/io/fs"
"github.com/casbin/casbin/v2"
"github.com/casbin/casbin/v2/model"
)
type AccessEnforcer interface { type AccessEnforcer interface {
Enforce(name, domain, resource, action string) bool Enforce(name, domain, resource, action string) bool
} }
@@ -11,11 +18,41 @@ type AccessManager interface {
} }
type access struct { type access struct {
fs fs.Filesystem
enforcer *casbin.Enforcer
} }
func NewAccessManager() (AccessManager, error) { func NewAccessManager(fs fs.Filesystem) (AccessManager, error) {
return &access{}, nil am := &access{
fs: fs,
}
m := model.NewModel()
m.AddDef("r", "r", "sub, dom, obj, act")
m.AddDef("p", "p", "sub, dom, obj, act")
m.AddDef("g", "g", "_, _, _")
m.AddDef("e", "e", "some(where (p.eft == allow))")
m.AddDef("m", "m", `g(r.sub, p.sub, r.dom) && r.dom == p.dom && ResourceMatch(r.obj, r.dom, p.obj) && ActionMatch(r.act, p.act) || r.sub == "$superuser"`)
a := newAdapter(fs, "./policy.json")
e, err := casbin.NewEnforcer(m, a)
if err != nil {
return nil, err
}
e.AddFunction("ResourceMatch", resourceMatchFunc)
e.AddFunction("ActionMatch", actionMatchFunc)
am.enforcer = e
return am, nil
} }
func (a *access) AddPolicy() {} func (am *access) AddPolicy() {}
func (a *access) Enforce(name, domain, resource, action string) bool { return false } func (am *access) Enforce(name, domain, resource, action string) bool {
ok, _, _ := am.enforcer.EnforceEx(name, domain, resource, action)
return ok
}

485
iam/adapter.go Normal file
View File

@@ -0,0 +1,485 @@
package iam
import (
"encoding/json"
"fmt"
"os"
"strings"
"sync"
"github.com/datarhei/core/v16/io/fs"
"github.com/casbin/casbin/v2/model"
"github.com/casbin/casbin/v2/persist"
)
// Adapter is the file adapter for Casbin.
// It can load policy from file or save policy to file.
type adapter struct {
fs fs.Filesystem
filePath string
groups []Group
lock sync.Mutex
}
func newAdapter(fs fs.Filesystem, filePath string) persist.Adapter {
return &adapter{filePath: filePath}
}
// Adapter
func (a *adapter) LoadPolicy(model model.Model) error {
a.lock.Lock()
defer a.lock.Unlock()
if a.filePath == "" {
return fmt.Errorf("invalid file path, file path cannot be empty")
}
/*
logger := &log.DefaultLogger{}
logger.EnableLog(true)
model.SetLogger(logger)
*/
return a.loadPolicyFile(model)
}
func (a *adapter) loadPolicyFile(model model.Model) error {
if _, err := a.fs.Stat(a.filePath); os.IsNotExist(err) {
a.groups = []Group{}
return nil
}
data, err := a.fs.ReadFile(a.filePath)
if err != nil {
return err
}
groups := []Group{}
err = json.Unmarshal(data, &groups)
if err != nil {
return err
}
rule := [5]string{}
for _, group := range groups {
rule[0] = "p"
rule[2] = group.Name
for name, roles := range group.Roles {
rule[1] = "role:" + name
for _, role := range roles {
rule[3] = role.Resource
rule[4] = role.Actions
if err := a.importPolicy(model, rule[0:5]); err != nil {
return err
}
}
}
for _, policy := range group.Policies {
rule[1] = policy.Username
rule[3] = policy.Resource
rule[4] = policy.Actions
if err := a.importPolicy(model, rule[0:5]); err != nil {
return err
}
}
rule[0] = "g"
rule[3] = group.Name
for _, ug := range group.UserRoles {
rule[1] = ug.Username
rule[2] = "role:" + ug.Role
if err := a.importPolicy(model, rule[0:4]); err != nil {
return err
}
}
}
a.groups = groups
return nil
}
func (a *adapter) importPolicy(model model.Model, rule []string) error {
copiedRule := make([]string, len(rule))
copy(copiedRule, rule)
ok, err := model.HasPolicyEx(copiedRule[0], copiedRule[0], copiedRule[1:])
if err != nil {
return err
}
if ok {
return nil // skip duplicated policy
}
model.AddPolicy(copiedRule[0], copiedRule[0], copiedRule[1:])
return nil
}
// Adapter
func (a *adapter) SavePolicy(model model.Model) error {
a.lock.Lock()
defer a.lock.Unlock()
return a.savePolicyFile()
}
func (a *adapter) savePolicyFile() error {
if a.filePath == "" {
return fmt.Errorf("invalid file path, file path cannot be empty")
}
jsondata, err := json.MarshalIndent(a.groups, "", " ")
if err != nil {
return err
}
_, _, err = a.fs.WriteFileSafe(a.filePath, jsondata)
return err
}
// Adapter (auto-save)
func (a *adapter) AddPolicy(sec, ptype string, rule []string) error {
a.lock.Lock()
defer a.lock.Unlock()
err := a.addPolicy(ptype, rule)
if err != nil {
return err
}
return a.savePolicyFile()
}
// BatchAdapter (auto-save)
func (a *adapter) AddPolicies(sec string, ptype string, rules [][]string) error {
a.lock.Lock()
defer a.lock.Unlock()
for _, rule := range rules {
err := a.addPolicy(ptype, rule)
if err != nil {
return err
}
}
return a.savePolicyFile()
}
func (a *adapter) addPolicy(ptype string, rule []string) error {
ok, err := a.hasPolicy(ptype, rule)
if err != nil {
return err
}
if ok {
// the policy is already there, nothing to add
return nil
}
username := ""
role := ""
domain := ""
resource := ""
actions := ""
if ptype == "p" {
username = rule[0]
domain = rule[1]
resource = rule[2]
actions = rule[3]
} else if ptype == "g" {
username = rule[0]
role = rule[1]
domain = rule[2]
} else {
return fmt.Errorf("unknown ptype: %s", ptype)
}
var group *Group = nil
for i := range a.groups {
if a.groups[i].Name == domain {
group = &a.groups[i]
}
}
if group == nil {
g := Group{
Name: domain,
}
a.groups = append(a.groups, g)
group = &g
}
if ptype == "p" {
if strings.HasPrefix(username, "role:") {
if group.Roles == nil {
group.Roles = make(map[string][]Role)
}
role := strings.TrimPrefix(username, "role:")
group.Roles[role] = append(group.Roles[role], Role{
Resource: resource,
Actions: actions,
})
} else {
group.Policies = append(group.Policies, Policy{
Username: rule[0],
Role: Role{
Resource: resource,
Actions: actions,
},
})
}
} else {
group.UserRoles = append(group.UserRoles, MapUserRole{
Username: username,
Role: strings.TrimPrefix(role, "role:"),
})
}
return nil
}
func (a *adapter) hasPolicy(ptype string, rule []string) (bool, error) {
var username string
var role string
var domain string
var resource string
var actions string
if ptype == "p" {
if len(rule) != 4 {
return false, fmt.Errorf("invalid rule length. must be 'user/role, domain, resource, actions'")
}
username = rule[0]
domain = rule[1]
resource = rule[2]
actions = rule[3]
} else if ptype == "g" {
username = rule[0]
role = rule[1]
domain = rule[2]
} else {
return false, fmt.Errorf("unknown ptype: %s", ptype)
}
var group *Group = nil
for _, g := range a.groups {
if g.Name == domain {
group = &g
break
}
}
if group == nil {
// if we can't find any group with that name, then the policy doesn't exist
return false, nil
}
if ptype == "p" {
isRole := false
if strings.HasPrefix(username, "role:") {
isRole = true
username = strings.TrimPrefix(username, "role:")
}
if isRole {
roles, ok := group.Roles[username]
if !ok {
// unknown role, policy doesn't exist
return false, nil
}
for _, role := range roles {
if role.Resource == resource && role.Actions == actions {
return true, nil
}
}
} else {
for _, p := range group.Policies {
if p.Username == username && p.Resource == resource && p.Actions == actions {
return true, nil
}
}
}
} else {
role = strings.TrimPrefix(role, "role:")
for _, user := range group.UserRoles {
if user.Username == username && user.Role == role {
return true, nil
}
}
}
return false, nil
}
// Adapter (auto-save)
func (a *adapter) RemovePolicy(sec string, ptype string, rule []string) error {
a.lock.Lock()
defer a.lock.Unlock()
err := a.removePolicy(ptype, rule)
if err != nil {
return err
}
return a.savePolicyFile()
}
// BatchAdapter (auto-save)
func (a *adapter) RemovePolicies(sec string, ptype string, rules [][]string) error {
a.lock.Lock()
defer a.lock.Unlock()
for _, rule := range rules {
err := a.removePolicy(ptype, rule)
if err != nil {
return err
}
}
return a.savePolicyFile()
}
func (a *adapter) removePolicy(ptype string, rule []string) error {
ok, err := a.hasPolicy(ptype, rule)
if err != nil {
return err
}
if !ok {
// the policy is not there, nothing to remove
return nil
}
username := ""
role := ""
domain := ""
resource := ""
actions := ""
if ptype == "p" {
username = rule[0]
domain = rule[1]
resource = rule[2]
actions = rule[3]
} else if ptype == "g" {
username = rule[0]
role = rule[1]
domain = rule[2]
} else {
return fmt.Errorf("unknown ptype: %s", ptype)
}
var group *Group = nil
for i := range a.groups {
if a.groups[i].Name == domain {
group = &a.groups[i]
}
}
if ptype == "p" {
isRole := false
if strings.HasPrefix(username, "role:") {
isRole = true
username = strings.TrimPrefix(username, "role:")
}
if isRole {
roles := group.Roles[username]
newRoles := []Role{}
for _, role := range roles {
if role.Resource == resource && role.Actions == actions {
continue
}
newRoles = append(newRoles, role)
}
group.Roles[username] = newRoles
} else {
policies := []Policy{}
for _, p := range group.Policies {
if p.Username == username && p.Resource == resource && p.Actions == actions {
continue
}
policies = append(policies, p)
}
group.Policies = policies
}
} else {
role = strings.TrimPrefix(role, "role:")
users := []MapUserRole{}
for _, user := range group.UserRoles {
if user.Username == username && user.Role == role {
continue
}
users = append(users, user)
}
group.UserRoles = users
}
return nil
}
// Adapter
func (a *adapter) RemoveFilteredPolicy(sec string, ptype string, fieldIndex int, fieldValues ...string) error {
return fmt.Errorf("not implemented")
}
func (a *adapter) GetAllGroupNames() []string {
a.lock.Lock()
defer a.lock.Unlock()
groups := []string{}
for _, group := range a.groups {
groups = append(groups, group.Name)
}
return groups
}
type Group struct {
Name string `json:"name"`
Roles map[string][]Role `json:"roles"`
UserRoles []MapUserRole `json:"userroles"`
Policies []Policy `json:"policies"`
}
type Role struct {
Resource string `json:"resource"`
Actions string `json:"actions"`
}
type MapUserRole struct {
Username string `json:"username"`
Role string `json:"role"`
}
type Policy struct {
Username string `json:"username"`
Role
}

115
iam/casbin.go Normal file
View File

@@ -0,0 +1,115 @@
package iam
import (
"fmt"
"strings"
"github.com/gobwas/glob"
)
func resourceMatch(request, domain, policy string) bool {
reqPrefix, reqResource := getPrefix(request)
polPrefix, polResource := getPrefix(policy)
if reqPrefix != polPrefix {
return false
}
fmt.Printf("prefix: %s\n", reqPrefix)
fmt.Printf("requested resource: %s\n", reqResource)
fmt.Printf("requested domain: %s\n", domain)
fmt.Printf("policy resource: %s\n", polResource)
var match bool
var err error
if reqPrefix == "processid" {
match, err = globMatch(polResource, reqResource)
if err != nil {
return false
}
} else if reqPrefix == "api" {
match, err = globMatch(polResource, reqResource, rune('/'))
if err != nil {
return false
}
} else if reqPrefix == "fs" {
match, err = globMatch(polResource, reqResource, rune('/'))
if err != nil {
return false
}
} else if reqPrefix == "rtmp" {
match, err = globMatch(polResource, reqResource)
if err != nil {
return false
}
} else if reqPrefix == "srt" {
match, err = globMatch(polResource, reqResource)
if err != nil {
return false
}
} else {
match, err = globMatch(polResource, reqResource)
if err != nil {
return false
}
}
fmt.Printf("match: %v\n", match)
return match
}
func resourceMatchFunc(args ...interface{}) (interface{}, error) {
request := args[0].(string)
domain := args[1].(string)
policy := args[2].(string)
return (bool)(resourceMatch(request, domain, policy)), nil
}
func actionMatch(request string, policy string) bool {
request = strings.ToUpper(request)
actions := strings.Split(strings.ToUpper(policy), "|")
if len(actions) == 0 {
return false
}
for _, a := range actions {
if request == a {
return true
}
}
return false
}
func actionMatchFunc(args ...interface{}) (interface{}, error) {
request := args[0].(string)
policy := args[1].(string)
return (bool)(actionMatch(request, policy)), nil
}
func getPrefix(s string) (string, string) {
splits := strings.SplitN(s, ":", 2)
if len(splits) == 0 {
return "", ""
}
if len(splits) == 1 {
return "", splits[0]
}
return splits[0], splits[1]
}
func globMatch(pattern, name string, separators ...rune) (bool, error) {
g, err := glob.Compile(pattern, separators...)
if err != nil {
return false, err
}
return g.Match(name), nil
}

View File

@@ -1,26 +1,55 @@
package iam package iam
import "github.com/datarhei/core/v16/io/fs"
type IAM interface { type IAM interface {
Enforce(user, domain, resource, action string) bool Enforce(user, domain, resource, action string) bool
GetIdentity(name string) IdentityVerifier GetIdentity(name string) (IdentityVerifier, error)
GetIdentityByAuth0(name string) IdentityVerifier GetIdentityByAuth0(name string) (IdentityVerifier, error)
Close()
} }
type iam struct{} type iam struct {
im IdentityManager
am AccessManager
}
func NewIAM() (IAM, error) { func NewIAM(fs fs.Filesystem, superuser User) (IAM, error) {
return &iam{}, nil im, err := NewIdentityManager(fs, superuser)
if err != nil {
return nil, err
}
am, err := NewAccessManager(fs)
if err != nil {
return nil, err
}
return &iam{
im: im,
am: am,
}, nil
}
func (i *iam) Close() {
i.im.Close()
i.im = nil
i.am = nil
return
} }
func (i *iam) Enforce(user, domain, resource, action string) bool { func (i *iam) Enforce(user, domain, resource, action string) bool {
return false return i.am.Enforce(user, domain, resource, action)
} }
func (i *iam) GetIdentity(name string) IdentityVerifier { func (i *iam) GetIdentity(name string) (IdentityVerifier, error) {
return nil return i.im.GetVerifier(name)
} }
func (i *iam) GetIdentityByAuth0(name string) IdentityVerifier { func (i *iam) GetIdentityByAuth0(name string) (IdentityVerifier, error) {
return nil return i.im.GetVerifierByAuth0(name)
} }

View File

@@ -1,11 +1,14 @@
package iam package iam
import ( import (
"encoding/json"
"fmt" "fmt"
"os"
"regexp" "regexp"
"sync" "sync"
"github.com/datarhei/core/v16/http/jwt/jwks" "github.com/datarhei/core/v16/iam/jwks"
"github.com/datarhei/core/v16/io/fs"
jwtgo "github.com/golang-jwt/jwt/v4" jwtgo "github.com/golang-jwt/jwt/v4"
) )
@@ -16,28 +19,35 @@ import (
// the whole jwks will be part of this package // the whole jwks will be part of this package
type User struct { type User struct {
Name string `json:"name"` Name string `json:"name"`
Superuser bool `json:"superuser"` Superuser bool `json:"superuser"`
Auth struct { Auth UserAuth `json:"auth"`
API struct { }
Userpass struct {
Enable bool `json:"enable"` type UserAuth struct {
Password string `json:"password"` API UserAuthAPI `json:"api"`
} `json:"userpass"` Services UserAuthServices `json:"services"`
Auth0 struct { }
Enable bool `json:"enable"`
User string `json:"user"` type UserAuthAPI struct {
Tenant Auth0Tenant `json:"tenant"` Userpass UserAuthPassword `json:"userpass"`
} `json:"auth0"` Auth0 UserAuthAPIAuth0 `json:"auth0"`
} `json:"api"` }
Services struct {
Basic struct { type UserAuthAPIAuth0 struct {
Enable bool `json:"enable"` Enable bool `json:"enable"`
Password string `json:"password"` User string `json:"user"`
} `json:"basic"` Tenant Auth0Tenant `json:"tenant"`
Token string `json:"token"` }
} `json:"services"`
} `json:"auth"` type UserAuthServices struct {
Basic UserAuthPassword `json:"basic"`
Token string `json:"token"`
}
type UserAuthPassword struct {
Enable bool `json:"enable"`
Password string `json:"password"`
} }
func (u *User) validate() error { func (u *User) validate() error {
@@ -83,6 +93,10 @@ type identity struct {
lock sync.RWMutex lock sync.RWMutex
} }
func (i *identity) Name() string {
return i.user.Name
}
func (i *identity) VerifyAPIPassword(password string) bool { func (i *identity) VerifyAPIPassword(password string) bool {
i.lock.RLock() i.lock.RLock()
defer i.lock.RUnlock() defer i.lock.RUnlock()
@@ -238,6 +252,8 @@ func (i *identity) IsSuperuser() bool {
} }
type IdentityVerifier interface { type IdentityVerifier interface {
Name() string
VerifyAPIPassword(password string) bool VerifyAPIPassword(password string) bool
VerifyAPIAuth0(jwt string) bool VerifyAPIAuth0(jwt string) bool
@@ -252,11 +268,12 @@ type IdentityManager interface {
Remove(name string) error Remove(name string) error
Get(name string) (User, error) Get(name string) (User, error)
GetVerifier(name string) (IdentityVerifier, error) GetVerifier(name string) (IdentityVerifier, error)
GetVerifierByAuth0(name string) (IdentityVerifier, error)
Rename(oldname, newname string) error Rename(oldname, newname string) error
Update(name string, identity User) error Update(name string, identity User) error
Load(path string) error Save() error
Save(path string) error Close()
} }
type Auth0Tenant struct { type Auth0Tenant struct {
@@ -297,32 +314,72 @@ func newAuth0Tenant(tenant Auth0Tenant) (*auth0Tenant, error) {
return t, nil return t, nil
} }
func (a *auth0Tenant) Cancel() {
a.certs.Cancel()
}
type identityManager struct { type identityManager struct {
identities map[string]*identity identities map[string]*identity
tenants map[string]*auth0Tenant tenants map[string]*auth0Tenant
auth0UserIdentityMap map[string]string auth0UserIdentityMap map[string]string
fs fs.Filesystem
filePath string
lock sync.RWMutex lock sync.RWMutex
} }
func NewIdentityManager() (IdentityManager, error) { func NewIdentityManager(fs fs.Filesystem, superuser User) (IdentityManager, error) {
return &identityManager{ im := &identityManager{
identities: map[string]*identity{}, identities: map[string]*identity{},
tenants: map[string]*auth0Tenant{}, tenants: map[string]*auth0Tenant{},
auth0UserIdentityMap: map[string]string{}, auth0UserIdentityMap: map[string]string{},
}, nil fs: fs,
filePath: "./users.json",
}
err := im.load(im.filePath)
if err != nil {
return nil, err
}
if len(im.identities) == 0 {
superuser.Superuser = true
im.Create(superuser)
im.save(im.filePath)
}
return im, nil
} }
func (i *identityManager) Create(u User) error { func (im *identityManager) Close() {
im.lock.Lock()
defer im.lock.Unlock()
im.fs = nil
im.auth0UserIdentityMap = map[string]string{}
im.identities = map[string]*identity{}
for _, t := range im.tenants {
t.Cancel()
}
im.tenants = map[string]*auth0Tenant{}
return
}
func (im *identityManager) Create(u User) error {
if err := u.validate(); err != nil { if err := u.validate(); err != nil {
return err return err
} }
i.lock.Lock() im.lock.Lock()
defer i.lock.Unlock() defer im.lock.Unlock()
_, ok := i.identities[u.Name] _, ok := im.identities[u.Name]
if ok { if ok {
return fmt.Errorf("identity already exists") return fmt.Errorf("identity already exists")
} }
@@ -330,42 +387,42 @@ func (i *identityManager) Create(u User) error {
identity := u.marshalIdentity() identity := u.marshalIdentity()
if identity.user.Auth.API.Auth0.Enable { if identity.user.Auth.API.Auth0.Enable {
if _, ok := i.auth0UserIdentityMap[identity.user.Auth.API.Auth0.User]; ok { if _, ok := im.auth0UserIdentityMap[identity.user.Auth.API.Auth0.User]; ok {
return fmt.Errorf("the Auth0 user has already an identity") return fmt.Errorf("the Auth0 user has already an identity")
} }
auth0Key := identity.user.Auth.API.Auth0.Tenant.key() auth0Key := identity.user.Auth.API.Auth0.Tenant.key()
if _, ok := i.tenants[auth0Key]; !ok { if _, ok := im.tenants[auth0Key]; !ok {
tenant, err := newAuth0Tenant(identity.user.Auth.API.Auth0.Tenant) tenant, err := newAuth0Tenant(identity.user.Auth.API.Auth0.Tenant)
if err != nil { if err != nil {
return err return err
} }
i.tenants[auth0Key] = tenant im.tenants[auth0Key] = tenant
} }
} }
i.identities[identity.user.Name] = identity im.identities[identity.user.Name] = identity
return nil return nil
} }
func (i *identityManager) Update(name string, identity User) error { func (im *identityManager) Update(name string, identity User) error {
return nil return nil
} }
func (i *identityManager) Remove(name string) error { func (im *identityManager) Remove(name string) error {
i.lock.Lock() im.lock.Lock()
defer i.lock.Unlock() defer im.lock.Unlock()
user, ok := i.identities[name] user, ok := im.identities[name]
if !ok { if !ok {
return nil return nil
} }
delete(i.identities, name) delete(im.identities, name)
user.lock.Lock() user.lock.Lock()
user.valid = false user.valid = false
@@ -374,11 +431,8 @@ func (i *identityManager) Remove(name string) error {
return nil return nil
} }
func (i *identityManager) getIdentity(name string) (*identity, error) { func (im *identityManager) getIdentity(name string) (*identity, error) {
i.lock.RLock() identity, ok := im.identities[name]
defer i.lock.RUnlock()
identity, ok := i.identities[name]
if !ok { if !ok {
return nil, fmt.Errorf("not found") return nil, fmt.Errorf("not found")
} }
@@ -386,55 +440,117 @@ func (i *identityManager) getIdentity(name string) (*identity, error) {
return identity, nil return identity, nil
} }
func (i *identityManager) Get(name string) (User, error) { func (im *identityManager) Get(name string) (User, error) {
i.lock.RLock() im.lock.RLock()
defer i.lock.RUnlock() defer im.lock.RUnlock()
identity, ok := i.identities[name] identity, err := im.getIdentity(name)
if !ok { if err != nil {
return User{}, fmt.Errorf("not found") return User{}, fmt.Errorf("not found")
} }
return identity.user, nil return identity.user, nil
} }
func (i *identityManager) GetVerifier(name string) (IdentityVerifier, error) { func (im *identityManager) GetVerifier(name string) (IdentityVerifier, error) {
i.lock.RLock() im.lock.RLock()
defer i.lock.RUnlock() defer im.lock.RUnlock()
identity, ok := i.identities[name] return im.getIdentity(name)
}
func (im *identityManager) GetVerifierByAuth0(name string) (IdentityVerifier, error) {
im.lock.RLock()
defer im.lock.RUnlock()
name, ok := im.auth0UserIdentityMap[name]
if !ok { if !ok {
return nil, fmt.Errorf("not found") return nil, fmt.Errorf("not found")
} }
return identity, nil return im.getIdentity(name)
} }
func (i *identityManager) Rename(oldname, newname string) error { func (im *identityManager) Rename(oldname, newname string) error {
i.lock.Lock() im.lock.Lock()
defer i.lock.Unlock() defer im.lock.Unlock()
identity, ok := i.identities[oldname] identity, ok := im.identities[oldname]
if !ok { if !ok {
return nil return nil
} }
if _, ok := i.identities[newname]; ok { if _, ok := im.identities[newname]; ok {
return fmt.Errorf("the new name already exists") return fmt.Errorf("the new name already exists")
} }
delete(i.identities, oldname) delete(im.identities, oldname)
identity.user.Name = newname identity.user.Name = newname
i.identities[newname] = identity im.identities[newname] = identity
return nil return nil
} }
func (i *identityManager) Load(path string) error { func (im *identityManager) load(filePath string) error {
return fmt.Errorf("not implemented") if im.fs == nil {
return fmt.Errorf("no filesystem provided")
}
if _, err := im.fs.Stat(filePath); os.IsNotExist(err) {
return nil
}
data, err := im.fs.ReadFile(filePath)
if err != nil {
return err
}
users := []User{}
err = json.Unmarshal(data, &users)
if err != nil {
return err
}
for _, u := range users {
err = im.Create(u)
if err != nil {
return err
}
}
return nil
} }
func (i *identityManager) Save(path string) error { func (im *identityManager) Save() error {
return fmt.Errorf("not implemented") return im.save(im.filePath)
}
func (im *identityManager) save(filePath string) error {
if im.fs == nil {
return fmt.Errorf("no filesystem provided")
}
if filePath == "" {
return fmt.Errorf("invalid file path, file path cannot be empty")
}
im.lock.RLock()
defer im.lock.RUnlock()
users := []User{}
for _, u := range im.identities {
users = append(users, u.user)
}
jsondata, err := json.MarshalIndent(users, "", " ")
if err != nil {
return err
}
_, _, err = im.fs.WriteFileSafe(filePath, jsondata)
return err
} }