diff --git a/app/api/api.go b/app/api/api.go index da2ee327..1e585b3a 100644 --- a/app/api/api.go +++ b/app/api/api.go @@ -383,12 +383,50 @@ func (a *api) start() error { a.sessions = sessions } - iam, err := iam.NewIAM() - if err != nil { - return fmt.Errorf("iam: %w", err) - } + { + superuser := iam.User{ + 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{ Root: cfg.Storage.Disk.Dir, @@ -639,7 +677,7 @@ func (a *api) start() error { 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 { 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 { 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 { return fmt.Errorf("unable to add Auth0 JWT validator: %w", err) } @@ -1303,6 +1341,10 @@ func (a *api) stop() { return } + if a.iam != nil { + a.iam.Close() + } + // Stop JWT authentication if a.httpjwt != nil { a.httpjwt.ClearValidators() diff --git a/http/jwt/validator.go b/http/jwt/validator.go index 47a951ac..60d02937 100644 --- a/http/jwt/validator.go +++ b/http/jwt/validator.go @@ -46,8 +46,8 @@ func (v *localValidator) Validate(c echo.Context) (bool, string, error) { return false, "", nil } - identity := v.iam.GetIdentity(login.Username) - if identity == nil { + identity, err := v.iam.GetIdentity(login.Username) + if err != nil { 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, login.Username, nil + return true, identity.Name(), nil } func (v *localValidator) Cancel() {} @@ -109,8 +109,8 @@ func (v *auth0Validator) Validate(c echo.Context) (bool, string, error) { } } - identity := v.iam.GetIdentityByAuth0(subject) - if identity == nil { + identity, err := v.iam.GetIdentityByAuth0(subject) + if err != nil { 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, subject, nil + return true, identity.Name(), nil } func (v *auth0Validator) Cancel() {} diff --git a/iam/access.go b/iam/access.go index 51390848..3ddf7dc0 100644 --- a/iam/access.go +++ b/iam/access.go @@ -1,5 +1,12 @@ package iam +import ( + "github.com/datarhei/core/v16/io/fs" + + "github.com/casbin/casbin/v2" + "github.com/casbin/casbin/v2/model" +) + type AccessEnforcer interface { Enforce(name, domain, resource, action string) bool } @@ -11,11 +18,41 @@ type AccessManager interface { } type access struct { + fs fs.Filesystem + + enforcer *casbin.Enforcer } -func NewAccessManager() (AccessManager, error) { - return &access{}, nil +func NewAccessManager(fs fs.Filesystem) (AccessManager, error) { + 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 (a *access) Enforce(name, domain, resource, action string) bool { return false } +func (am *access) AddPolicy() {} +func (am *access) Enforce(name, domain, resource, action string) bool { + ok, _, _ := am.enforcer.EnforceEx(name, domain, resource, action) + + return ok +} diff --git a/iam/adapter.go b/iam/adapter.go new file mode 100644 index 00000000..8b5a8f78 --- /dev/null +++ b/iam/adapter.go @@ -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 +} diff --git a/iam/casbin.go b/iam/casbin.go new file mode 100644 index 00000000..3d175115 --- /dev/null +++ b/iam/casbin.go @@ -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 +} diff --git a/iam/iam.go b/iam/iam.go index ba11f33b..37382742 100644 --- a/iam/iam.go +++ b/iam/iam.go @@ -1,26 +1,55 @@ package iam +import "github.com/datarhei/core/v16/io/fs" + type IAM interface { Enforce(user, domain, resource, action string) bool - GetIdentity(name string) IdentityVerifier - GetIdentityByAuth0(name string) IdentityVerifier + GetIdentity(name string) (IdentityVerifier, error) + GetIdentityByAuth0(name string) (IdentityVerifier, error) + + Close() } -type iam struct{} +type iam struct { + im IdentityManager + am AccessManager +} -func NewIAM() (IAM, error) { - return &iam{}, nil +func NewIAM(fs fs.Filesystem, superuser User) (IAM, error) { + 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 { - return false + return i.am.Enforce(user, domain, resource, action) } -func (i *iam) GetIdentity(name string) IdentityVerifier { - return nil +func (i *iam) GetIdentity(name string) (IdentityVerifier, error) { + return i.im.GetVerifier(name) } -func (i *iam) GetIdentityByAuth0(name string) IdentityVerifier { - return nil +func (i *iam) GetIdentityByAuth0(name string) (IdentityVerifier, error) { + return i.im.GetVerifierByAuth0(name) } diff --git a/iam/identity.go b/iam/identity.go index 39828210..2d78bc2d 100644 --- a/iam/identity.go +++ b/iam/identity.go @@ -1,11 +1,14 @@ package iam import ( + "encoding/json" "fmt" + "os" "regexp" "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" ) @@ -16,28 +19,35 @@ import ( // the whole jwks will be part of this package type User struct { - Name string `json:"name"` - Superuser bool `json:"superuser"` - Auth struct { - API struct { - Userpass struct { - Enable bool `json:"enable"` - Password string `json:"password"` - } `json:"userpass"` - Auth0 struct { - Enable bool `json:"enable"` - User string `json:"user"` - Tenant Auth0Tenant `json:"tenant"` - } `json:"auth0"` - } `json:"api"` - Services struct { - Basic struct { - Enable bool `json:"enable"` - Password string `json:"password"` - } `json:"basic"` - Token string `json:"token"` - } `json:"services"` - } `json:"auth"` + Name string `json:"name"` + Superuser bool `json:"superuser"` + Auth UserAuth `json:"auth"` +} + +type UserAuth struct { + API UserAuthAPI `json:"api"` + Services UserAuthServices `json:"services"` +} + +type UserAuthAPI struct { + Userpass UserAuthPassword `json:"userpass"` + Auth0 UserAuthAPIAuth0 `json:"auth0"` +} + +type UserAuthAPIAuth0 struct { + Enable bool `json:"enable"` + User string `json:"user"` + Tenant Auth0Tenant `json:"tenant"` +} + +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 { @@ -83,6 +93,10 @@ type identity struct { lock sync.RWMutex } +func (i *identity) Name() string { + return i.user.Name +} + func (i *identity) VerifyAPIPassword(password string) bool { i.lock.RLock() defer i.lock.RUnlock() @@ -238,6 +252,8 @@ func (i *identity) IsSuperuser() bool { } type IdentityVerifier interface { + Name() string + VerifyAPIPassword(password string) bool VerifyAPIAuth0(jwt string) bool @@ -252,11 +268,12 @@ type IdentityManager interface { Remove(name string) error Get(name string) (User, error) GetVerifier(name string) (IdentityVerifier, error) + GetVerifierByAuth0(name string) (IdentityVerifier, error) Rename(oldname, newname string) error Update(name string, identity User) error - Load(path string) error - Save(path string) error + Save() error + Close() } type Auth0Tenant struct { @@ -297,32 +314,72 @@ func newAuth0Tenant(tenant Auth0Tenant) (*auth0Tenant, error) { return t, nil } +func (a *auth0Tenant) Cancel() { + a.certs.Cancel() +} + type identityManager struct { identities map[string]*identity tenants map[string]*auth0Tenant auth0UserIdentityMap map[string]string + fs fs.Filesystem + filePath string + lock sync.RWMutex } -func NewIdentityManager() (IdentityManager, error) { - return &identityManager{ +func NewIdentityManager(fs fs.Filesystem, superuser User) (IdentityManager, error) { + im := &identityManager{ identities: map[string]*identity{}, tenants: map[string]*auth0Tenant{}, 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 { return err } - i.lock.Lock() - defer i.lock.Unlock() + im.lock.Lock() + defer im.lock.Unlock() - _, ok := i.identities[u.Name] + _, ok := im.identities[u.Name] if ok { return fmt.Errorf("identity already exists") } @@ -330,42 +387,42 @@ func (i *identityManager) Create(u User) error { identity := u.marshalIdentity() 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") } 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) if err != nil { return err } - i.tenants[auth0Key] = tenant + im.tenants[auth0Key] = tenant } } - i.identities[identity.user.Name] = identity + im.identities[identity.user.Name] = identity return nil } -func (i *identityManager) Update(name string, identity User) error { +func (im *identityManager) Update(name string, identity User) error { return nil } -func (i *identityManager) Remove(name string) error { - i.lock.Lock() - defer i.lock.Unlock() +func (im *identityManager) Remove(name string) error { + im.lock.Lock() + defer im.lock.Unlock() - user, ok := i.identities[name] + user, ok := im.identities[name] if !ok { return nil } - delete(i.identities, name) + delete(im.identities, name) user.lock.Lock() user.valid = false @@ -374,11 +431,8 @@ func (i *identityManager) Remove(name string) error { return nil } -func (i *identityManager) getIdentity(name string) (*identity, error) { - i.lock.RLock() - defer i.lock.RUnlock() - - identity, ok := i.identities[name] +func (im *identityManager) getIdentity(name string) (*identity, error) { + identity, ok := im.identities[name] if !ok { return nil, fmt.Errorf("not found") } @@ -386,55 +440,117 @@ func (i *identityManager) getIdentity(name string) (*identity, error) { return identity, nil } -func (i *identityManager) Get(name string) (User, error) { - i.lock.RLock() - defer i.lock.RUnlock() +func (im *identityManager) Get(name string) (User, error) { + im.lock.RLock() + defer im.lock.RUnlock() - identity, ok := i.identities[name] - if !ok { + identity, err := im.getIdentity(name) + if err != nil { return User{}, fmt.Errorf("not found") } return identity.user, nil } -func (i *identityManager) GetVerifier(name string) (IdentityVerifier, error) { - i.lock.RLock() - defer i.lock.RUnlock() +func (im *identityManager) GetVerifier(name string) (IdentityVerifier, error) { + im.lock.RLock() + 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 { return nil, fmt.Errorf("not found") } - return identity, nil + return im.getIdentity(name) } -func (i *identityManager) Rename(oldname, newname string) error { - i.lock.Lock() - defer i.lock.Unlock() +func (im *identityManager) Rename(oldname, newname string) error { + im.lock.Lock() + defer im.lock.Unlock() - identity, ok := i.identities[oldname] + identity, ok := im.identities[oldname] if !ok { return nil } - if _, ok := i.identities[newname]; ok { + if _, ok := im.identities[newname]; ok { return fmt.Errorf("the new name already exists") } - delete(i.identities, oldname) + delete(im.identities, oldname) identity.user.Name = newname - i.identities[newname] = identity + im.identities[newname] = identity return nil } -func (i *identityManager) Load(path string) error { - return fmt.Errorf("not implemented") +func (im *identityManager) load(filePath string) error { + 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 { - return fmt.Errorf("not implemented") +func (im *identityManager) Save() error { + 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 } diff --git a/http/jwt/jwks/config.go b/iam/jwks/config.go similarity index 100% rename from http/jwt/jwks/config.go rename to iam/jwks/config.go diff --git a/http/jwt/jwks/doc.go b/iam/jwks/doc.go similarity index 100% rename from http/jwt/jwks/doc.go rename to iam/jwks/doc.go diff --git a/http/jwt/jwks/ecdsa.go b/iam/jwks/ecdsa.go similarity index 100% rename from http/jwt/jwks/ecdsa.go rename to iam/jwks/ecdsa.go diff --git a/http/jwt/jwks/jwks.go b/iam/jwks/jwks.go similarity index 100% rename from http/jwt/jwks/jwks.go rename to iam/jwks/jwks.go diff --git a/http/jwt/jwks/rsa.go b/iam/jwks/rsa.go similarity index 100% rename from http/jwt/jwks/rsa.go rename to iam/jwks/rsa.go