diff --git a/libs/hwauthz/authz.go b/libs/hwauthz/authz.go index a7d1c41b3..4e4ebed21 100644 --- a/libs/hwauthz/authz.go +++ b/libs/hwauthz/authz.go @@ -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 @@ -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 diff --git a/libs/hwauthz/spicedb/spicedb.go b/libs/hwauthz/spicedb/spicedb.go index ab92ba0a4..20ce1b258 100644 --- a/libs/hwauthz/spicedb/spicedb.go +++ b/libs/hwauthz/spicedb/spicedb.go @@ -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 +} diff --git a/libs/hwauthz/spicedb/spicedb_test.go b/libs/hwauthz/spicedb/spicedb_test.go index 1904ab626..1715fd6a9 100644 --- a/libs/hwauthz/spicedb/spicedb_test.go +++ b/libs/hwauthz/spicedb/spicedb_test.go @@ -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) +} diff --git a/libs/hwauthz/test/true_authz.go b/libs/hwauthz/test/true_authz.go index 6b40e16e9..841607b0e 100644 --- a/libs/hwauthz/test/true_authz.go +++ b/libs/hwauthz/test/true_authz.go @@ -54,3 +54,7 @@ func (s *TrueAuthZ) LookupResources( ) ([]string, error) { return []string{}, nil } + +func (s *TrueAuthZ) DeleteObject(_ context.Context, _ hwauthz.Object) error { + return nil +} diff --git a/services/tasks-svc/internal/bed/bed.go b/services/tasks-svc/internal/bed/bed.go index 711d00e17..9151329e7 100644 --- a/services/tasks-svc/internal/bed/bed.go +++ b/services/tasks-svc/internal/bed/bed.go @@ -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{ diff --git a/services/tasks-svc/internal/patient/projections/patientSpiceDBProjection/patient_spicedb_projection.go b/services/tasks-svc/internal/patient/projections/patientSpiceDBProjection/patient_spicedb_projection.go index a345ae48b..0423fa221 100644 --- a/services/tasks-svc/internal/patient/projections/patientSpiceDBProjection/patient_spicedb_projection.go +++ b/services/tasks-svc/internal/patient/projections/patientSpiceDBProjection/patient_spicedb_projection.go @@ -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" ) @@ -41,34 +38,20 @@ 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) @@ -76,3 +59,10 @@ func (p *Projection) onPatientCreated(ctx context.Context, evt hwes.Event) (erro 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 +} diff --git a/services/tasks-svc/internal/room/room.go b/services/tasks-svc/internal/room/room.go index 9d78fdd86..1bee3cd98 100644 --- a/services/tasks-svc/internal/room/room.go +++ b/services/tasks-svc/internal/room/room.go @@ -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), diff --git a/services/tasks-svc/internal/task-template/task_template.go b/services/tasks-svc/internal/task-template/task_template.go index 2e49416d3..d84473612 100644 --- a/services/tasks-svc/internal/task-template/task_template.go +++ b/services/tasks-svc/internal/task-template/task_template.go @@ -4,6 +4,7 @@ import ( "common" "common/auth" "context" + "fmt" pbEventsV1 "gen/libs/events/v1" "hwauthz" "hwauthz/commonPerm" @@ -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") diff --git a/services/tasks-svc/internal/task/projections/task_spicedb/spicedb.go b/services/tasks-svc/internal/task/projections/task_spicedb/spicedb.go index 9a4f06d99..8e46f39e2 100644 --- a/services/tasks-svc/internal/task/projections/task_spicedb/spicedb.go +++ b/services/tasks-svc/internal/task/projections/task_spicedb/spicedb.go @@ -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) { @@ -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 +} diff --git a/services/tasks-svc/internal/ward/ward.go b/services/tasks-svc/internal/ward/ward.go index 110155f1a..f766cb85b 100644 --- a/services/tasks-svc/internal/ward/ward.go +++ b/services/tasks-svc/internal/ward/ward.go @@ -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 } @@ -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 diff --git a/services/user-svc/internal/organization/organization.go b/services/user-svc/internal/organization/organization.go index 6ab7cb7a0..3dc486abf 100644 --- a/services/user-svc/internal/organization/organization.go +++ b/services/user-svc/internal/organization/organization.go @@ -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 }