From 6e978d64d0077f30f28855b2bb9e94dfde1ab2da Mon Sep 17 00:00:00 2001 From: Karan Kajla Date: Thu, 29 Jun 2023 16:57:07 -0700 Subject: [PATCH] Track events for cascade warrant deletes on object deletion (#175) * Track access revoked events when cascade deleting warrants for an object being deleted * Fix incorrect test * Only track access revoked events if warrants are actually deleted --- pkg/authz/warrant/mysql.go | 102 ++++++++++++++++++++++++++------ pkg/authz/warrant/postgres.go | 102 ++++++++++++++++++++++++++------ pkg/authz/warrant/repository.go | 7 ++- pkg/authz/warrant/service.go | 56 ++++++++++++++++-- pkg/authz/warrant/sqlite.go | 102 ++++++++++++++++++++++++++------ tests/authz-policy.json | 2 +- 6 files changed, 306 insertions(+), 65 deletions(-) diff --git a/pkg/authz/warrant/mysql.go b/pkg/authz/warrant/mysql.go index 530fce0e..9080fd96 100644 --- a/pkg/authz/warrant/mysql.go +++ b/pkg/authz/warrant/mysql.go @@ -6,6 +6,7 @@ import ( "fmt" "time" + "github.com/jmoiron/sqlx" "github.com/pkg/errors" "github.com/warrant-dev/warrant/pkg/database" "github.com/warrant-dev/warrant/pkg/service" @@ -60,32 +61,39 @@ func (repo MySQLRepository) Create(ctx context.Context, model Model) (int64, err return newWarrantId, nil } -func (repo MySQLRepository) DeleteById(ctx context.Context, id int64) error { - _, err := repo.DB.ExecContext( - ctx, +func (repo MySQLRepository) DeleteById(ctx context.Context, ids []int64) error { + query, args, err := sqlx.In( ` UPDATE warrant SET deletedAt = ? WHERE - id = ? AND + id IN (?) AND deletedAt IS NULL `, time.Now().UTC(), - id, + ids, + ) + if err != nil { + return errors.Wrapf(err, "error deleting warrants %v", ids) + } + _, err = repo.DB.ExecContext( + ctx, + query, + args..., ) if err != nil { switch err { case sql.ErrNoRows: - return service.NewRecordNotFoundError("Warrant", id) + return nil default: - return errors.Wrapf(err, "error deleting warrant %d", id) + return errors.Wrapf(err, "error deleting warrants %v", ids) } } return nil } -func (repo MySQLRepository) DeleteAllByObject(ctx context.Context, objectType string, objectId string) error { +func (repo MySQLRepository) Delete(ctx context.Context, objectType string, objectId string, relation string, subjectType string, subjectId string, subjectRelation string, policyHash string) error { _, err := repo.DB.ExecContext( ctx, ` @@ -95,50 +103,106 @@ func (repo MySQLRepository) DeleteAllByObject(ctx context.Context, objectType st WHERE objectType = ? AND objectId = ? AND + relation = ? AND + subjectType = ? AND + subjectId = ? AND + subjectRelation = ? AND + policyHash = ? AND deletedAt IS NULL `, time.Now().UTC(), objectType, objectId, + relation, + subjectType, + subjectId, + subjectRelation, + policyHash, ) if err != nil { switch err { case sql.ErrNoRows: - return nil + wntErrorId := fmt.Sprintf("%s:%s#%s@%s:%s", objectType, objectId, relation, subjectType, subjectId) + if subjectRelation != "" { + wntErrorId = fmt.Sprintf("%s#%s", wntErrorId, subjectRelation) + } + if policyHash != "" { + wntErrorId = fmt.Sprintf("%s[%s]", wntErrorId, policyHash) + } + + return service.NewRecordNotFoundError("Warrant", wntErrorId) default: - return errors.Wrapf(err, "error deleting warrants with object %s:%s", objectType, objectId) + return errors.Wrap(err, "error deleting warrant") } } return nil } -func (repo MySQLRepository) DeleteAllBySubject(ctx context.Context, subjectType string, subjectId string) error { - _, err := repo.DB.ExecContext( +func (repo MySQLRepository) GetAllMatchingObject(ctx context.Context, objectType string, objectId string) ([]Model, error) { + models := make([]Model, 0) + warrants := make([]Warrant, 0) + err := repo.DB.SelectContext( ctx, + &warrants, ` - UPDATE warrant - SET - deletedAt = ? + SELECT id, objectType, objectId, relation, subjectType, subjectId, subjectRelation, policy, createdAt, updatedAt, deletedAt + FROM warrant + WHERE + objectType = ? AND + objectId = ? AND + deletedAt IS NULL + `, + objectType, + objectId, + ) + if err != nil { + switch err { + case sql.ErrNoRows: + return models, nil + default: + return models, errors.Wrapf(err, "error deleting warrants with object %s:%s", objectType, objectId) + } + } + + for i := range warrants { + models = append(models, &warrants[i]) + } + + return models, nil +} + +func (repo MySQLRepository) GetAllMatchingSubject(ctx context.Context, subjectType string, subjectId string) ([]Model, error) { + models := make([]Model, 0) + warrants := make([]Warrant, 0) + err := repo.DB.SelectContext( + ctx, + &warrants, + ` + SELECT id, objectType, objectId, relation, subjectType, subjectId, subjectRelation, policy, createdAt, updatedAt, deletedAt + FROM warrant WHERE subjectType = ? AND subjectId = ? AND deletedAt IS NULL `, - time.Now().UTC(), subjectType, subjectId, ) if err != nil { switch err { case sql.ErrNoRows: - return nil + return models, nil default: - return errors.Wrapf(err, "error deleting warrants with subject %s:%s", subjectType, subjectId) + return models, errors.Wrapf(err, "error deleting warrants with subject %s:%s", subjectType, subjectId) } } - return nil + for i := range warrants { + models = append(models, &warrants[i]) + } + + return models, nil } func (repo MySQLRepository) Get(ctx context.Context, objectType string, objectId string, relation string, subjectType string, subjectId string, subjectRelation string, policyHash string) (Model, error) { diff --git a/pkg/authz/warrant/postgres.go b/pkg/authz/warrant/postgres.go index 4f67f26b..fd7be1cf 100644 --- a/pkg/authz/warrant/postgres.go +++ b/pkg/authz/warrant/postgres.go @@ -7,6 +7,7 @@ import ( "regexp" "time" + "github.com/jmoiron/sqlx" "github.com/pkg/errors" "github.com/warrant-dev/warrant/pkg/database" "github.com/warrant-dev/warrant/pkg/service" @@ -59,32 +60,39 @@ func (repo PostgresRepository) Create(ctx context.Context, model Model) (int64, return newWarrantId, nil } -func (repo PostgresRepository) DeleteById(ctx context.Context, id int64) error { - _, err := repo.DB.ExecContext( - ctx, +func (repo PostgresRepository) DeleteById(ctx context.Context, ids []int64) error { + query, args, err := sqlx.In( ` UPDATE warrant SET deleted_at = ? WHERE - id = ? AND + id IN (?) AND deleted_at IS NULL `, time.Now().UTC(), - id, + ids, + ) + if err != nil { + return errors.Wrapf(err, "error deleting warrants %v", ids) + } + _, err = repo.DB.ExecContext( + ctx, + query, + args..., ) if err != nil { switch err { case sql.ErrNoRows: - return service.NewRecordNotFoundError("Warrant", id) + return nil default: - return errors.Wrapf(err, "error deleting warrant %d", id) + return errors.Wrapf(err, "error deleting warrants %v", ids) } } return nil } -func (repo PostgresRepository) DeleteAllByObject(ctx context.Context, objectType string, objectId string) error { +func (repo PostgresRepository) Delete(ctx context.Context, objectType string, objectId string, relation string, subjectType string, subjectId string, subjectRelation string, policyHash string) error { _, err := repo.DB.ExecContext( ctx, ` @@ -94,50 +102,106 @@ func (repo PostgresRepository) DeleteAllByObject(ctx context.Context, objectType WHERE object_type = ? AND object_id = ? AND + relation = ? AND + subject_type = ? AND + subject_id = ? AND + subject_relation = ? AND + policy_hash = ? AND deleted_at IS NULL `, time.Now().UTC(), objectType, objectId, + relation, + subjectType, + subjectId, + subjectRelation, + policyHash, ) if err != nil { switch err { case sql.ErrNoRows: - return nil + wntErrorId := fmt.Sprintf("%s:%s#%s@%s:%s", objectType, objectId, relation, subjectType, subjectId) + if subjectRelation != "" { + wntErrorId = fmt.Sprintf("%s#%s", wntErrorId, subjectRelation) + } + if policyHash != "" { + wntErrorId = fmt.Sprintf("%s[%s]", wntErrorId, policyHash) + } + + return service.NewRecordNotFoundError("Warrant", wntErrorId) default: - return errors.Wrapf(err, "error deleting warrants with object %s:%s", objectType, objectId) + return errors.Wrap(err, "error deleting warrant") } } return nil } -func (repo PostgresRepository) DeleteAllBySubject(ctx context.Context, subjectType string, subjectId string) error { - _, err := repo.DB.ExecContext( +func (repo PostgresRepository) GetAllMatchingObject(ctx context.Context, objectType string, objectId string) ([]Model, error) { + models := make([]Model, 0) + warrants := make([]Warrant, 0) + err := repo.DB.SelectContext( ctx, + &warrants, ` - UPDATE warrant - SET - deleted_at = ? + SELECT id, object_type, object_id, relation, subject_type, subject_id, subject_relation, policy, created_at, updated_at, deleted_at + FROM warrant + WHERE + object_type = ? AND + object_id = ? AND + deleted_at IS NULL + `, + objectType, + objectId, + ) + if err != nil { + switch err { + case sql.ErrNoRows: + return models, nil + default: + return models, errors.Wrapf(err, "error deleting warrants with object %s:%s", objectType, objectId) + } + } + + for i := range warrants { + models = append(models, &warrants[i]) + } + + return models, nil +} + +func (repo PostgresRepository) GetAllMatchingSubject(ctx context.Context, subjectType string, subjectId string) ([]Model, error) { + models := make([]Model, 0) + warrants := make([]Warrant, 0) + err := repo.DB.SelectContext( + ctx, + &warrants, + ` + SELECT id, object_type, object_id, relation, subject_type, subject_id, subject_relation, policy, created_at, updated_at, deleted_at + FROM warrant WHERE subject_type = ? AND subject_id = ? AND deleted_at IS NULL `, - time.Now().UTC(), subjectType, subjectId, ) if err != nil { switch err { case sql.ErrNoRows: - return nil + return models, nil default: - return errors.Wrapf(err, "error deleting warrants with subject %s:%s", subjectType, subjectId) + return models, errors.Wrapf(err, "error deleting warrants with subject %s:%s", subjectType, subjectId) } } - return nil + for i := range warrants { + models = append(models, &warrants[i]) + } + + return models, nil } func (repo PostgresRepository) Get(ctx context.Context, objectType string, objectId string, relation string, subjectType string, subjectId string, subjectRelation string, policyHash string) (Model, error) { diff --git a/pkg/authz/warrant/repository.go b/pkg/authz/warrant/repository.go index d01e9857..c8105373 100644 --- a/pkg/authz/warrant/repository.go +++ b/pkg/authz/warrant/repository.go @@ -17,9 +17,10 @@ type WarrantRepository interface { GetAllMatchingObjectAndRelation(ctx context.Context, objectType string, objectId string, relation string) ([]Model, error) GetAllMatchingObjectAndRelationBySubjectType(ctx context.Context, objectType string, objectId string, relation string, subjectType string) ([]Model, error) List(ctx context.Context, filterOptions *FilterOptions, listParams service.ListParams) ([]Model, error) - DeleteById(ctx context.Context, id int64) error - DeleteAllByObject(ctx context.Context, objectType string, objectId string) error - DeleteAllBySubject(ctx context.Context, subjectType string, subjectId string) error + Delete(ctx context.Context, objectType string, objectId string, relation string, subjectType string, subjectId string, subjectRelation string, policyHash string) error + DeleteById(ctx context.Context, ids []int64) error + GetAllMatchingObject(ctx context.Context, objectType string, objectId string) ([]Model, error) + GetAllMatchingSubject(ctx context.Context, subjectType string, subjectId string) ([]Model, error) } func NewRepository(db database.Database) (WarrantRepository, error) { diff --git a/pkg/authz/warrant/service.go b/pkg/authz/warrant/service.go index 29f7e39d..1a279a4f 100644 --- a/pkg/authz/warrant/service.go +++ b/pkg/authz/warrant/service.go @@ -110,12 +110,12 @@ func (svc WarrantService) Delete(ctx context.Context, warrantSpec WarrantSpec) ( return nil } - warrant, err := svc.Repository.Get(txCtx, warrantToDelete.GetObjectType(), warrantToDelete.GetObjectId(), warrantToDelete.GetRelation(), warrantToDelete.GetSubjectType(), warrantToDelete.GetSubjectId(), warrantToDelete.GetSubjectRelation(), warrantToDelete.GetPolicyHash()) + _, err = svc.Repository.Get(txCtx, warrantToDelete.GetObjectType(), warrantToDelete.GetObjectId(), warrantToDelete.GetRelation(), warrantToDelete.GetSubjectType(), warrantToDelete.GetSubjectId(), warrantToDelete.GetSubjectRelation(), warrantToDelete.GetPolicyHash()) if err != nil { return err } - err = svc.Repository.DeleteById(txCtx, warrant.GetID()) + err = svc.Repository.Delete(txCtx, warrantToDelete.GetObjectType(), warrantToDelete.GetObjectId(), warrantToDelete.GetRelation(), warrantToDelete.GetSubjectType(), warrantToDelete.GetSubjectId(), warrantToDelete.GetSubjectRelation(), warrantToDelete.GetPolicyHash()) if err != nil { return err } @@ -142,16 +142,64 @@ func (svc WarrantService) Delete(ctx context.Context, warrantSpec WarrantSpec) ( func (svc WarrantService) DeleteRelatedWarrants(ctx context.Context, objectType string, objectId string) (*wookie.Token, error) { newWookie, e := svc.WookieSvc.WithWookieUpdate(ctx, func(txCtx context.Context) error { - err := svc.Repository.DeleteAllByObject(txCtx, objectType, objectId) + warrantIdsToDelete := make([]int64, 0) + accessRevokedEvents := make([]event.CreateAccessEventSpec, 0) + warrantsMatchingObject, err := svc.Repository.GetAllMatchingObject(txCtx, objectType, objectId) if err != nil { return err } - err = svc.Repository.DeleteAllBySubject(txCtx, objectType, objectId) + for _, warrant := range warrantsMatchingObject { + warrantIdsToDelete = append(warrantIdsToDelete, warrant.GetID()) + accessRevokedEvents = append(accessRevokedEvents, event.CreateAccessEventSpec{ + Type: event.EventTypeAccessRevoked, + Source: event.EventSourceApi, + ObjectType: warrant.GetObjectType(), + ObjectId: warrant.GetObjectId(), + Relation: warrant.GetRelation(), + SubjectType: warrant.GetSubjectType(), + SubjectId: warrant.GetSubjectId(), + SubjectRelation: warrant.GetSubjectRelation(), + Meta: map[string]interface{}{ + "policy": warrant.GetPolicy(), + }, + }) + } + + warrantsMatchingSubject, err := svc.Repository.GetAllMatchingSubject(txCtx, objectType, objectId) if err != nil { return err } + for _, warrant := range warrantsMatchingSubject { + warrantIdsToDelete = append(warrantIdsToDelete, warrant.GetID()) + accessRevokedEvents = append(accessRevokedEvents, event.CreateAccessEventSpec{ + Type: event.EventTypeAccessRevoked, + Source: event.EventSourceApi, + ObjectType: warrant.GetObjectType(), + ObjectId: warrant.GetObjectId(), + Relation: warrant.GetRelation(), + SubjectType: warrant.GetSubjectType(), + SubjectId: warrant.GetSubjectId(), + SubjectRelation: warrant.GetSubjectRelation(), + Meta: map[string]interface{}{ + "policy": warrant.GetPolicy(), + }, + }) + } + + if len(warrantIdsToDelete) > 0 { + err = svc.Repository.DeleteById(ctx, warrantIdsToDelete) + if err != nil { + return err + } + + err = svc.EventSvc.TrackAccessEvents(ctx, accessRevokedEvents) + if err != nil { + return err + } + } + return nil }) if e != nil { diff --git a/pkg/authz/warrant/sqlite.go b/pkg/authz/warrant/sqlite.go index 4afa72f6..19858c03 100644 --- a/pkg/authz/warrant/sqlite.go +++ b/pkg/authz/warrant/sqlite.go @@ -6,6 +6,7 @@ import ( "fmt" "time" + "github.com/jmoiron/sqlx" "github.com/pkg/errors" "github.com/warrant-dev/warrant/pkg/database" "github.com/warrant-dev/warrant/pkg/service" @@ -64,32 +65,39 @@ func (repo SQLiteRepository) Create(ctx context.Context, model Model) (int64, er return newWarrantId, nil } -func (repo SQLiteRepository) DeleteById(ctx context.Context, id int64) error { - _, err := repo.DB.ExecContext( - ctx, +func (repo SQLiteRepository) DeleteById(ctx context.Context, ids []int64) error { + query, args, err := sqlx.In( ` UPDATE warrant SET deletedAt = ? WHERE - id = ? AND + id IN (?) AND deletedAt IS NULL `, time.Now().UTC(), - id, + ids, + ) + if err != nil { + return errors.Wrapf(err, "error deleting warrants %v", ids) + } + _, err = repo.DB.ExecContext( + ctx, + query, + args..., ) if err != nil { switch err { case sql.ErrNoRows: - return service.NewRecordNotFoundError("Warrant", id) + return nil default: - return errors.Wrapf(err, "error deleting warrant %d", id) + return errors.Wrapf(err, "error deleting warrants %v", ids) } } return nil } -func (repo SQLiteRepository) DeleteAllByObject(ctx context.Context, objectType string, objectId string) error { +func (repo SQLiteRepository) Delete(ctx context.Context, objectType string, objectId string, relation string, subjectType string, subjectId string, subjectRelation string, policyHash string) error { _, err := repo.DB.ExecContext( ctx, ` @@ -99,50 +107,106 @@ func (repo SQLiteRepository) DeleteAllByObject(ctx context.Context, objectType s WHERE objectType = ? AND objectId = ? AND + relation = ? AND + subjectType = ? AND + subjectId = ? AND + subjectRelation = ? AND + policyHash = ? AND deletedAt IS NULL `, time.Now().UTC(), objectType, objectId, + relation, + subjectType, + subjectId, + subjectRelation, + policyHash, ) if err != nil { switch err { case sql.ErrNoRows: - return nil + wntErrorId := fmt.Sprintf("%s:%s#%s@%s:%s", objectType, objectId, relation, subjectType, subjectId) + if subjectRelation != "" { + wntErrorId = fmt.Sprintf("%s#%s", wntErrorId, subjectRelation) + } + if policyHash != "" { + wntErrorId = fmt.Sprintf("%s[%s]", wntErrorId, policyHash) + } + + return service.NewRecordNotFoundError("Warrant", wntErrorId) default: - return errors.Wrapf(err, "error deleting warrants with object %s:%s", objectType, objectId) + return errors.Wrap(err, "error deleting warrant") } } return nil } -func (repo SQLiteRepository) DeleteAllBySubject(ctx context.Context, subjectType string, subjectId string) error { - _, err := repo.DB.ExecContext( +func (repo SQLiteRepository) GetAllMatchingObject(ctx context.Context, objectType string, objectId string) ([]Model, error) { + models := make([]Model, 0) + warrants := make([]Warrant, 0) + err := repo.DB.SelectContext( ctx, + &warrants, ` - UPDATE warrant - SET - deletedAt = ? + SELECT id, objectType, objectId, relation, subjectType, subjectId, subjectRelation, policy, createdAt, updatedAt, deletedAt + FROM warrant + WHERE + objectType = ? AND + objectId = ? AND + deletedAt IS NULL + `, + objectType, + objectId, + ) + if err != nil { + switch err { + case sql.ErrNoRows: + return models, nil + default: + return models, errors.Wrapf(err, "error deleting warrants with object %s:%s", objectType, objectId) + } + } + + for i := range warrants { + models = append(models, &warrants[i]) + } + + return models, nil +} + +func (repo SQLiteRepository) GetAllMatchingSubject(ctx context.Context, subjectType string, subjectId string) ([]Model, error) { + models := make([]Model, 0) + warrants := make([]Warrant, 0) + err := repo.DB.SelectContext( + ctx, + &warrants, + ` + SELECT id, objectType, objectId, relation, subjectType, subjectId, subjectRelation, policy, createdAt, updatedAt, deletedAt + FROM warrant WHERE subjectType = ? AND subjectId = ? AND deletedAt IS NULL `, - time.Now().UTC(), subjectType, subjectId, ) if err != nil { switch err { case sql.ErrNoRows: - return nil + return models, nil default: - return errors.Wrapf(err, "error deleting warrants with subject %s:%s", subjectType, subjectId) + return models, errors.Wrapf(err, "error deleting warrants with subject %s:%s", subjectType, subjectId) } } - return nil + for i := range warrants { + models = append(models, &warrants[i]) + } + + return models, nil } func (repo SQLiteRepository) Get(ctx context.Context, objectType string, objectId string, relation string, subjectType string, subjectId string, subjectRelation string, policyHash string) (Model, error) { diff --git a/tests/authz-policy.json b/tests/authz-policy.json index f49ad184..4e17463b 100644 --- a/tests/authz-policy.json +++ b/tests/authz-policy.json @@ -229,7 +229,7 @@ "objectType": "user", "objectId": "user-a" }, - "policy": "clientIp matches \"192\\.168\\..*\\..*\"" + "policy": "clientIp matches \"192\\\\.168\\\\..*\\\\..*\"" } }, "expectedResponse": {