Skip to content

Commit

Permalink
feat: permissions adapter for casbin (#1440)
Browse files Browse the repository at this point in the history
* feat: permissions adapter for casbin

[skip ci]

* use condition in policy_definition for ABAC

[skip ci]

* feat: add abac request provider

[skip ci]

* feat: remove middleware for abac

[skip ci]

* feat: permissions for playbook approval

[skip ci]

* chore: bump duty

* fix: failing test

* chore: use accessor from duty
  • Loading branch information
adityathebe authored Sep 26, 2024
1 parent 816d496 commit 00c1eab
Show file tree
Hide file tree
Showing 13 changed files with 246 additions and 100 deletions.
13 changes: 0 additions & 13 deletions db/playbooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,19 +41,6 @@ func FindPlaybookRun(ctx context.Context, id uuid.UUID) (*models.PlaybookRun, er
return &p, nil
}

func FindPlaybook(ctx context.Context, id uuid.UUID) (*models.Playbook, error) {
var p models.Playbook
if err := ctx.DB().Where("id = ?", id).First(&p).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}

return nil, err
}

return &p, nil
}

// CanApprove returns true if the given person can approve runs of the given playbook.
func CanApprove(ctx context.Context, personID, playbookID string) (bool, error) {
query := `
Expand Down
13 changes: 9 additions & 4 deletions playbook/approval.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ import (

"github.com/flanksource/commons/collections"
"github.com/flanksource/duty/api"
dutyAPI "github.com/flanksource/duty/api"
"github.com/flanksource/duty/context"
"github.com/flanksource/duty/models"
"github.com/google/uuid"
"github.com/labstack/echo/v4"

v1 "github.com/flanksource/incident-commander/api/v1"
"github.com/flanksource/incident-commander/db"
"github.com/flanksource/incident-commander/rbac"
)

func HandlePlaybookRunApproval(c echo.Context) error {
Expand All @@ -25,14 +25,14 @@ func HandlePlaybookRunApproval(c echo.Context) error {

runUUID, err := uuid.Parse(runID)
if err != nil {
return c.JSON(http.StatusBadRequest, dutyAPI.HTTPError{Err: err.Error(), Message: "invalid run id"})
return c.JSON(http.StatusBadRequest, api.HTTPError{Err: err.Error(), Message: "invalid run id"})
}

if err := ApproveRun(ctx, runUUID); err != nil {
return dutyAPI.WriteError(c, err)
return api.WriteError(c, err)
}

return c.JSON(http.StatusOK, dutyAPI.HTTPSuccess{Message: "playbook run approved"})
return c.JSON(http.StatusOK, api.HTTPSuccess{Message: "playbook run approved"})
}

func ApproveRun(ctx context.Context, runID uuid.UUID) error {
Expand All @@ -52,6 +52,11 @@ func requiresApproval(spec v1.PlaybookSpec) bool {

func approveRun(ctx context.Context, run *models.PlaybookRun) error {
approver := ctx.User()
if objects, err := run.GetRBACAttributes(ctx.DB()); err != nil {
return ctx.Oops().Wrap(err)
} else if !rbac.HasPermission(ctx, approver.ID.String(), objects, rbac.ActionPlaybookApprove) {
return ctx.Oops().Code(api.EFORBIDDEN).Errorf("forbidden to approve playbook")
}

var spec v1.PlaybookSpec
if err := json.Unmarshal(run.Spec, &spec); err != nil {
Expand Down
7 changes: 4 additions & 3 deletions playbook/controllers.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
dutyAPI "github.com/flanksource/duty/api"
"github.com/flanksource/duty/context"
"github.com/flanksource/duty/models"
"github.com/flanksource/duty/query"
"github.com/labstack/echo/v4"
"github.com/samber/lo"
"github.com/samber/oops"
Expand Down Expand Up @@ -64,7 +65,7 @@ func HandlePlaybookRun(c echo.Context) error {
return c.JSON(http.StatusBadRequest, oops.Wrapf(err, "invalid request"))
}

playbook, err := db.FindPlaybook(ctx, req.ID)
playbook, err := query.FindPlaybook(ctx, req.ID.String())
if err != nil {
return c.JSON(http.StatusInternalServerError, oops.Wrapf(err, "failed to get playbook"))
} else if playbook == nil {
Expand All @@ -73,7 +74,7 @@ func HandlePlaybookRun(c echo.Context) error {

run, err := Run(ctx, playbook, req)
if err != nil {
return c.JSON(http.StatusInternalServerError, oops.Wrap(err))
return dutyAPI.WriteError(c, oops.Wrap(err))
}

return c.JSON(http.StatusCreated, RunResponse{
Expand All @@ -97,7 +98,7 @@ func HandleGetPlaybookParams(c echo.Context) error {
return c.JSON(http.StatusBadRequest, dutyAPI.HTTPError{Err: err.Error(), Message: "invalid request"})
}

playbook, err := db.FindPlaybook(ctx, req.ID)
playbook, err := query.FindPlaybook(ctx, req.ID.String())
if err != nil {
return c.JSON(http.StatusInternalServerError, dutyAPI.HTTPError{Err: err.Error(), Message: "failed to get playbook"})
} else if playbook == nil {
Expand Down
24 changes: 17 additions & 7 deletions playbook/playbook.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ import (
"github.com/flanksource/duty/types"
"github.com/google/uuid"
"github.com/samber/lo"
"github.com/samber/oops"
"golang.org/x/text/cases"
"golang.org/x/text/language"

"github.com/flanksource/incident-commander/api"
v1 "github.com/flanksource/incident-commander/api/v1"
"github.com/flanksource/incident-commander/db"
"github.com/flanksource/incident-commander/playbook/runner"
"github.com/flanksource/incident-commander/rbac"
yamlutil "k8s.io/apimachinery/pkg/util/yaml"
)

Expand Down Expand Up @@ -130,6 +132,12 @@ func Run(ctx context.Context, playbook *models.Playbook, req RunParams) (*models
return nil, ctx.Oops().Wrap(err)
}

if objects, err := run.GetRBACAttributes(ctx.DB()); err != nil {
return nil, ctx.Oops().Wrap(err)
} else if !rbac.HasPermission(ctx, ctx.User().ID.String(), objects, rbac.ActionPlaybookRun) {
return nil, ctx.Oops().Code(dutyAPI.EFORBIDDEN).Errorf("forbidden to run playbook")
}

if err := req.setDefaults(ctx, spec, templateEnv); err != nil {
return nil, ctx.Oops().Wrap(err)
}
Expand All @@ -149,7 +157,7 @@ func Run(ctx context.Context, playbook *models.Playbook, req RunParams) (*models
return nil, err
}

if err := savePlaybookRun(ctx, playbook, &run); err != nil {
if err := savePlaybookRun(ctx, &run); err != nil {
return nil, ctx.Oops().Wrapf(err, "failed to create playbook run")
}

Expand Down Expand Up @@ -207,7 +215,7 @@ func saveRunAsConfigChange(ctx context.Context, playbook *models.Playbook, run m
}

// savePlaybookRun saves the run and attempts register an approval from the caller.
func savePlaybookRun(ctx context.Context, playbook *models.Playbook, run *models.PlaybookRun) error {
func savePlaybookRun(ctx context.Context, run *models.PlaybookRun) error {
tx := ctx.DB().Begin()
if tx.Error != nil {
return ctx.Oops("db").Wrap(tx.Error)
Expand All @@ -227,11 +235,13 @@ func savePlaybookRun(ctx context.Context, playbook *models.Playbook, run *models
if requiresApproval(spec) {
// Attempt to auto approve run
if err := ApproveRun(ctx, run.ID); err != nil {
switch dutyAPI.ErrorCode(err) {
case dutyAPI.EFORBIDDEN, dutyAPI.EINVALID:
// ignore these errors
default:
return ctx.Oops().Errorf("error while attempting to auto approve run: %w", err)
if oopserr, ok := oops.AsOops(err); ok {
switch oopserr.Code() {
case dutyAPI.EFORBIDDEN, dutyAPI.EINVALID:
// ignore these errors
default:
return ctx.Oops().Errorf("error while attempting to auto approve run: %w", err)
}
}
}
}
Expand Down
18 changes: 9 additions & 9 deletions playbook/playbook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ var _ = Describe("Playbook", func() {
})

It("should execute the playbook", func() {
run = runPlaybook(playbook, RunParams{
run = runPlaybook(DefaultContext.WithUser(&dummy.JohnDoe), playbook, RunParams{
ConfigID: lo.ToPtr(dummy.EKSCluster.ID),
Params: map[string]string{
"path": dataFile,
Expand Down Expand Up @@ -308,7 +308,7 @@ var _ = Describe("Playbook", func() {
})

It("should store playbook run via API", func() {
run = runPlaybook(playbook, RunParams{
run = runPlaybook(DefaultContext.WithUser(&dummy.JohnDoe), playbook, RunParams{
ConfigID: lo.ToPtr(dummy.EKSCluster.ID),
Params: map[string]string{
"path": dataFile,
Expand Down Expand Up @@ -395,7 +395,7 @@ var _ = Describe("Playbook", func() {
})

It("should execute the playbook", func() {
run = runPlaybook(playbook, RunParams{
run = runPlaybook(DefaultContext.WithUser(&dummy.JohnDoe), playbook, RunParams{
ConfigID: lo.ToPtr(dummy.KubernetesNodeA.ID),
}, models.PlaybookRunStatusScheduled, models.PlaybookRunStatusWaiting)

Expand Down Expand Up @@ -467,7 +467,7 @@ var _ = Describe("Playbook", func() {
return
}

run := createAndRun("exec-powershell", RunParams{
run := createAndRun(DefaultContext.WithUser(&dummy.JohnDoe), "exec-powershell", RunParams{
ConfigID: lo.ToPtr(dummy.KubernetesNodeA.ID),
})
Expect(run.Status).To(Equal(models.PlaybookRunStatusCompleted), run.String(DefaultContext.DB()))
Expand Down Expand Up @@ -513,7 +513,7 @@ var _ = Describe("Playbook", func() {

for _, test := range tests {
It(test.description, func() {
run := createAndRun(test.name, test.params, test.status)
run := createAndRun(DefaultContext.WithUser(&dummy.JohnDoe), test.name, test.params, test.status)
if test.extra != nil {
test.extra(run)
}
Expand All @@ -522,13 +522,13 @@ var _ = Describe("Playbook", func() {
})
})

func createAndRun(name string, params RunParams, statuses ...models.PlaybookRunStatus) *models.PlaybookRun {
func createAndRun(ctx context.Context, name string, params RunParams, statuses ...models.PlaybookRunStatus) *models.PlaybookRun {
playbook, _ := createPlaybook(name)
return runPlaybook(playbook, params, statuses...)
return runPlaybook(ctx, playbook, params, statuses...)
}

func runPlaybook(playbook models.Playbook, params RunParams, statuses ...models.PlaybookRunStatus) *models.PlaybookRun {
run, err := Run(DefaultContext, &playbook, params)
func runPlaybook(ctx context.Context, playbook models.Playbook, params RunParams, statuses ...models.PlaybookRunStatus) *models.PlaybookRun {
run, err := Run(ctx, &playbook, params)
Expect(err).To(BeNil())
return waitFor(run, statuses...)
}
Expand Down
29 changes: 29 additions & 0 deletions rbac/abac.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package rbac

import (
"github.com/flanksource/duty/models"
"github.com/labstack/echo/v4"
)

const (
ActionPlaybookRun = "playbook:run"
ActionPlaybookApprove = "playbook:approve"
)

type ABACResource struct {
Playbook models.Playbook `json:"playbook"`
Check models.Check `json:"check"`
Config models.ConfigItem `json:"config"`
Component models.Component `json:"component"`
}

func (r ABACResource) AsMap() map[string]any {
return map[string]any{
"component": r.Component.AsMap(),
"config": r.Config.AsMap(),
"check": r.Check.AsMap(),
"playbook": r.Playbook.AsMap(),
}
}

type EchoABACResourceGetter func(c echo.Context, action string) (string, *ABACResource, error)
59 changes: 0 additions & 59 deletions rbac/adapter.go

This file was deleted.

59 changes: 59 additions & 0 deletions rbac/adapter/permission.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package adapter

import (
"fmt"

"github.com/casbin/casbin/v2/model"
"github.com/casbin/casbin/v2/persist"
gormadapter "github.com/casbin/gorm-adapter/v3"
"github.com/flanksource/duty/models"
"gorm.io/gorm"
)

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.Find(&permissions).Error; err != nil {
return fmt.Errorf("failed to load permissions: %w", err)
}

for _, permission := range permissions {
policy := permissionToCasbinRule(permission)
if err := persist.LoadPolicyArray(policy, model); err != nil {
return err
}
}

return nil
}

func permissionToCasbinRule(permission models.Permission) []string {
m := []string{
"p",
permission.Principal(),
"*",
permission.Action,
permission.Effect(),
permission.Condition(),
}

return m
}
Loading

0 comments on commit 00c1eab

Please sign in to comment.