diff --git a/app/casbin/adapter.go b/app/casbin/adapter.go new file mode 100644 index 00000000..9c65a549 --- /dev/null +++ b/app/casbin/adapter.go @@ -0,0 +1,542 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + "sync" + + "github.com/casbin/casbin/v2/model" + "github.com/casbin/casbin/v2/persist" + "github.com/datarhei/core/v16/io/file" +) + +// Adapter is the file adapter for Casbin. +// It can load policy from file or save policy to file. +type adapter struct { + filePath string + groups []Group + lock sync.Mutex +} + +func NewAdapter(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 { + data, err := os.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 + } + + dir, filename := filepath.Split(a.filePath) + + tmpfile, err := os.CreateTemp(dir, filename) + if err != nil { + return err + } + + defer os.Remove(tmpfile.Name()) + + if _, err := tmpfile.Write(jsondata); err != nil { + return err + } + + if err := tmpfile.Close(); err != nil { + return err + } + + if err := file.Rename(tmpfile.Name(), a.filePath); err != nil { + return err + } + + return nil +} + +// 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:"users"` + 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 +} + +/* +group = { + name: "igelcamp", + roles: { + "admin": [ + ["api:/process/**", "GET|POST|PUT|DELETE"], + ["processid:*", "CONFIG|PROGRESS|REPORT|METADATA|COMMAND"], + ["rtmp:igelcamp/*", "PUBLISH|PLAY"], + ["srt:igelcamp/*", "PUBLISH|PLAY"], + ["fs:/igelcamp/**", "GET|POST|PUT|DELETE"], + ["fs:/memfs/igelcamp/**", "GET|POST|PUT|DELETE"], + ], + "user": [ + ["api:/process/**", "GET"], + ["processid:*", "PROGRESS"], + ["rtmp:igelcamp/*", "PLAY"], + ["srt:igelcamp/*", "PLAY"], + ["fs:/igelcamp/**", "GET"], + ["fs:/memfs/igelcamp/**", "GET"], + ], + }, + users: [ + {"username": "alice", "role": "admin"], + ["bob", "user"], + ], + policies: [ + ["bob", "processid:*", "COMMAND"] + ], +} + +group = { + name: "$none", + roles: { + "anonymous": [ + {"fs:/*", "GET"} + ] + }, + users: [ + {"alice", "anonymous"}, + {"bob", "anonymous"}, + ] +} +*/ diff --git a/app/casbin/casbin b/app/casbin/casbin index 6d86154b..af59b479 100755 Binary files a/app/casbin/casbin and b/app/casbin/casbin differ diff --git a/app/casbin/main.go b/app/casbin/main.go index 9d3dde54..f8e7831b 100644 --- a/app/casbin/main.go +++ b/app/casbin/main.go @@ -17,14 +17,15 @@ func main() { var object string var action string - flag.StringVar(&subject, "subject", "", "subject of this request") + flag.StringVar(&subject, "subject", "$anon", "subject of this request") flag.StringVar(&domain, "domain", "$none", "domain of this request") flag.StringVar(&object, "object", "", "object of this request") flag.StringVar(&action, "action", "", "action of this request") flag.Parse() - e, err := casbin.NewEnforcer("./model.conf", "./policy.csv") + policy := NewAdapter("./policy.json") + e, err := casbin.NewEnforcer("./model.conf", policy) if err != nil { fmt.Printf("error: %s\n", err) os.Exit(1) @@ -33,6 +34,24 @@ func main() { e.AddFunction("ResourceMatch", ResourceMatchFunc) e.AddFunction("ActionMatch", ActionMatchFunc) + if err := addGroup(e, "foobar"); err != nil { + fmt.Printf("error: %s\n", err) + os.Exit(1) + } + + if err := addGroupUser(e, "foobar", "franz", "admin"); err != nil { + fmt.Printf("error: %s\n", err) + os.Exit(1) + } + + if err := addGroupUser(e, "foobar", "$anon", "anonymous"); err != nil { + fmt.Printf("error: %s\n", err) + os.Exit(1) + } + + e.RemovePolicy("bob", "igelcamp", "processid:*", "COMMAND") + e.AddPolicy("bob", "igelcamp", "processid:bob-*", "COMMAND") + ok, err := e.Enforce(subject, domain, object, action) if err != nil { fmt.Printf("error: %s\n", err) @@ -138,3 +157,36 @@ func getPrefix(s string) (string, string) { return splits[0], splits[1] } + +func addGroup(e *casbin.Enforcer, name string) error { + rules := [][]string{} + + rules = append(rules, []string{"role:admin", name, "api:/process/**", "GET|POST|PUT|DELETE"}) + rules = append(rules, []string{"role:admin", name, "processid:*", "CONFIG|PROGRESS|REPORT|METADATA|COMMAND"}) + rules = append(rules, []string{"role:admin", name, "rtmp:" + name + "/*", "PUBLISH|PLAY"}) + rules = append(rules, []string{"role:admin", name, "srt:" + name + "/*", "PUBLISH|PLAY"}) + rules = append(rules, []string{"role:admin", name, "fs:/" + name + "/**", "GET|POST|PUT|DELETE"}) + rules = append(rules, []string{"role:admin", name, "fs:/memfs/" + name + "/**", "GET|POST|PUT|DELETE"}) + + rules = append(rules, []string{"role:user", name, "api:/process/**", "GET"}) + rules = append(rules, []string{"role:user", name, "processid:*", "PROGRESS"}) + rules = append(rules, []string{"role:user", name, "rtmp:" + name + "/*", "PLAY"}) + rules = append(rules, []string{"role:user", name, "srt:" + name + "/*", "PLAY"}) + rules = append(rules, []string{"role:user", name, "fs:/" + name + "/**", "GET"}) + rules = append(rules, []string{"role:user", name, "fs:/memfs/" + name + "/**", "GET"}) + + rules = append(rules, []string{"role:anonymous", name, "rtmp:" + name + "/*", "PLAY"}) + rules = append(rules, []string{"role:anonymous", name, "srt:" + name + "/*", "PLAY"}) + rules = append(rules, []string{"role:anonymous", name, "fs:/" + name + "/**", "GET"}) + rules = append(rules, []string{"role:anonymous", name, "fs:/memfs/" + name + "/**", "GET"}) + + _, err := e.AddPolicies(rules) + + return err +} + +func addGroupUser(e *casbin.Enforcer, group, username, role string) error { + _, err := e.AddGroupingPolicy(username, "role:"+role, group) + + return err +} diff --git a/app/casbin/policy.json b/app/casbin/policy.json new file mode 100644 index 00000000..745775eb --- /dev/null +++ b/app/casbin/policy.json @@ -0,0 +1,206 @@ +[ + { + "name": "igelcamp", + "roles": { + "admin": [ + { + "resource": "api:/process/**", + "actions": "GET|POST|PUT|DELETE" + }, + { + "resource": "processid:*", + "actions": "CONFIG|PROGRESS|REPORT|METADATA|COMMAND" + }, + { + "resource": "rtmp:igelcamp/*", + "actions": "PUBLISH|PLAY" + }, + { + "resource": "srt:igelcamp/*", + "actions": "PUBLISH|PLAY" + }, + { + "resource": "fs:/igelcamp/**", + "actions": "GET|POST|PUT|DELETE" + }, + { + "resource": "fs:/memfs/igelcamp/**", + "actions": "GET|POST|PUT|DELETE" + } + ], + "anonymous": [ + { + "resource": "rtmp:igelcamp/*", + "actions": "PLAY" + }, + { + "resource": "srt:igelcamp/*", + "actions": "PLAY" + }, + { + "resource": "fs:/igelcamp/**", + "actions": "GET" + }, + { + "resource": "fs:/memfs/igelcamp/**", + "actions": "GET" + } + ], + "user": [ + { + "resource": "api:/process/**", + "actions": "GET" + }, + { + "resource": "processid:*", + "actions": "PROGRESS" + }, + { + "resource": "rtmp:igelcamp/*", + "actions": "PLAY" + }, + { + "resource": "srt:igelcamp/*", + "actions": "PLAY" + }, + { + "resource": "fs:/igelcamp/**", + "actions": "GET" + }, + { + "resource": "fs:/memfs/igelcamp/**", + "actions": "GET" + } + ] + }, + "users": [ + { + "username": "alice", + "role": "admin" + }, + { + "username": "bob", + "role": "user" + }, + { + "username": "$anon", + "role": "anonymous" + } + ], + "policies": [ + { + "username": "bob", + "resource": "processid:bob-*", + "actions": "COMMAND" + } + ] + }, + { + "name": "$none", + "roles": { + "anonymous": [ + { + "resource": "fs:/*", + "actions": "GET" + } + ] + }, + "users": [ + { + "username": "$anon", + "role": "anonymous" + }, + { + "username": "alice", + "role": "anonymous" + }, + { + "username": "bob", + "role": "anonymous" + } + ], + "policies": null + }, + { + "name": "foobar", + "roles": { + "admin": [ + { + "resource": "processid:*", + "actions": "CONFIG|PROGRESS|REPORT|METADATA|COMMAND" + }, + { + "resource": "rtmp:foobar/*", + "actions": "PUBLISH|PLAY" + }, + { + "resource": "srt:foobar/*", + "actions": "PUBLISH|PLAY" + }, + { + "resource": "fs:/foobar/**", + "actions": "GET|POST|PUT|DELETE" + }, + { + "resource": "fs:/memfs/foobar/**", + "actions": "GET|POST|PUT|DELETE" + } + ], + "anonymous": [ + { + "resource": "rtmp:foobar/*", + "actions": "PLAY" + }, + { + "resource": "srt:foobar/*", + "actions": "PLAY" + }, + { + "resource": "fs:/foobar/**", + "actions": "GET" + }, + { + "resource": "fs:/memfs/foobar/**", + "actions": "GET" + } + ], + "user": [ + { + "resource": "api:/process/**", + "actions": "GET" + }, + { + "resource": "processid:*", + "actions": "PROGRESS" + }, + { + "resource": "rtmp:foobar/*", + "actions": "PLAY" + }, + { + "resource": "srt:foobar/*", + "actions": "PLAY" + }, + { + "resource": "fs:/foobar/**", + "actions": "GET" + }, + { + "resource": "fs:/memfs/foobar/**", + "actions": "GET" + } + ] + }, + "users": [ + { + "username": "franz", + "role": "admin" + }, + { + "username": "$anon", + "role": "anonymous" + } + ], + "policies": null + } +] \ No newline at end of file