mirror of
https://github.com/datarhei/core.git
synced 2025-10-06 00:17:07 +08:00
WIP: add casbin to access manager, allow to persist identities
This commit is contained in:
@@ -383,12 +383,50 @@ func (a *api) start() error {
|
||||
a.sessions = sessions
|
||||
}
|
||||
|
||||
iam, err := iam.NewIAM()
|
||||
{
|
||||
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,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
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()
|
||||
|
@@ -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() {}
|
||||
|
@@ -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
|
||||
}
|
||||
|
485
iam/adapter.go
Normal file
485
iam/adapter.go
Normal 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
115
iam/casbin.go
Normal 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
|
||||
}
|
49
iam/iam.go
49
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)
|
||||
}
|
||||
|
238
iam/identity.go
238
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"
|
||||
)
|
||||
@@ -18,26 +21,33 @@ import (
|
||||
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 {
|
||||
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"`
|
||||
} `json:"auth0"`
|
||||
} `json:"api"`
|
||||
Services struct {
|
||||
Basic struct {
|
||||
}
|
||||
|
||||
type UserAuthServices struct {
|
||||
Basic UserAuthPassword `json:"basic"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
type UserAuthPassword struct {
|
||||
Enable bool `json:"enable"`
|
||||
Password string `json:"password"`
|
||||
} `json:"basic"`
|
||||
Token string `json:"token"`
|
||||
} `json:"services"`
|
||||
} `json:"auth"`
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
Reference in New Issue
Block a user