diff --git a/context/context.go b/context/context.go index 4e6f49f6..821ed3f2 100644 --- a/context/context.go +++ b/context/context.go @@ -181,6 +181,26 @@ func (k Context) WithUser(user *models.Person) Context { return k.WithValue("user", user) } +// Rbac subject +func (k Context) WithSubject(subject string) Context { + k.GetSpan().SetAttributes(attribute.String("rbac-subject", subject)) + return k.WithValue("rbac-subject", subject) +} + +func (k Context) Subject() string { + subject := k.Value("rbac-subject") + if subject != "" { + return subject.(string) + } + + user := k.User() + if user != nil { + return user.ID.String() + } + + return "" +} + func (k Context) WithoutName() Context { k.Logger = logger.GetLogger() return k diff --git a/models/notifications.go b/models/notifications.go index 87c32f00..d206d64f 100644 --- a/models/notifications.go +++ b/models/notifications.go @@ -67,6 +67,12 @@ const ( NotificationStatusSilenced = "silenced" NotificationStatusRepeatInterval = "repeat-interval" + // an event was triggered and the notification is waiting for the playbook run to be triggered. + NotificationStatusPendingPlaybookRun = "pending_playbook_run" + + // A playbook is currently in progress + NotificationStatusPendingPlaybookCompletion = "pending_playbook_completion" + // health related notifications of kubernetes config items get into this state // to wait for the incremental scraper to re-evaluate the health. NotificationStatusEvaluatingWaitFor = "evaluating-waitfor" @@ -137,6 +143,11 @@ func (t *NotificationSendHistory) Sending() *NotificationSendHistory { return t } +func (t *NotificationSendHistory) PendingPlaybookRun() *NotificationSendHistory { + t.Status = NotificationStatusPendingPlaybookRun + return t.End() +} + func (t *NotificationSendHistory) Sent() *NotificationSendHistory { t.Status = NotificationStatusSent return t.End() diff --git a/models/permission.go b/models/permission.go index 0bc4ffa9..91fe5c4d 100644 --- a/models/permission.go +++ b/models/permission.go @@ -9,22 +9,23 @@ import ( ) type Permission struct { - ID uuid.UUID `json:"id" gorm:"default:generate_ulid()"` - Action string `json:"action"` - ConnectionID *uuid.UUID `json:"connection_id,omitempty"` - 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"` + ID uuid.UUID `json:"id" gorm:"default:generate_ulid()"` + Action string `json:"action"` + ConnectionID *uuid.UUID `json:"connection_id,omitempty"` + 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"` + NotificationID *uuid.UUID `json:"notification_id,omitempty"` + Until *time.Time `json:"until"` + UpdatedAt *time.Time `json:"updated_at"` + UpdatedBy *uuid.UUID `json:"updated_by"` } func (t *Permission) Principal() string { @@ -36,6 +37,10 @@ func (t *Permission) Principal() string { return t.TeamID.String() } + if t.NotificationID != nil { + return t.NotificationID.String() + } + return "" } @@ -43,19 +48,19 @@ func (t *Permission) Condition() string { var rule []string if t.ComponentID != nil { - rule = append(rule, fmt.Sprintf("r.obj.component != undefined && r.obj.component.id == %q", t.ComponentID.String())) + rule = append(rule, fmt.Sprintf("r.obj.component.id == %q", t.ComponentID.String())) } if t.ConfigID != nil { - rule = append(rule, fmt.Sprintf("r.obj.config != undefined && r.obj.config.id == %q", t.ConfigID.String())) + rule = append(rule, fmt.Sprintf("r.obj.config.id == %q", t.ConfigID.String())) } if t.CanaryID != nil { - rule = append(rule, fmt.Sprintf("r.obj.canary != undefined && r.obj.canary.id == %q", t.CanaryID.String())) + rule = append(rule, fmt.Sprintf("r.obj.canary.id == %q", t.CanaryID.String())) } if t.PlaybookID != nil { - rule = append(rule, fmt.Sprintf("r.obj.playbook != undefined && r.obj.playbook.id == %q", t.PlaybookID.String())) + rule = append(rule, fmt.Sprintf("r.obj.playbook.id == %q", t.PlaybookID.String())) } return strings.Join(rule, " && ") diff --git a/models/permission_test.go b/models/permission_test.go index 1ddd9525..8ce1cb75 100644 --- a/models/permission_test.go +++ b/models/permission_test.go @@ -18,7 +18,7 @@ func TestPermission_Condition(t *testing.T) { perm: Permission{ PlaybookID: lo.ToPtr(uuid.MustParse("33333333-3333-3333-3333-333333333333")), }, - expected: `r.obj.playbook != undefined && r.obj.playbook.id == "33333333-3333-3333-3333-333333333333"`, + expected: `r.obj.playbook.id == "33333333-3333-3333-3333-333333333333"`, }, { name: "Multiple fields II", @@ -26,7 +26,7 @@ func TestPermission_Condition(t *testing.T) { ConfigID: lo.ToPtr(uuid.MustParse("88888888-8888-8888-8888-888888888888")), PlaybookID: lo.ToPtr(uuid.MustParse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa")), }, - expected: `r.obj.config != undefined && r.obj.config.id == "88888888-8888-8888-8888-888888888888" && r.obj.playbook != undefined && r.obj.playbook.id == "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"`, + expected: `r.obj.config.id == "88888888-8888-8888-8888-888888888888" && r.obj.playbook.id == "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"`, }, { name: "No fields set", diff --git a/models/playbooks.go b/models/playbooks.go index 82c82087..bbf989d9 100644 --- a/models/playbooks.go +++ b/models/playbooks.go @@ -134,6 +134,8 @@ type PlaybookRun struct { Parameters types.JSONStringMap `json:"parameters,omitempty" gorm:"default:null"` Request types.JSONMap `json:"request,omitempty" gorm:"default:null"` AgentID *uuid.UUID `json:"agent_id,omitempty"` + + NotificationSendID *uuid.UUID `json:"notification_send_id,omitempty"` } func (p PlaybookRun) TableName() string { @@ -193,10 +195,28 @@ func (p PlaybookRun) End(db *gorm.DB) error { status = PlaybookRunStatusFailed } - return p.Update(db, map[string]any{ + if err := p.Update(db, map[string]any{ "status": status, "end_time": gorm.Expr("CLOCK_TIMESTAMP()"), - }) + }); err != nil { + return err + } + + if p.NotificationSendID != nil { + updates := map[string]any{} + if status == PlaybookRunStatusFailed { + updates["status"] = NotificationStatusError + updates["error"] = "playbook failed" + } else { + updates["status"] = NotificationStatusSent + } + + if err := db.Model(&NotificationSendHistory{}).Where("id = ?", *p.NotificationSendID).Updates(updates).Error; err != nil { + return err + } + } + + return nil } func (p PlaybookRun) Assign(db *gorm.DB, agent *Agent, action string) error { @@ -348,14 +368,14 @@ func (run *PlaybookRun) GetRBACAttributes(db *gorm.DB) (map[string]any, error) { if err := db.First(&playbook, run.PlaybookID).Error; err != nil { return nil, err } - output["playbook"] = playbook + output["playbook"] = playbook.AsMap() if run.ComponentID != nil { var component Component if err := db.First(&component, run.ComponentID).Error; err != nil { return nil, err } - output["component"] = component + output["component"] = component.AsMap() } if run.CheckID != nil { @@ -363,7 +383,7 @@ func (run *PlaybookRun) GetRBACAttributes(db *gorm.DB) (map[string]any, error) { if err := db.First(&check, run.CheckID).Error; err != nil { return nil, err } - output["check"] = check + output["check"] = check.AsMap() } if run.ConfigID != nil { @@ -371,7 +391,7 @@ func (run *PlaybookRun) GetRBACAttributes(db *gorm.DB) (map[string]any, error) { if err := db.First(&config, run.ConfigID).Error; err != nil { return nil, err } - output["config"] = config + output["config"] = config.AsMap() } return output, nil diff --git a/postq/sync_consumer.go b/postq/sync_consumer.go index a74ee8e5..59a887f4 100644 --- a/postq/sync_consumer.go +++ b/postq/sync_consumer.go @@ -55,7 +55,7 @@ func (t *SyncEventConsumer) Handle(ctx context.Context) (int, error) { } event.Attempts++ - // event.SetError(err.Error()) + event.SetError(err.Error()) const query = `UPDATE event_queue SET error=$1, attempts=$2, last_attempt=NOW() WHERE id=$3` if _, err := ctx.Pool().Exec(ctx, query, event.Error, event.Attempts, event.ID); err != nil { ctx.Debugf("error saving event attempt updates to event_queue: %v\n", err) diff --git a/schema/permissions.hcl b/schema/permissions.hcl index 2e92f88d..65b52d86 100644 --- a/schema/permissions.hcl +++ b/schema/permissions.hcl @@ -70,6 +70,11 @@ table "permissions" { type = uuid } + column "notification_id" { + null = true + type = uuid + } + column "updated_by" { null = true type = uuid @@ -97,52 +102,55 @@ table "permissions" { columns = [column.playbook_id] ref_columns = [table.playbooks.column.id] on_update = NO_ACTION - on_delete = NO_ACTION + on_delete = CASCADE } - foreign_key "permissions_canary_id_fkey" { columns = [column.canary_id] ref_columns = [table.canaries.column.id] on_update = NO_ACTION - on_delete = NO_ACTION + on_delete = CASCADE } foreign_key "permissions_component_id_fkey" { columns = [column.component_id] ref_columns = [table.components.column.id] on_update = NO_ACTION - on_delete = NO_ACTION + on_delete = CASCADE } foreign_key "permissions_connection_id_fkey" { columns = [column.connection_id] ref_columns = [table.connections.column.id] on_update = NO_ACTION - on_delete = NO_ACTION + on_delete = CASCADE } foreign_key "permissions_config_id_fkey" { columns = [column.config_id] ref_columns = [table.config_items.column.id] on_update = NO_ACTION - on_delete = NO_ACTION + on_delete = CASCADE } foreign_key "permissions_created_by_fkey" { columns = [column.created_by] ref_columns = [table.people.column.id] on_update = NO_ACTION - on_delete = NO_ACTION + on_delete = CASCADE + } + foreign_key "permissions_notification_fkey" { + columns = [column.notification_id] + ref_columns = [table.notifications.column.id] + on_update = NO_ACTION + on_delete = CASCADE } - foreign_key "permissions_person_fkey" { columns = [column.person_id] ref_columns = [table.people.column.id] on_update = NO_ACTION - on_delete = NO_ACTION + on_delete = CASCADE } - foreign_key "permissions_team_fkey" { columns = [column.team_id] ref_columns = [table.teams.column.id] on_update = NO_ACTION - on_delete = NO_ACTION + on_delete = CASCADE } index "permissions_config_id_idx" { diff --git a/schema/playbooks.hcl b/schema/playbooks.hcl index af3dcd7b..c96540ac 100644 --- a/schema/playbooks.hcl +++ b/schema/playbooks.hcl @@ -6,8 +6,8 @@ table "playbooks" { default = sql("generate_ulid()") } column "namespace" { - null = false - type = text + null = false + type = text default = "default" } column "name" { @@ -15,8 +15,8 @@ table "playbooks" { type = text } column "title" { - null = false - type = text + null = false + type = text default = "" } column "icon" { @@ -40,8 +40,8 @@ table "playbooks" { type = enum.source } column "category" { - null = false - type = text + null = false + type = text default = "" } column "created_at" { @@ -136,8 +136,8 @@ table "playbook_runs" { type = uuid } column "spec" { - null = false - type = jsonb + null = false + type = jsonb default = "{}" # temporary default value to make the migration possible. we can remove this later. } column "status" { @@ -169,6 +169,11 @@ table "playbook_runs" { null = true type = uuid } + column "notification_send_id" { + null = true + column = "the notification dispatch that triggered this run" + type = uuid + } column "check_id" { null = true type = uuid @@ -207,6 +212,12 @@ table "playbook_runs" { on_update = NO_ACTION on_delete = CASCADE } + foreign_key "playbook_run_notification_send_id_fkey" { + columns = [column.notification_send_id] + ref_columns = [table.notification_send_history.column.id] + on_update = NO_ACTION + on_delete = NO_ACTION + } foreign_key "playbook_run_created_by_fkey" { columns = [column.created_by] ref_columns = [table.people.column.id]