Skip to content

Commit

Permalink
feat: handle spicedb deletes (#896)
Browse files Browse the repository at this point in the history
* add spicedb.DeleteObject()

* delete most things

* fix linter issues
  • Loading branch information
FoseFx authored Dec 5, 2024
1 parent 8515eff commit 98310d3
Show file tree
Hide file tree
Showing 11 changed files with 148 additions and 35 deletions.
4 changes: 3 additions & 1 deletion libs/hwauthz/authz.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ func (b *Tx) Commit(ctx context.Context) (ConsistencyToken, error) {
type AuthZ interface {
// Create adds one or many Relationship Tuples to the Permissions Graph
Create(relationships ...Relationship) *Tx
// Delete removes one or many Relationship Tuples to the Permissions Graph
// Delete removes one or many Relationship Tuples to the Permissions Graph, also see DeleteObject
Delete(relationships ...Relationship) *Tx
// Check queries the Permission Graph for the existence of a PermissionCheck (i.e., a Relationship)
// We do not support the use of ConsistencyToken yet
Expand All @@ -189,6 +189,8 @@ type AuthZ interface {
// Useful, where the set of accessible resources is much smaller than the query results.
// Use this to first lookup permitted resources, and then restrict your database query to them.
LookupResources(ctx context.Context, subject Object, relation Relation, resourceType ObjectType) ([]string, error)
// DeleteObject deletes any direct relationships to this object from the permission graph
DeleteObject(ctx context.Context, object Object) error
}

// Error returns err, if not nil or StatusErrorPermissionDenied, if permissionGranted is false
Expand Down
42 changes: 42 additions & 0 deletions libs/hwauthz/spicedb/spicedb.go
Original file line number Diff line number Diff line change
Expand Up @@ -298,3 +298,45 @@ func (s *SpiceDBAuthZ) LookupResources(

return resources, nil
}

func (s *SpiceDBAuthZ) deleteDirectRelationships(ctx context.Context, object hwauthz.Object, isSubject bool) error {
var filter *v1.RelationshipFilter
if isSubject {
filter = &v1.RelationshipFilter{
OptionalSubjectFilter: &v1.SubjectFilter{
SubjectType: string(object.Type()),
OptionalSubjectId: object.ID(),
},
}
} else {
filter = &v1.RelationshipFilter{
ResourceType: string(object.Type()),
OptionalResourceId: object.ID(),
}
}

_, err := s.client.DeleteRelationships(ctx, &v1.DeleteRelationshipsRequest{
RelationshipFilter: filter,
})
if err != nil {
return fmt.Errorf("spicedb: could not delete resources: %w", err)
}

return nil
}

func (s *SpiceDBAuthZ) DeleteObject(ctx context.Context, object hwauthz.Object) error {
// delete all direct relationships where object is resource
err := s.deleteDirectRelationships(ctx, object, false)
if err != nil {
return fmt.Errorf("could not lookup related subjects: %w", err)
}

// delete all direct relationships where object is subject
err = s.deleteDirectRelationships(ctx, object, true)
if err != nil {
return fmt.Errorf("could not lookup related resources: %w", err)
}

return nil
}
48 changes: 48 additions & 0 deletions libs/hwauthz/spicedb/spicedb_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,51 @@ func TestLookupResources(t *testing.T) {
// assert
assert.ElementsMatch(t, []string{"0", "1", "2"}, subjects)
}

func TestDeleteObject(t *testing.T) {
// client
ctx := context.Background()
client := NewSpiceDBAuthZ()

// setup permission graph
ward := commonPerm.GenericObject{
Typ: "ward",
Id: uuid.New().String(),
}

org := commonPerm.GenericObject{
Typ: "organization",
Id: uuid.New().String(),
}
// the ward, which we will delete, is the (direct) resource of this relationship
orgRelation := hwauthz.Relation("organization")
resourceRelationship := hwauthz.NewRelationship(org, orgRelation, ward)
tx := client.Create(resourceRelationship)

room := commonPerm.GenericObject{
Typ: "room",
Id: uuid.New().String(),
}

// the ward is the (direct) subject of this relationship
roomRelation := hwauthz.Relation("ward")
subjectRelationship := hwauthz.NewRelationship(ward, roomRelation, room)
tx = tx.Create(subjectRelationship)

_, err := tx.Commit(ctx)
require.NoError(t, err)

// they exist before
exists, err := client.BulkCheck(ctx, []hwauthz.PermissionCheck{subjectRelationship, resourceRelationship})
require.NoError(t, err)
require.Equal(t, []bool{true, true}, exists)

// delete ward
err = client.DeleteObject(ctx, ward)
require.NoError(t, err)

// check relationships
exists, err = client.BulkCheck(ctx, []hwauthz.PermissionCheck{subjectRelationship, resourceRelationship})
require.NoError(t, err)
require.Equal(t, []bool{false, false}, exists)
}
4 changes: 4 additions & 0 deletions libs/hwauthz/test/true_authz.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,7 @@ func (s *TrueAuthZ) LookupResources(
) ([]string, error) {
return []string{}, nil
}

func (s *TrueAuthZ) DeleteObject(_ context.Context, _ hwauthz.Object) error {
return nil
}
7 changes: 5 additions & 2 deletions services/tasks-svc/internal/bed/bed.go
Original file line number Diff line number Diff line change
Expand Up @@ -395,12 +395,15 @@ func (s ServiceServer) DeleteBed(ctx context.Context, req *pb.DeleteBedRequest)
return nil, err
}

// delete from permission graph
if err := s.authz.DeleteObject(ctx, perm.Bed(bedID)); err != nil {
return nil, fmt.Errorf("could not delete bed from spicedb: %w", err)
}

log.Info().
Str("bedID", bedID.String()).
Msg("bed deleted")

// todo: delete from permission graph

// store event
if err := eventstoredb.SaveEntityEventForAggregate(ctx, s.es, NewBedAggregate(bedID),
&pbEventsV1.BedDeletedEvent{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,6 @@ import (
"tasks-svc/internal/patient/perm"

"github.com/EventStore/EventStore-Client-Go/v4/esdb"
"github.com/google/uuid"
zlog "github.com/rs/zerolog/log"

"tasks-svc/internal/patient/aggregate"
patientEventsV1 "tasks-svc/internal/patient/events/v1"
)
Expand All @@ -41,38 +38,31 @@ func NewProjection(es *esdb.Client, authz hwauthz.AuthZ, serviceName string) *Pr

func (p *Projection) initEventListeners() {
p.RegisterEventListener(patientEventsV1.PatientCreated, p.onPatientCreated)
p.RegisterEventListener(patientEventsV1.PatientDeleted, p.onPatientDeleted)
}

// Event handlers
func (p *Projection) onPatientCreated(ctx context.Context, evt hwes.Event) (error, *esdb.NackAction) {
log := zlog.Ctx(ctx)

var payload patientEventsV1.PatientCreatedEvent
if err := evt.GetJsonData(&payload); err != nil {
log.Error().Err(err).Msg("unmarshal failed")
return err, hwutil.PtrTo(esdb.NackActionPark)
}

patientID, err := uuid.Parse(payload.ID)
if err != nil {
return err, hwutil.PtrTo(esdb.NackActionPark)
}

if evt.OrganizationID == nil {
return errors.New("onPatientCreated: organizationID missing"), hwutil.PtrTo(esdb.NackActionSkip)
}
organizationID := *evt.OrganizationID

organization := commonPerm.Organization(organizationID)
patient := perm.Patient(patientID)

organization := commonPerm.Organization(*evt.OrganizationID)
patient := perm.Patient(evt.AggregateID)
relationship := hwauthz.NewRelationship(organization, perm.PatientOrganization, patient)

_, err = p.authz.Create(relationship).Commit(ctx)
_, err := p.authz.Create(relationship).Commit(ctx)
if err != nil {
return fmt.Errorf("onPatientCreated: could not write spicedb relationship: %w", err),
hwutil.PtrTo(esdb.NackActionRetry)
}

return nil, nil
}

func (p *Projection) onPatientDeleted(ctx context.Context, evt hwes.Event) (error, *esdb.NackAction) {
if err := p.authz.DeleteObject(ctx, perm.Patient(evt.AggregateID)); err != nil {
return fmt.Errorf("could not delete patient from spicedb: %w", err), hwutil.PtrTo(esdb.NackActionRetry)
}
return nil, nil
}
7 changes: 4 additions & 3 deletions services/tasks-svc/internal/room/room.go
Original file line number Diff line number Diff line change
Expand Up @@ -299,9 +299,10 @@ func (s ServiceServer) DeleteRoom(ctx context.Context, req *pb.DeleteRoomRequest
return nil, err
}

// TODO: Handle beds

// TODO: remove from spice (also beds)
// remove from permission graph
if err := s.authz.DeleteObject(ctx, perm.Room(roomID)); err != nil {
return nil, fmt.Errorf("could not delete room from spicedb: %w", err)
}

// store event
if err := eventstoredb.SaveEntityEventForAggregate(ctx, s.es, NewRoomAggregate(roomID),
Expand Down
6 changes: 6 additions & 0 deletions services/tasks-svc/internal/task-template/task_template.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"common"
"common/auth"
"context"
"fmt"
pbEventsV1 "gen/libs/events/v1"
"hwauthz"
"hwauthz/commonPerm"
Expand Down Expand Up @@ -182,6 +183,11 @@ func (s ServiceServer) DeleteTaskTemplate(
return nil, err
}

// delete from permission graph
if err := s.authz.DeleteObject(ctx, perm.TaskTemplate(id)); err != nil {
return nil, fmt.Errorf("could not delete template from spicedb: %w", err)
}

log.Info().
Str("taskTemplateID", id.String()).
Msg("taskTemplate deleted")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ func NewSpiceDBProjection(es *esdb.Client, authz hwauthz.AuthZ, serviceName stri

func (p *Projection) initEventListeners() {
p.RegisterEventListener(taskEventsV1.TaskCreated, p.onTaskCreated)
p.RegisterEventListener(taskEventsV1.TaskDeleted, p.onTaskDeleted)
}

func (p *Projection) onTaskCreated(ctx context.Context, evt hwes.Event) (error, *esdb.NackAction) {
Expand Down Expand Up @@ -75,3 +76,10 @@ func (p *Projection) onTaskCreated(ctx context.Context, evt hwes.Event) (error,

return nil, nil
}

func (p *Projection) onTaskDeleted(ctx context.Context, evt hwes.Event) (error, *esdb.NackAction) {
if err := p.authz.DeleteObject(ctx, perm.Task(evt.AggregateID)); err != nil {
return fmt.Errorf("could not delete task from spicedb: %w", err), hwutil.PtrTo(esdb.NackActionRetry)
}
return nil, nil
}
19 changes: 11 additions & 8 deletions services/tasks-svc/internal/ward/ward.go
Original file line number Diff line number Diff line change
Expand Up @@ -296,20 +296,20 @@ func (s *ServiceServer) DeleteWard(ctx context.Context, req *pb.DeleteWardReques
wardRepo := ward_repo.New(hwdb.GetDB())

// parse input
id, err := uuid.Parse(req.GetId())
wardID, err := uuid.Parse(req.GetId())
if err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error())
}

// check permissions
user := commonPerm.UserFromCtx(ctx)
check := hwauthz.NewPermissionCheck(user, perm.WardCanUserDelete, perm.Ward(id))
check := hwauthz.NewPermissionCheck(user, perm.WardCanUserDelete, perm.Ward(wardID))
if err := s.authz.Must(ctx, check); err != nil {
return nil, err
}

// check if exists
exists, err := wardRepo.ExistsWard(ctx, id)
exists, err := wardRepo.ExistsWard(ctx, wardID)
if !exists {
return nil, nil
}
Expand All @@ -319,21 +319,24 @@ func (s *ServiceServer) DeleteWard(ctx context.Context, req *pb.DeleteWardReques
}

// do query
err = wardRepo.DeleteWard(ctx, id)
err = wardRepo.DeleteWard(ctx, wardID)
err = hwdb.Error(ctx, err)
if err != nil {
return nil, err
}

// TODO: remove from spice (also rooms and beds)
// delete from permission graph
if err := s.authz.DeleteObject(ctx, perm.Ward(wardID)); err != nil {
return nil, fmt.Errorf("could not delete ward from spicedb: %w", err)
}

// remove from "recently used"
tracking.RemoveWardFromRecentActivity(ctx, id.String())
tracking.RemoveWardFromRecentActivity(ctx, wardID.String())

// store event
if err := eventstoredb.SaveEntityEventForAggregate(ctx, s.es, NewWardAggregate(id),
if err := eventstoredb.SaveEntityEventForAggregate(ctx, s.es, NewWardAggregate(wardID),
&pbEventsV1.WardDeletedEvent{
Id: id.String(),
Id: wardID.String(),
},
); err != nil {
return nil, err
Expand Down
6 changes: 6 additions & 0 deletions services/user-svc/internal/organization/organization.go
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,12 @@ func (s ServiceServer) DeleteOrganization(
return nil, err
}

// TODO: uncomment in #888
// // delete from permission graph
// if err := s.authz.DeleteObject(ctx, commonPerm.Organization(organizationID)); err != nil {
// return nil, fmt.Errorf("could not delete organization from spicedb: %w", err)
// }

return &pb.DeleteOrganizationResponse{}, nil
}

Expand Down

0 comments on commit 98310d3

Please sign in to comment.