diff --git a/job/permission.go b/job/permission.go new file mode 100644 index 00000000..6572f232 --- /dev/null +++ b/job/permission.go @@ -0,0 +1,35 @@ +package job + +import ( + "github.com/flanksource/duty/context" + "github.com/flanksource/duty/models" + "gorm.io/gorm/clause" +) + +func SyncPermissionToCasbinRule(ctx context.Context) error { + var permissions []models.Permission + if err := ctx.DB().Find(&permissions).Error; err != nil { + return err + } + + for _, permission := range permissions { + rule := permissionToCasbinRule(permission) + if err := ctx.DB().Clauses(clause.OnConflict{OnConstraint: "casbin_rule_idx", UpdateAll: true}).Create(&rule).Error; err != nil { + return err + } + } + + return nil +} + +func permissionToCasbinRule(permission models.Permission) models.CasbinRule { + m := models.CasbinRule{ + PType: "p", + V0: permission.Principal(), + V1: "", // the principal (v0) handles this + V2: permission.Action, + V3: permission.Effect(), + } + + return m +} diff --git a/models/permission.go b/models/permission.go new file mode 100644 index 00000000..afe6d035 --- /dev/null +++ b/models/permission.go @@ -0,0 +1,74 @@ +package models + +import ( + "fmt" + "strings" + "time" + + "github.com/google/uuid" +) + +type CasbinRule struct { + ID int64 `gorm:"primaryKey;autoIncrement"` + PType string `json:"ptype"` + V0 string `json:"v0"` + V1 string `json:"v1"` + V2 string `json:"v2"` + V3 string `json:"v3"` + V4 string `json:"v4"` + V5 string `json:"v5"` +} + +type Permission struct { + ID uuid.UUID `json:"id" gorm:"default:generate_ulid()"` + Action string `json:"action"` + CanaryID *uuid.UUID `json:"canary_id,omitempty"` + ComponentID *uuid.UUID `json:"component_id,omitempty"` + ConfigID *uuid.UUID `json:"config_id,omitempty"` + CreatedAt time.Time `json:"created_at"` + CreatedBy uuid.UUID `json:"created_by"` + Deny bool `json:"deny"` + Description string `json:"description"` + PersonID *uuid.UUID `json:"person_id,omitempty"` + PlaybookID *uuid.UUID `json:"playbook_id,omitempty"` + TeamID *uuid.UUID `json:"team_id,omitempty"` + Until *time.Time `json:"until"` + UpdatedAt *time.Time `json:"updated_at"` + UpdatedBy *uuid.UUID `json:"updated_by"` +} + +func (t *Permission) Principal() string { + var rule []string + + if t.PersonID != nil { + rule = append(rule, fmt.Sprintf("r.sub.id == %s", t.PersonID.String())) + } else if t.TeamID != nil { + rule = append(rule, fmt.Sprintf("r.sub.id == %s", t.TeamID.String())) + } + + if t.ComponentID != nil { + rule = append(rule, fmt.Sprintf("r.component.id == %s", t.ComponentID.String())) + } + + if t.ConfigID != nil { + rule = append(rule, fmt.Sprintf("r.config.id == %s", t.ConfigID.String())) + } + + if t.CanaryID != nil { + rule = append(rule, fmt.Sprintf("r.canary.id == %s", t.CanaryID.String())) + } + + if t.PlaybookID != nil { + rule = append(rule, fmt.Sprintf("r.playbook.id == %s", t.PlaybookID.String())) + } + + return strings.Join(rule, " && ") +} + +func (t *Permission) Effect() string { + if t.Deny { + return "deny" + } + + return "allow" +} diff --git a/models/permission_test.go b/models/permission_test.go new file mode 100644 index 00000000..f2cc655f --- /dev/null +++ b/models/permission_test.go @@ -0,0 +1,62 @@ +package models + +import ( + "testing" + + "github.com/google/uuid" + "github.com/samber/lo" +) + +func TestPermission_Principal(t *testing.T) { + tests := []struct { + name string + perm Permission + expected string + }{ + { + name: "PersonID only", + perm: Permission{ + PersonID: lo.ToPtr(uuid.MustParse("11111111-1111-1111-1111-111111111111")), + }, + expected: "r.sub.id == 11111111-1111-1111-1111-111111111111", + }, + { + name: "TeamID only", + perm: Permission{ + TeamID: lo.ToPtr(uuid.MustParse("22222222-2222-2222-2222-222222222222")), + }, + expected: "r.sub.id == 22222222-2222-2222-2222-222222222222", + }, + { + name: "Multiple fields", + perm: Permission{ + PersonID: lo.ToPtr(uuid.MustParse("33333333-3333-3333-3333-333333333333")), + ConfigID: lo.ToPtr(uuid.MustParse("55555555-5555-5555-5555-555555555555")), + }, + expected: "r.sub.id == 33333333-3333-3333-3333-333333333333 && r.config.id == 55555555-5555-5555-5555-555555555555", + }, + { + name: "Multiple fields II", + perm: Permission{ + PersonID: lo.ToPtr(uuid.MustParse("66666666-6666-6666-6666-666666666666")), + ConfigID: lo.ToPtr(uuid.MustParse("88888888-8888-8888-8888-888888888888")), + PlaybookID: lo.ToPtr(uuid.MustParse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa")), + }, + expected: "r.sub.id == 66666666-6666-6666-6666-666666666666 && r.config.id == 88888888-8888-8888-8888-888888888888 && r.playbook.id == aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", + }, + { + name: "No fields set", + perm: Permission{}, + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.perm.Principal() + if tt.expected != result { + t.Errorf("Expected %s, got %s", tt.expected, result) + } + }) + } +}