From 6a8b3b2e13df4f379a5bad627fd0ce98be07eb60 Mon Sep 17 00:00:00 2001 From: Aditya Thebe Date: Mon, 10 Feb 2025 18:37:34 +0545 Subject: [PATCH] chore: remove permission adapter --- rbac/adapter/permission.go | 202 ------------------------ rbac/custom_functions.go | 27 +++- rbac/{init.go => enforcer.go} | 19 ++- rbac/{rbac_test.go => enforcer_test.go} | 45 +----- rbac/types/types.go | 68 -------- 5 files changed, 39 insertions(+), 322 deletions(-) delete mode 100644 rbac/adapter/permission.go rename rbac/{init.go => enforcer.go} (92%) rename rbac/{rbac_test.go => enforcer_test.go} (73%) delete mode 100644 rbac/types/types.go diff --git a/rbac/adapter/permission.go b/rbac/adapter/permission.go deleted file mode 100644 index 7293f5c0..00000000 --- a/rbac/adapter/permission.go +++ /dev/null @@ -1,202 +0,0 @@ -package adapter - -import ( - "encoding/json" - "fmt" - "strings" - - "github.com/casbin/casbin/v2/model" - "github.com/casbin/casbin/v2/persist" - gormadapter "github.com/casbin/gorm-adapter/v3" - "github.com/flanksource/commons/collections" - "github.com/flanksource/duty/models" - pkgPolicy "github.com/flanksource/duty/rbac/policy" - "github.com/flanksource/duty/rbac/types" - "github.com/samber/lo" - "gorm.io/gorm" - "gorm.io/gorm/clause" -) - -type PermissionAdapter struct { - *gormadapter.Adapter // gorm adapter for `casbin_rules` table - - db *gorm.DB -} - -var _ persist.BatchAdapter = &PermissionAdapter{} - -func NewPermissionAdapter(db *gorm.DB, main *gormadapter.Adapter) *PermissionAdapter { - return &PermissionAdapter{ - db: db, - Adapter: main, - } -} - -func (a *PermissionAdapter) LoadPolicy(model model.Model) error { - if err := a.Adapter.LoadPolicy(model); err != nil { - return err - } - - var permissions []models.Permission - if err := a.db.Where("deleted_at IS NULL").Find(&permissions).Error; err != nil { - return fmt.Errorf("failed to load permissions: %w", err) - } - - for _, permission := range permissions { - policies := PermissionToCasbinRule(permission) - for _, policy := range policies { - if err := persist.LoadPolicyArray(policy, model); err != nil { - return err - } - } - } - - var permissionGroups []models.PermissionGroup - if err := a.db.Where("deleted_at IS NULL").Find(&permissionGroups).Error; err != nil { - return fmt.Errorf("failed to load permissions: %w", err) - } - - for _, pg := range permissionGroups { - policies, err := a.permissionGroupToCasbinRule(pg) - if err != nil { - return err - } - - for _, policy := range policies { - if err := persist.LoadPolicyArray(policy, model); err != nil { - return err - } - } - } - - return nil -} - -func PermissionToCasbinRule(permission models.Permission) [][]string { - var policies [][]string - patterns := strings.Split(permission.Action, ",") - - for _, action := range pkgPolicy.AllActions { - if !collections.MatchItems(action, patterns...) { - continue - } - - policies = append(policies, createPolicy(permission, action)) - - if objectSelector := rbacToABACObjectSelector(permission, action); objectSelector != nil { - abacPermission := permission - abacPermission.Object = "" - abacPermission.ObjectSelector = objectSelector - policies = append(policies, createPolicy(abacPermission, action)) - } - } - - return policies -} - -// createPolicy generates a Casbin policy rule from a permission. -func createPolicy(permission models.Permission, action string) []string { - return []string{ - "p", - permission.Principal(), - permission.GetObject(), - action, - permission.Effect(), - permission.Condition(), - permission.ID.String(), - } -} - -// rbacToABACObjectSelector returns object selectors (v1.PermissionObject) in JSON -// for ABAC policies from a global permission. -func rbacToABACObjectSelector(permission models.Permission, action string) []byte { - switch permission.Object { - case pkgPolicy.ObjectPlaybooks: - if lo.Contains([]string{pkgPolicy.ActionPlaybookRun, pkgPolicy.ActionPlaybookApprove}, action) { - return []byte(`{"playbooks": [{"name":"*"}]}`) - } - - case pkgPolicy.ObjectCatalog: - if pkgPolicy.ActionRead == action { - return []byte(`{"configs": [{"name":"*"}]}`) - } - - case pkgPolicy.ObjectTopology: - if pkgPolicy.ActionRead == action { - return []byte(`{"components": [{"name":"*"}]}`) - } - } - - return nil -} - -func (a *PermissionAdapter) permissionGroupToCasbinRule(permission models.PermissionGroup) ([][]string, error) { - var subject types.PermissionGroupSubjects - if err := json.Unmarshal(permission.Selectors, &subject); err != nil { - return nil, err - } - - var allIDs []string - - if len(subject.Notifications) > 0 { - var clauses []clause.Expression - for _, selector := range subject.Notifications { - if selector.Empty() { - continue - } - - var conditions []clause.Expression - if selector.Namespace != "" { - conditions = append(conditions, clause.Eq{Column: "namespace", Value: selector.Namespace}) - } - if selector.Name != "" { - conditions = append(conditions, clause.Eq{Column: "name", Value: selector.Name}) - } - - clauses = append(clauses, clause.And(conditions...)) - } - - if len(clauses) > 0 { - var notifications []string - if err := a.db.Select("id").Model(&models.Notification{}).Clauses(clause.Or(clauses...)).Find(¬ifications).Error; err != nil { - return nil, err - } - - allIDs = append(allIDs, notifications...) - } - } - - if len(subject.People) > 0 { - var personIDs []string - if err := a.db.Select("id").Model(&models.Person{}).Where("email IN ? OR name IN ?", subject.People, subject.People).Find(&personIDs).Error; err != nil { - return nil, err - } - - allIDs = append(allIDs, personIDs...) - } - - if len(subject.Teams) > 0 { - var teamIDs []string - if err := a.db.Select("id").Model(&models.Team{}).Where("name = ?", subject.Teams).Find(&teamIDs).Error; err != nil { - return nil, err - } - - allIDs = append(allIDs, teamIDs...) - } - - var policies [][]string - for _, id := range allIDs { - policy := []string{ - "g", - id, - permission.Name, - "", - "", - "", - } - - policies = append(policies, policy) - } - - return policies, nil -} diff --git a/rbac/custom_functions.go b/rbac/custom_functions.go index ba092818..55aeb3ff 100644 --- a/rbac/custom_functions.go +++ b/rbac/custom_functions.go @@ -9,11 +9,32 @@ import ( "github.com/casbin/govaluate" "github.com/flanksource/commons/collections" "github.com/flanksource/duty/models" - "github.com/flanksource/duty/rbac/types" + "github.com/flanksource/duty/types" "github.com/google/uuid" "github.com/samber/lo" ) +type Selectors struct { + Playbooks []types.ResourceSelector `json:"playbooks,omitempty"` + Configs []types.ResourceSelector `json:"configs,omitempty"` + Components []types.ResourceSelector `json:"components,omitempty"` +} + +func (t Selectors) RequiredMatchCount() int { + var count int + if len(t.Playbooks) > 0 { + count++ + } + if len(t.Configs) > 0 { + count++ + } + if len(t.Components) > 0 { + count++ + } + + return count +} + func matchPerm(attr *models.ABACAttribute, _agents any, tagsEncoded string) (bool, error) { var rAgents []string switch v := _agents.(type) { @@ -43,7 +64,7 @@ type addableEnforcer interface { AddFunction(name string, function govaluate.ExpressionFunction) } -func addCustomFunctions(enforcer addableEnforcer) { +func AddCustomFunctions(enforcer addableEnforcer) { enforcer.AddFunction("matchPerm", func(args ...any) (any, error) { if len(args) != 3 { return false, fmt.Errorf("matchPerm needs 3 arguments. got %d", len(args)) @@ -98,7 +119,7 @@ func addCustomFunctions(enforcer addableEnforcer) { return false, err } - var objectSelector types.PermissionObject + var objectSelector Selectors if err := json.Unmarshal([]byte(rs), &objectSelector); err != nil { return false, err } diff --git a/rbac/init.go b/rbac/enforcer.go similarity index 92% rename from rbac/init.go rename to rbac/enforcer.go index e8f0da21..367038f1 100644 --- a/rbac/init.go +++ b/rbac/enforcer.go @@ -8,12 +8,13 @@ import ( "github.com/casbin/casbin/v2" "github.com/casbin/casbin/v2/model" + "github.com/casbin/casbin/v2/persist" gormadapter "github.com/casbin/gorm-adapter/v3" "github.com/flanksource/duty/context" "github.com/flanksource/duty/query" - pkgAdapater "github.com/flanksource/duty/rbac/adapter" "github.com/flanksource/duty/rbac/policy" "gopkg.in/yaml.v3" + "gorm.io/gorm" ) var enforcer *casbin.SyncedCachedEnforcer @@ -22,10 +23,12 @@ var enforcer *casbin.SyncedCachedEnforcer var defaultPolicies string //go:embed model.ini -var defaultModel string +var DefaultModel string -func Init(ctx context.Context, adminUserID string) error { - model, err := model.NewModelFromString(defaultModel) +type Adapter func(db *gorm.DB, main *gormadapter.Adapter) persist.Adapter + +func Init(ctx context.Context, adminUserID string, adapters ...Adapter) error { + model, err := model.NewModelFromString(DefaultModel) if err != nil { return fmt.Errorf("error creating rbac model: %v", err) } @@ -54,7 +57,11 @@ func Init(ctx context.Context, adminUserID string) error { return fmt.Errorf("error creating rbac adapter: %v", err) } - adapter := pkgAdapater.NewPermissionAdapter(db, casbinRuleAdapter) + var adapter any = casbinRuleAdapter + for _, a := range adapters { + adapter = a(db, casbinRuleAdapter) + } + enforcer, err = casbin.NewSyncedCachedEnforcer(model, adapter) if err != nil { return fmt.Errorf("error creating rbac enforcer: %v", err) @@ -69,7 +76,7 @@ func Init(ctx context.Context, adminUserID string) error { enforcer.EnableLog(true) } - addCustomFunctions(enforcer) + AddCustomFunctions(enforcer) if adminUserID != "" { if _, err := enforcer.AddRoleForUser(adminUserID, policy.RoleAdmin); err != nil { diff --git a/rbac/rbac_test.go b/rbac/enforcer_test.go similarity index 73% rename from rbac/rbac_test.go rename to rbac/enforcer_test.go index af166994..e39fb65a 100644 --- a/rbac/rbac_test.go +++ b/rbac/enforcer_test.go @@ -7,21 +7,18 @@ import ( casbinModel "github.com/casbin/casbin/v2/model" stringadapter "github.com/casbin/casbin/v2/persist/string-adapter" "github.com/flanksource/duty/models" - "github.com/flanksource/duty/rbac/adapter" - "github.com/flanksource/duty/rbac/policy" "github.com/google/uuid" - "github.com/samber/lo" ) func NewEnforcer(policy string) (*casbin.Enforcer, error) { - model, err := casbinModel.NewModelFromString(defaultModel) + model, err := casbinModel.NewModelFromString(DefaultModel) if err != nil { return nil, err } sa := stringadapter.NewAdapter(policy) e, err := casbin.NewEnforcer(model, sa) - addCustomFunctions(e) + AddCustomFunctions(e) return e, err } @@ -37,42 +34,11 @@ p, alice, *, playbook:run, deny, r.obj.Playbook.Name == 'restart-deployment' && var userID = uuid.New() - permissions := []models.Permission{ - { - ID: uuid.New(), - PersonID: lo.ToPtr(userID), - Object: policy.ObjectCatalog, - Action: policy.ActionRead, - Tags: map[string]string{ - "namespace": "default", - "cluster": "aws", - }, - Agents: []string{"123"}, - }, - { - ID: uuid.New(), - PersonID: lo.ToPtr(userID), - Object: "*", - Action: policy.ActionRead, - Tags: map[string]string{ - "namespace": "default", - }, - }, - } - enforcer, err := NewEnforcer(policies) if err != nil { t.Fatal(err) } - for _, p := range permissions { - for _, policy := range adapter.PermissionToCasbinRule(p) { - if ok, err := enforcer.AddPolicy(policy[1:]); err != nil || !ok { - t.Fatal() - } - } - } - testData := []struct { description string user string @@ -138,13 +104,6 @@ p, alice, *, playbook:run, deny, r.obj.Playbook.Name == 'restart-deployment' && act: "read", allowed: false, }, - { - description: "abac catalog test", - user: userID.String(), - obj: &models.ABACAttribute{Config: models.ConfigItem{ID: uuid.New(), Tags: map[string]string{"namespace": "default"}}}, - act: "read", - allowed: true, - }, } for _, td := range testData { diff --git a/rbac/types/types.go b/rbac/types/types.go deleted file mode 100644 index d0ebea0c..00000000 --- a/rbac/types/types.go +++ /dev/null @@ -1,68 +0,0 @@ -package types - -import ( - "github.com/flanksource/duty/rbac/policy" - "github.com/flanksource/duty/types" -) - -// +kubebuilder:object:generate=true -type PermissionGroupSubjects struct { - Notifications []PermissionGroupSelector `json:"notifications,omitempty"` - People []string `json:"people,omitempty"` - Teams []string `json:"teams,omitempty"` -} - -// +kubebuilder:object:generate=true -type PermissionGroupSelector struct { - Namespace string `json:"namespace,omitempty"` - Name string `json:"name,omitempty"` -} - -func (t PermissionGroupSelector) Empty() bool { - return t.Name == "" && t.Namespace == "" -} - -type PermissionObject struct { - Playbooks []types.ResourceSelector `json:"playbooks,omitempty"` - Configs []types.ResourceSelector `json:"configs,omitempty"` - Components []types.ResourceSelector `json:"components,omitempty"` -} - -// GlobalObject checks if the object selector semantically maps to a global object -// and returns the corresponding global object if applicable. -// For example: -// -// configs: -// - name: '*' -// -// is interpreted as the object: catalog. -func (t *PermissionObject) GlobalObject() (string, bool) { - if len(t.Playbooks) == 1 && len(t.Configs) == 0 && len(t.Components) == 0 && t.Playbooks[0].Wildcard() { - return policy.ObjectPlaybooks, true - } - - if len(t.Configs) == 1 && len(t.Playbooks) == 0 && len(t.Components) == 0 && t.Configs[0].Wildcard() { - return policy.ObjectCatalog, true - } - - if len(t.Components) == 1 && len(t.Playbooks) == 0 && len(t.Configs) == 0 && t.Components[0].Wildcard() { - return policy.ObjectTopology, true - } - - return "", false -} - -func (t PermissionObject) RequiredMatchCount() int { - var count int - if len(t.Playbooks) > 0 { - count++ - } - if len(t.Configs) > 0 { - count++ - } - if len(t.Components) > 0 { - count++ - } - - return count -}