From ba810a63e52177367bb13c205907926f90fbc872 Mon Sep 17 00:00:00 2001 From: Max Baumann Date: Sun, 3 Nov 2024 16:11:24 +0100 Subject: [PATCH] More Permissions (#880) --- libs/hwauthz/commonPerm/userOrg.go | 28 ++++ services/property-svc/cmd/service/main.go | 8 +- .../commands/v1/create_property_set.go | 14 +- .../property-set/handlers/handlers.go | 7 +- .../internal/property-set/perm/permission.go | 26 ++++ .../set_spicedb_projection.go | 86 ++++++++++++ .../queries/v1/get_property_set_by_id.go | 14 +- .../property/commands/v1/create_property.go | 5 +- .../property/commands/v1/update_property.go | 3 +- .../internal/property/perm/permission.go | 22 --- .../property_spicedb_projection.go} | 10 +- .../v1/get_properties_by_subject_type.go | 3 +- .../property/queries/v1/get_property_by_id.go | 3 +- .../is_property_always_included_for_view.go | 3 +- services/tasks-svc/cmd/service/main.go | 6 +- services/tasks-svc/internal/bed/bed.go | 108 ++++++++++++-- .../tasks-svc/internal/bed/perm/permission.go | 26 ++++ .../internal/room/perm/permission.go | 27 ++++ services/tasks-svc/internal/room/room.go | 117 +++++++++++++--- .../internal/ward/perm/permission.go | 29 ++++ services/tasks-svc/internal/ward/ward.go | 132 ++++++++++++++++-- .../tasks-svc/stories/PatientCRUD_test.go | 2 + services/tasks-svc/stories/WardCRUD_test.go | 17 ++- spicedb/bed.test.yaml | 49 +++++++ spicedb/bed.zed | 9 ++ spicedb/organization.test.yaml | 10 ++ spicedb/organization.zed | 6 + spicedb/property.test.yaml | 13 ++ spicedb/property.zed | 8 ++ spicedb/room.test.yaml | 51 +++++++ spicedb/room.zed | 12 ++ spicedb/ward.test.yaml | 41 ++++++ spicedb/ward.zed | 12 ++ 33 files changed, 821 insertions(+), 86 deletions(-) create mode 100644 libs/hwauthz/commonPerm/userOrg.go create mode 100644 services/property-svc/internal/property-set/perm/permission.go create mode 100644 services/property-svc/internal/property-set/projections/spiceDBProjection/set_spicedb_projection.go rename services/property-svc/internal/property/projections/{spicedb_projection/spicedb_projection.go => spiceDBProjection/property_spicedb_projection.go} (90%) create mode 100644 services/tasks-svc/internal/bed/perm/permission.go create mode 100644 services/tasks-svc/internal/room/perm/permission.go create mode 100644 services/tasks-svc/internal/ward/perm/permission.go create mode 100644 spicedb/bed.test.yaml create mode 100644 spicedb/bed.zed create mode 100644 spicedb/room.test.yaml create mode 100644 spicedb/room.zed create mode 100644 spicedb/ward.test.yaml create mode 100644 spicedb/ward.zed diff --git a/libs/hwauthz/commonPerm/userOrg.go b/libs/hwauthz/commonPerm/userOrg.go new file mode 100644 index 000000000..657480590 --- /dev/null +++ b/libs/hwauthz/commonPerm/userOrg.go @@ -0,0 +1,28 @@ +package commonPerm + +import ( + "common/auth" + "context" + + "github.com/google/uuid" +) + +type User uuid.UUID + +func (t User) Type() string { return "user" } +func (t User) ID() string { return uuid.UUID(t).String() } + +func UserFromCtx(ctx context.Context) User { + userID := auth.MustGetUserID(ctx) + return User(userID) +} + +type Organization uuid.UUID + +func (p Organization) Type() string { return "organization" } +func (p Organization) ID() string { return uuid.UUID(p).String() } + +func OrganizationFromCtx(ctx context.Context) Organization { + organizationID := auth.MustGetOrganizationID(ctx) + return Organization(organizationID) +} diff --git a/services/property-svc/cmd/service/main.go b/services/property-svc/cmd/service/main.go index cb51ccad6..870b17b6a 100644 --- a/services/property-svc/cmd/service/main.go +++ b/services/property-svc/cmd/service/main.go @@ -13,6 +13,7 @@ import ( hwspicedb "hwauthz/spicedb" propertySet "property-svc/internal/property-set/api" psh "property-svc/internal/property-set/handlers" + propertySetSpiceDBProjection "property-svc/internal/property-set/projections/spiceDBProjection" propertyValue "property-svc/internal/property-value/api" pvh "property-svc/internal/property-value/handlers" "property-svc/internal/property-value/projections/property_value_postgres_projection" @@ -22,7 +23,7 @@ import ( property "property-svc/internal/property/api" ph "property-svc/internal/property/handlers" propertyPostgresProjection "property-svc/internal/property/projections/postgres_projection" - propertySpicedbProjection "property-svc/internal/property/projections/spicedb_projection" + propertySpiceDBProjection "property-svc/internal/property/projections/spiceDBProjection" ) const ServiceName = "property-svc" @@ -53,14 +54,15 @@ func Main(version string, ready func()) { go projections.StartProjections( ctx, common.Shutdown, - propertySpicedbProjection.NewProjection(eventStore, ServiceName, authz), + propertySpiceDBProjection.NewProjection(eventStore, ServiceName, authz), + propertySetSpiceDBProjection.NewProjection(eventStore, ServiceName, authz), propertyPostgresProjection.NewProjection(eventStore, ServiceName, hwdb.GetDB()), property_value_postgres_projection.NewProjection(eventStore, ServiceName, hwdb.GetDB()), property_rules_postgres.NewProjection(eventStore, ServiceName), ) propertyHandlers := ph.NewPropertyHandlers(aggregateStore, authz) - propertySetHandlers := psh.NewPropertySetHandlers(aggregateStore) + propertySetHandlers := psh.NewPropertySetHandlers(aggregateStore, authz) propertyViewHandlers := pvih.NewPropertyViewHandlers(aggregateStore) propertyValueHandlers := pvh.NewPropertyValueHandlers(aggregateStore) diff --git a/services/property-svc/internal/property-set/commands/v1/create_property_set.go b/services/property-svc/internal/property-set/commands/v1/create_property_set.go index db1e3b3c0..a352475a0 100644 --- a/services/property-svc/internal/property-set/commands/v1/create_property_set.go +++ b/services/property-svc/internal/property-set/commands/v1/create_property_set.go @@ -5,8 +5,12 @@ import ( "context" "errors" "fmt" + "hwauthz" + "hwauthz/commonPerm" "hwes" + "property-svc/internal/property-set/perm" + "github.com/google/uuid" "property-svc/internal/property-set/aggregate" @@ -20,8 +24,16 @@ type CreatePropertySetCommandHandler func( name string, ) (common.ConsistencyToken, error) -func NewCreatePropertySetCommandHandler(as hwes.AggregateStore) CreatePropertySetCommandHandler { +func NewCreatePropertySetCommandHandler(as hwes.AggregateStore, authz hwauthz.AuthZ) CreatePropertySetCommandHandler { return func(ctx context.Context, propertySetID uuid.UUID, name string) (common.ConsistencyToken, error) { + user := commonPerm.UserFromCtx(ctx) + org := commonPerm.OrganizationFromCtx(ctx) + + check := hwauthz.NewPermissionCheck(user, perm.OrganizationCanUserCreatePropertySet, org) + if err := authz.Must(ctx, check); err != nil { + return 0, err + } + a := aggregate.NewPropertySetAggregate(propertySetID) exists, err := as.Exists(ctx, a) diff --git a/services/property-svc/internal/property-set/handlers/handlers.go b/services/property-svc/internal/property-set/handlers/handlers.go index e89f8a57a..c38650825 100644 --- a/services/property-svc/internal/property-set/handlers/handlers.go +++ b/services/property-svc/internal/property-set/handlers/handlers.go @@ -1,6 +1,7 @@ package handlers import ( + "hwauthz" "hwes" commandsV1 "property-svc/internal/property-set/commands/v1" @@ -20,16 +21,16 @@ type Handlers struct { Queries *Queries } -func NewPropertySetHandlers(as hwes.AggregateStore) *Handlers { +func NewPropertySetHandlers(as hwes.AggregateStore, authz hwauthz.AuthZ) *Handlers { return &Handlers{ Commands: &Commands{ V1: &commandsV1.PropertySetCommands{ - CreatePropertySet: commandsV1.NewCreatePropertySetCommandHandler(as), + CreatePropertySet: commandsV1.NewCreatePropertySetCommandHandler(as, authz), }, }, Queries: &Queries{ V1: &queriesV1.PropertySetQueries{ - GetPropertySetByID: queriesV1.NewGetPropertySetByIDQueryHandler(as), + GetPropertySetByID: queriesV1.NewGetPropertySetByIDQueryHandler(as, authz), }, }, } diff --git a/services/property-svc/internal/property-set/perm/permission.go b/services/property-svc/internal/property-set/perm/permission.go new file mode 100644 index 000000000..88f0240e3 --- /dev/null +++ b/services/property-svc/internal/property-set/perm/permission.go @@ -0,0 +1,26 @@ +package perm + +import ( + "hwauthz" + + "github.com/google/uuid" +) + +// Types + +type PropertySet uuid.UUID + +func (t PropertySet) Type() string { return "property_set" } +func (t PropertySet) ID() string { return uuid.UUID(t).String() } + +// Direct Relations + +const PropertySetOrganization hwauthz.Relation = "organization" + +// Permissions + +const OrganizationCanUserCreatePropertySet hwauthz.Permission = "create_property_set" + +const ( + PropertySetCanUserGet hwauthz.Permission = "get" +) diff --git a/services/property-svc/internal/property-set/projections/spiceDBProjection/set_spicedb_projection.go b/services/property-svc/internal/property-set/projections/spiceDBProjection/set_spicedb_projection.go new file mode 100644 index 000000000..876d3db42 --- /dev/null +++ b/services/property-svc/internal/property-set/projections/spiceDBProjection/set_spicedb_projection.go @@ -0,0 +1,86 @@ +package spiceDBProjection + +import ( + "context" + "errors" + "fmt" + "hwauthz" + "hwauthz/commonPerm" + "hwes" + "hwes/eventstoredb/projections/custom" + "hwutil" + + "property-svc/internal/property-set/aggregate" + + "github.com/EventStore/EventStore-Client-Go/v4/esdb" + "github.com/google/uuid" + zlog "github.com/rs/zerolog/log" + + propertySetEventsV1 "property-svc/internal/property-set/events/v1" + "property-svc/internal/property-set/perm" +) + +type Projection struct { + *custom.CustomProjection + authz hwauthz.AuthZ +} + +func NewProjection(es *esdb.Client, serviceName string, authz hwauthz.AuthZ) *Projection { + subscriptionGroupName := serviceName + "-property-set-spicedb-projection" + p := &Projection{ + CustomProjection: custom.NewCustomProjection( + es, + subscriptionGroupName, + &[]string{aggregate.PropertySetAggregateType + "-"}, + ), + authz: authz, + } + p.initEventListeners() + return p +} + +func (p *Projection) initEventListeners() { + p.RegisterEventListener(propertySetEventsV1.PropertySetCreated, p.onPropertySetCreated) +} + +func (p *Projection) onPropertySetCreated(ctx context.Context, evt hwes.Event) (error, *esdb.NackAction) { + log := zlog.Ctx(ctx) + + // Parse Values + var payload propertySetEventsV1.PropertySetCreatedEvent + if err := evt.GetJsonData(&payload); err != nil { + log.Error().Err(err).Msg("unmarshal failed") + return err, hwutil.PtrTo(esdb.NackActionPark) + } + + propertySetID, err := uuid.Parse(payload.ID) + if err != nil { + return err, hwutil.PtrTo(esdb.NackActionPark) + } + + if evt.OrganizationID == nil { + return errors.New("organization is missing from event"), hwutil.PtrTo(esdb.NackActionPark) + } + organizationID := *evt.OrganizationID + + relationship := hwauthz.NewRelationship( + commonPerm.Organization(organizationID), + perm.PropertySetOrganization, + perm.PropertySet(propertySetID), + ) + + // add to permission graph + _, err = p.authz. + Create(relationship). + Commit(ctx) + if err != nil { + return fmt.Errorf("could not create spice relationship %s: %w", relationship.DebugString(), err), + hwutil.PtrTo(esdb.NackActionRetry) + } + + log.Debug(). + Str("relationship", relationship.DebugString()). + Msg("spice relationship created") + + return nil, nil +} diff --git a/services/property-svc/internal/property-set/queries/v1/get_property_set_by_id.go b/services/property-svc/internal/property-set/queries/v1/get_property_set_by_id.go index 7297572bf..678d470bd 100644 --- a/services/property-svc/internal/property-set/queries/v1/get_property_set_by_id.go +++ b/services/property-svc/internal/property-set/queries/v1/get_property_set_by_id.go @@ -3,8 +3,12 @@ package v1 import ( "context" "fmt" + "hwauthz" + "hwauthz/commonPerm" "hwes" + "property-svc/internal/property-set/perm" + "github.com/google/uuid" "property-svc/internal/property-set/aggregate" @@ -13,8 +17,16 @@ import ( type GetPropertySetByIDQueryHandler func(ctx context.Context, propertySetID uuid.UUID) (*models.PropertySet, error) -func NewGetPropertySetByIDQueryHandler(as hwes.AggregateStore) GetPropertySetByIDQueryHandler { +func NewGetPropertySetByIDQueryHandler(as hwes.AggregateStore, authz hwauthz.AuthZ) GetPropertySetByIDQueryHandler { return func(ctx context.Context, propertySetID uuid.UUID) (*models.PropertySet, error) { + user := commonPerm.UserFromCtx(ctx) + set := perm.PropertySet(propertySetID) + check := hwauthz.NewPermissionCheck(user, perm.PropertySetCanUserGet, set) + + if err := authz.Must(ctx, check); err != nil { + return nil, err + } + propertySetAggregate, err := aggregate.LoadPropertySetAggregate(ctx, as, propertySetID) if err != nil { return nil, fmt.Errorf("GetPropertySetByIDQueryHandler: %w", err) diff --git a/services/property-svc/internal/property/commands/v1/create_property.go b/services/property-svc/internal/property/commands/v1/create_property.go index e0b531783..405b20804 100644 --- a/services/property-svc/internal/property/commands/v1/create_property.go +++ b/services/property-svc/internal/property/commands/v1/create_property.go @@ -6,6 +6,7 @@ import ( "errors" pb "gen/services/property_svc/v1" "hwauthz" + "hwauthz/commonPerm" "hwes" "github.com/google/uuid" @@ -35,8 +36,8 @@ func NewCreatePropertyCommandHandler(as hwes.AggregateStore, authz hwauthz.AuthZ setID *string, fieldTypeData *models.FieldTypeData, ) (version common.ConsistencyToken, err error) { - user := perm.UserFromCtx(ctx) - organization := perm.OrganizationFromCtx(ctx) + user := commonPerm.UserFromCtx(ctx) + organization := commonPerm.OrganizationFromCtx(ctx) check := hwauthz.NewPermissionCheck(user, perm.OrganizationCanUserCreateProperty, organization) if err = authz.Must(ctx, check); err != nil { diff --git a/services/property-svc/internal/property/commands/v1/update_property.go b/services/property-svc/internal/property/commands/v1/update_property.go index fa7d8ce27..6d19feef5 100644 --- a/services/property-svc/internal/property/commands/v1/update_property.go +++ b/services/property-svc/internal/property/commands/v1/update_property.go @@ -5,6 +5,7 @@ import ( "context" pb "gen/services/property_svc/v1" "hwauthz" + "hwauthz/commonPerm" "hwes" "github.com/google/uuid" @@ -40,7 +41,7 @@ func NewUpdatePropertyCommandHandler(as hwes.AggregateStore, authz hwauthz.AuthZ removeOptions []string, isArchived *bool, ) (common.ConsistencyToken, error) { - user := perm.UserFromCtx(ctx) + user := commonPerm.UserFromCtx(ctx) check := hwauthz.NewPermissionCheck(user, perm.PropertyCanUserUpdate, perm.Property(propertyID)) if err := authz.Must(ctx, check); err != nil { diff --git a/services/property-svc/internal/property/perm/permission.go b/services/property-svc/internal/property/perm/permission.go index 1e18cd142..726ce7af7 100644 --- a/services/property-svc/internal/property/perm/permission.go +++ b/services/property-svc/internal/property/perm/permission.go @@ -1,8 +1,6 @@ package perm import ( - "common/auth" - "context" "hwauthz" "github.com/google/uuid" @@ -15,26 +13,6 @@ type Property uuid.UUID func (t Property) Type() string { return "property" } func (t Property) ID() string { return uuid.UUID(t).String() } -type User uuid.UUID - -func (t User) Type() string { return "user" } -func (t User) ID() string { return uuid.UUID(t).String() } - -func UserFromCtx(ctx context.Context) User { - userID := auth.MustGetUserID(ctx) - return User(userID) -} - -type Organization uuid.UUID - -func (p Organization) Type() string { return "organization" } -func (p Organization) ID() string { return uuid.UUID(p).String() } - -func OrganizationFromCtx(ctx context.Context) Organization { - organizationID := auth.MustGetOrganizationID(ctx) - return Organization(organizationID) -} - // Direct Relations const PropertyOrganization hwauthz.Relation = "organization" diff --git a/services/property-svc/internal/property/projections/spicedb_projection/spicedb_projection.go b/services/property-svc/internal/property/projections/spiceDBProjection/property_spicedb_projection.go similarity index 90% rename from services/property-svc/internal/property/projections/spicedb_projection/spicedb_projection.go rename to services/property-svc/internal/property/projections/spiceDBProjection/property_spicedb_projection.go index 7361bd7ea..608fb57c6 100644 --- a/services/property-svc/internal/property/projections/spicedb_projection/spicedb_projection.go +++ b/services/property-svc/internal/property/projections/spiceDBProjection/property_spicedb_projection.go @@ -1,10 +1,11 @@ -package spicedb_projection +package spiceDBProjection import ( "context" "errors" "fmt" "hwauthz" + "hwauthz/commonPerm" "hwes" "hwes/eventstoredb/projections/custom" "hwutil" @@ -25,7 +26,7 @@ type Projection struct { } func NewProjection(es *esdb.Client, serviceName string, authz hwauthz.AuthZ) *Projection { - subscriptionGroupName := serviceName + "-spicedb-projection" + subscriptionGroupName := serviceName + "-property-spicedb-projection" p := &Projection{ CustomProjection: custom.NewCustomProjection( es, @@ -63,9 +64,10 @@ func (p *Projection) onPropertyCreated(ctx context.Context, evt hwes.Event) (err organizationID := *evt.OrganizationID relationship := hwauthz.NewRelationship( - perm.Organization(organizationID), + commonPerm.Organization(organizationID), perm.PropertyOrganization, - perm.Property(propertyID)) + perm.Property(propertyID), + ) // add to permission graph _, err = p.authz. diff --git a/services/property-svc/internal/property/queries/v1/get_properties_by_subject_type.go b/services/property-svc/internal/property/queries/v1/get_properties_by_subject_type.go index cdbade94f..8c6228a06 100644 --- a/services/property-svc/internal/property/queries/v1/get_properties_by_subject_type.go +++ b/services/property-svc/internal/property/queries/v1/get_properties_by_subject_type.go @@ -5,6 +5,7 @@ import ( "context" pb "gen/services/property_svc/v1" "hwauthz" + "hwauthz/commonPerm" "hwdb" "hwutil" @@ -22,7 +23,7 @@ type GetPropertiesQueryHandler func( func NewGetPropertiesQueryHandler(authz hwauthz.AuthZ) GetPropertiesQueryHandler { return func(ctx context.Context, subjectType *pb.SubjectType) ([]*models.PropertyWithConsistency, error) { - user := perm.UserFromCtx(ctx) + user := commonPerm.UserFromCtx(ctx) propertyRepo := property_repo.New(hwdb.GetDB()) diff --git a/services/property-svc/internal/property/queries/v1/get_property_by_id.go b/services/property-svc/internal/property/queries/v1/get_property_by_id.go index 444c43cf8..90aaaa644 100644 --- a/services/property-svc/internal/property/queries/v1/get_property_by_id.go +++ b/services/property-svc/internal/property/queries/v1/get_property_by_id.go @@ -6,6 +6,7 @@ import ( "fmt" pb "gen/services/property_svc/v1" "hwauthz" + "hwauthz/commonPerm" "hwdb" "github.com/google/uuid" @@ -22,7 +23,7 @@ type GetPropertyByIDQueryHandler func( func NewGetPropertyByIDQueryHandler(authz hwauthz.AuthZ) GetPropertyByIDQueryHandler { return func(ctx context.Context, propertyID uuid.UUID) (*models.Property, common.ConsistencyToken, error) { - user := perm.UserFromCtx(ctx) + user := commonPerm.UserFromCtx(ctx) // Verify user is allowed to see this property check := hwauthz.NewPermissionCheck(user, perm.PropertyCanUserGet, perm.Property(propertyID)) diff --git a/services/property-svc/internal/property/queries/v1/is_property_always_included_for_view.go b/services/property-svc/internal/property/queries/v1/is_property_always_included_for_view.go index 8fea2d54c..bd5a4feb2 100644 --- a/services/property-svc/internal/property/queries/v1/is_property_always_included_for_view.go +++ b/services/property-svc/internal/property/queries/v1/is_property_always_included_for_view.go @@ -4,6 +4,7 @@ import ( "context" pb "gen/services/property_svc/v1" "hwauthz" + "hwauthz/commonPerm" "hwutil" "github.com/google/uuid" @@ -32,7 +33,7 @@ func NewIsPropertyAlwaysIncludedForViewSourceHandler(authz hwauthz.AuthZ) IsProp subjectType pb.SubjectType, propertyID uuid.UUID, ) (bool, error) { - user := perm.UserFromCtx(ctx) + user := commonPerm.UserFromCtx(ctx) // Is user allowed to see this property? check := hwauthz.NewPermissionCheck(user, perm.PropertyCanUserGet, perm.Property(propertyID)) diff --git a/services/tasks-svc/cmd/service/main.go b/services/tasks-svc/cmd/service/main.go index 0f93fad58..84f6eefdb 100644 --- a/services/tasks-svc/cmd/service/main.go +++ b/services/tasks-svc/cmd/service/main.go @@ -57,9 +57,9 @@ func Main(version string, ready func()) { pb.RegisterTaskServiceServer(grpcServer, task.NewTaskGrpcService(aggregateStore, taskHandlers)) pb.RegisterPatientServiceServer(grpcServer, patient.NewPatientGrpcService(aggregateStore, patientHandlers)) - pb.RegisterBedServiceServer(grpcServer, bed.NewServiceServer()) - pb.RegisterRoomServiceServer(grpcServer, room.NewServiceServer()) - pb.RegisterWardServiceServer(grpcServer, ward.NewServiceServer()) + pb.RegisterBedServiceServer(grpcServer, bed.NewServiceServer(authz)) + pb.RegisterRoomServiceServer(grpcServer, room.NewServiceServer(authz)) + pb.RegisterWardServiceServer(grpcServer, ward.NewServiceServer(authz)) pb.RegisterTaskTemplateServiceServer(grpcServer, task_template.NewServiceServer()) if ready != nil { diff --git a/services/tasks-svc/internal/bed/bed.go b/services/tasks-svc/internal/bed/bed.go index c7f2c1e6f..b6211ff5e 100644 --- a/services/tasks-svc/internal/bed/bed.go +++ b/services/tasks-svc/internal/bed/bed.go @@ -4,10 +4,16 @@ import ( "common" "common/hwerr" "context" + "fmt" + "hwauthz" + "hwauthz/commonPerm" "hwdb" "hwlocale" "hwutil" + "tasks-svc/internal/bed/perm" + roomPerm "tasks-svc/internal/room/perm" + "github.com/jackc/pgx/v5/pgconn" "google.golang.org/genproto/googleapis/rpc/errdetails" @@ -24,21 +30,34 @@ import ( type ServiceServer struct { pb.UnimplementedBedServiceServer + authz hwauthz.AuthZ } -func NewServiceServer() *ServiceServer { - return &ServiceServer{} +func NewServiceServer(authz hwauthz.AuthZ) *ServiceServer { + return &ServiceServer{ + UnimplementedBedServiceServer: pb.UnimplementedBedServiceServer{}, + authz: authz, + } } -func (ServiceServer) CreateBed(ctx context.Context, req *pb.CreateBedRequest) (*pb.CreateBedResponse, error) { +func (s ServiceServer) CreateBed(ctx context.Context, req *pb.CreateBedRequest) (*pb.CreateBedResponse, error) { log := zlog.Ctx(ctx) bedRepo := bed_repo.New(hwdb.GetDB()) + // parse inputs roomId, err := uuid.Parse(req.GetRoomId()) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } + // check permissions + user := commonPerm.UserFromCtx(ctx) + check := hwauthz.NewPermissionCheck(user, roomPerm.RoomCanUserCreateBed, roomPerm.Room(roomId)) + if err := s.authz.Must(ctx, check); err != nil { + return nil, err + } + + // do query bed, err := bedRepo.CreateBed(ctx, bed_repo.CreateBedParams{ RoomID: roomId, Name: req.GetName(), @@ -68,20 +87,43 @@ func (ServiceServer) CreateBed(ctx context.Context, req *pb.CreateBedRequest) (* Str("name", bed.Name). Msg("bed created") + // update permission graph + relationship := hwauthz.NewRelationship( + roomPerm.Room(roomId), + perm.BedRoom, + perm.Bed(bed.ID), + ) + _, err = s.authz. + Create(relationship). + Commit(ctx) + if err != nil { + return nil, fmt.Errorf("could not create spice relationship %s: %w", relationship.DebugString(), err) + } + + // return return &pb.CreateBedResponse{ Id: bed.ID.String(), Consistency: common.ConsistencyToken(bed.Consistency).String(), //nolint:gosec }, nil } -func (ServiceServer) GetBed(ctx context.Context, req *pb.GetBedRequest) (*pb.GetBedResponse, error) { +func (s ServiceServer) GetBed(ctx context.Context, req *pb.GetBedRequest) (*pb.GetBedResponse, error) { bedRepo := bed_repo.New(hwdb.GetDB()) + // parse inputs id, 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.BedCanUserGet, perm.Bed(id)) + if err := s.authz.Must(ctx, check); err != nil { + return nil, err + } + + // do query bed, err := hwdb.Optional(bedRepo.GetBedById)(ctx, id) if bed == nil { return nil, status.Error(codes.InvalidArgument, "id not found") @@ -91,6 +133,7 @@ func (ServiceServer) GetBed(ctx context.Context, req *pb.GetBedRequest) (*pb.Get return nil, err } + // return return &pb.GetBedResponse{ Id: bed.ID.String(), RoomId: bed.RoomID.String(), @@ -137,7 +180,7 @@ func (ServiceServer) GetBedByPatient( }, nil } -func (ServiceServer) GetBeds(ctx context.Context, req *pb.GetBedsRequest) (*pb.GetBedsResponse, error) { +func (s ServiceServer) GetBeds(ctx context.Context, req *pb.GetBedsRequest) (*pb.GetBedsResponse, error) { roomID, err := hwutil.ParseNullUUID(req.RoomId) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) @@ -150,6 +193,19 @@ func (ServiceServer) GetBeds(ctx context.Context, req *pb.GetBedsRequest) (*pb.G return nil, hwdb.Error(ctx, err) } + // check permissions + user := commonPerm.UserFromCtx(ctx) + checks := hwutil.Map(beds, func(b bed_repo.Bed) hwauthz.PermissionCheck { + return hwauthz.NewPermissionCheck(user, perm.BedCanUserGet, perm.Bed(b.ID)) + }) + canGet, err := s.authz.BulkCheck(ctx, checks) + if err != nil { + return nil, err + } + beds = hwutil.Filter(beds, func(i int, _ bed_repo.Bed) bool { + return canGet[i] + }) + return &pb.GetBedsResponse{ Beds: hwutil.Map(beds, func(bed bed_repo.Bed) *pb.GetBedsResponse_Bed { return &pb.GetBedsResponse_Bed{ @@ -162,7 +218,7 @@ func (ServiceServer) GetBeds(ctx context.Context, req *pb.GetBedsRequest) (*pb.G }, nil } -func (ServiceServer) GetBedsByRoom( +func (s ServiceServer) GetBedsByRoom( ctx context.Context, req *pb.GetBedsByRoomRequest, ) (*pb.GetBedsByRoomResponse, error) { @@ -188,6 +244,19 @@ func (ServiceServer) GetBedsByRoom( Beds: []*pb.GetBedsByRoomResponse_Bed{}, } + // check permissions + user := commonPerm.UserFromCtx(ctx) + checks := hwutil.Map(beds, func(b bed_repo.Bed) hwauthz.PermissionCheck { + return hwauthz.NewPermissionCheck(user, perm.BedCanUserGet, perm.Bed(b.ID)) + }) + canGet, err := s.authz.BulkCheck(ctx, checks) + if err != nil { + return nil, err + } + beds = hwutil.Filter(beds, func(i int, _ bed_repo.Bed) bool { + return canGet[i] + }) + for _, bed := range beds { res.Beds = append(res.Beds, &pb.GetBedsByRoomResponse_Bed{ Id: bed.ID.String(), @@ -199,9 +268,10 @@ func (ServiceServer) GetBedsByRoom( return &res, nil } -func (ServiceServer) UpdateBed(ctx context.Context, req *pb.UpdateBedRequest) (*pb.UpdateBedResponse, error) { +func (s ServiceServer) UpdateBed(ctx context.Context, req *pb.UpdateBedRequest) (*pb.UpdateBedResponse, error) { bedRepo := bed_repo.New(hwdb.GetDB()) + // parse inputs bedID, err := uuid.Parse(req.GetId()) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) @@ -212,6 +282,14 @@ func (ServiceServer) UpdateBed(ctx context.Context, req *pb.UpdateBedRequest) (* return nil, status.Error(codes.InvalidArgument, err.Error()) } + // check permissions + user := commonPerm.UserFromCtx(ctx) + check := hwauthz.NewPermissionCheck(user, perm.BedCanUserUpdate, perm.Bed(bedID)) + if err := s.authz.Must(ctx, check); err != nil { + return nil, err + } + + // do query consistency, err := bedRepo.UpdateBed(ctx, bed_repo.UpdateBedParams{ ID: bedID, Name: req.Name, @@ -222,21 +300,31 @@ func (ServiceServer) UpdateBed(ctx context.Context, req *pb.UpdateBedRequest) (* return nil, err } + // return return &pb.UpdateBedResponse{ Conflict: nil, // TODO Consistency: common.ConsistencyToken(consistency).String(), //nolint:gosec }, nil } -func (ServiceServer) DeleteBed(ctx context.Context, req *pb.DeleteBedRequest) (*pb.DeleteBedResponse, error) { +func (s ServiceServer) DeleteBed(ctx context.Context, req *pb.DeleteBedRequest) (*pb.DeleteBedResponse, error) { log := zlog.Ctx(ctx) bedRepo := bed_repo.New(hwdb.GetDB()) + // parse inputs bedID, 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.BedCanUserDelete, perm.Bed(bedID)) + if err := s.authz.Must(ctx, check); err != nil { + return nil, err + } + + // query exists exists, err := bedRepo.ExistsBed(ctx, bedID) err = hwdb.Error(ctx, err) if err != nil { @@ -246,6 +334,7 @@ func (ServiceServer) DeleteBed(ctx context.Context, req *pb.DeleteBedRequest) (* return &pb.DeleteBedResponse{}, err } + // do delete err = bedRepo.DeleteBed(ctx, bedID) err = hwdb.Error(ctx, err) if err != nil { @@ -256,5 +345,8 @@ func (ServiceServer) DeleteBed(ctx context.Context, req *pb.DeleteBedRequest) (* Str("bedID", bedID.String()). Msg("bed deleted") + // todo: delete from permission graph + + // return return &pb.DeleteBedResponse{}, err } diff --git a/services/tasks-svc/internal/bed/perm/permission.go b/services/tasks-svc/internal/bed/perm/permission.go new file mode 100644 index 000000000..52cd61f07 --- /dev/null +++ b/services/tasks-svc/internal/bed/perm/permission.go @@ -0,0 +1,26 @@ +package perm + +import ( + "hwauthz" + + "github.com/google/uuid" +) + +// Types + +type Bed uuid.UUID + +func (t Bed) Type() string { return "bed" } +func (t Bed) ID() string { return uuid.UUID(t).String() } + +// Direct Relations + +const BedRoom hwauthz.Relation = "room" + +// Permissions + +const ( + BedCanUserGet hwauthz.Permission = "get" + BedCanUserUpdate hwauthz.Permission = "update" + BedCanUserDelete hwauthz.Permission = "delete" +) diff --git a/services/tasks-svc/internal/room/perm/permission.go b/services/tasks-svc/internal/room/perm/permission.go new file mode 100644 index 000000000..fa97e6ea6 --- /dev/null +++ b/services/tasks-svc/internal/room/perm/permission.go @@ -0,0 +1,27 @@ +package perm + +import ( + "hwauthz" + + "github.com/google/uuid" +) + +// Types + +type Room uuid.UUID + +func (t Room) Type() string { return "room" } +func (t Room) ID() string { return uuid.UUID(t).String() } + +// Direct Relations + +const RoomWard hwauthz.Relation = "ward" + +// Permissions + +const ( + RoomCanUserGet hwauthz.Permission = "get" + RoomCanUserUpdate hwauthz.Permission = "update" + RoomCanUserDelete hwauthz.Permission = "delete" + RoomCanUserCreateBed hwauthz.Permission = "create_bed" +) diff --git a/services/tasks-svc/internal/room/room.go b/services/tasks-svc/internal/room/room.go index 868f714b1..2d24eff3b 100644 --- a/services/tasks-svc/internal/room/room.go +++ b/services/tasks-svc/internal/room/room.go @@ -3,9 +3,15 @@ package room import ( "common" "context" + "fmt" + "hwauthz" + "hwauthz/commonPerm" "hwdb" "hwutil" + "tasks-svc/internal/room/perm" + wardPerm "tasks-svc/internal/ward/perm" + "github.com/google/uuid" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -18,23 +24,35 @@ import ( ) type ServiceServer struct { + authz hwauthz.AuthZ pb.UnimplementedRoomServiceServer } -func NewServiceServer() *ServiceServer { - return &ServiceServer{} +func NewServiceServer(authz hwauthz.AuthZ) *ServiceServer { + return &ServiceServer{ + authz: authz, + UnimplementedRoomServiceServer: pb.UnimplementedRoomServiceServer{}, + } } -func (ServiceServer) CreateRoom(ctx context.Context, req *pb.CreateRoomRequest) (*pb.CreateRoomResponse, error) { +func (s ServiceServer) CreateRoom(ctx context.Context, req *pb.CreateRoomRequest) (*pb.CreateRoomResponse, error) { log := zlog.Ctx(ctx) roomRepo := room_repo.New(hwdb.GetDB()) - // TODO: Auth + // parse input wardID, err := uuid.Parse(req.GetWardId()) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } + // check permission + user := commonPerm.UserFromCtx(ctx) + check := hwauthz.NewPermissionCheck(user, wardPerm.WardCanUserCreateRoom, wardPerm.Ward(wardID)) + if err := s.authz.Must(ctx, check); err != nil { + return nil, err + } + + // do query row, err := roomRepo.CreateRoom(ctx, room_repo.CreateRoomParams{ Name: req.GetName(), WardID: wardID, @@ -51,22 +69,43 @@ func (ServiceServer) CreateRoom(ctx context.Context, req *pb.CreateRoomRequest) Str("roomID", roomID.String()). Msg("room created") + // write relationship to permission graph + relationship := hwauthz.NewRelationship( + wardPerm.Ward(wardID), + perm.RoomWard, + perm.Room(roomID), + ) + _, err = s.authz. + Create(relationship). + Commit(ctx) + if err != nil { + return nil, fmt.Errorf("could not create spice relationship %s: %w", relationship.DebugString(), err) + } + + // return return &pb.CreateRoomResponse{ Id: roomID.String(), Consistency: common.ConsistencyToken(consistency).String(), //nolint:gosec }, nil } -func (ServiceServer) GetRoom(ctx context.Context, req *pb.GetRoomRequest) (*pb.GetRoomResponse, error) { +func (s ServiceServer) GetRoom(ctx context.Context, req *pb.GetRoomRequest) (*pb.GetRoomResponse, error) { roomRepo := room_repo.New(hwdb.GetDB()) - // TODO: Auth - + // parse inputs id, err := uuid.Parse(req.GetId()) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } + // check permission + user := commonPerm.UserFromCtx(ctx) + check := hwauthz.NewPermissionCheck(user, perm.RoomCanUserGet, perm.Room(id)) + if err := s.authz.Must(ctx, check); err != nil { + return nil, err + } + + // do query rows, err := roomRepo.GetRoomWithBedsById(ctx, id) err = hwdb.Error(ctx, err) if err != nil { @@ -101,18 +140,25 @@ func (ServiceServer) GetRoom(ctx context.Context, req *pb.GetRoomRequest) (*pb.G }, nil } -func (ServiceServer) UpdateRoom(ctx context.Context, req *pb.UpdateRoomRequest) (*pb.UpdateRoomResponse, error) { +func (s ServiceServer) UpdateRoom(ctx context.Context, req *pb.UpdateRoomRequest) (*pb.UpdateRoomResponse, error) { roomRepo := room_repo.New(hwdb.GetDB()) - // TODO: Auth - - patientID, err := uuid.Parse(req.GetId()) + // parse inputs + roomID, err := uuid.Parse(req.GetId()) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } + // check permission + user := commonPerm.UserFromCtx(ctx) + check := hwauthz.NewPermissionCheck(user, perm.RoomCanUserUpdate, perm.Room(roomID)) + if err := s.authz.Must(ctx, check); err != nil { + return nil, err + } + + // do query consistency, err := roomRepo.UpdateRoom(ctx, room_repo.UpdateRoomParams{ - ID: patientID, + ID: roomID, Name: req.Name, }) err = hwdb.Error(ctx, err) @@ -120,30 +166,33 @@ func (ServiceServer) UpdateRoom(ctx context.Context, req *pb.UpdateRoomRequest) return nil, err } + // return return &pb.UpdateRoomResponse{ Conflict: nil, // TODO Consistency: common.ConsistencyToken(consistency).String(), //nolint:gosec }, nil } -func (ServiceServer) GetRooms(ctx context.Context, req *pb.GetRoomsRequest) (*pb.GetRoomsResponse, error) { +func (s ServiceServer) GetRooms(ctx context.Context, req *pb.GetRoomsRequest) (*pb.GetRoomsResponse, error) { roomRepo := room_repo.New(hwdb.GetDB()) - // TODO: Auth + // parse inputs wardID, err := hwutil.ParseNullUUID(req.WardId) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } + // do query rows, err := roomRepo.GetRoomsWithBeds(ctx, wardID) err = hwdb.Error(ctx, err) if err != nil { return nil, err } + // re-structure rows processedRooms := make(map[uuid.UUID]bool) - roomsResponse := hwutil.FlatMap(rows, func(roomRow room_repo.GetRoomsWithBedsRow) **pb.GetRoomsResponse_Room { + rooms := hwutil.FlatMap(rows, func(roomRow room_repo.GetRoomsWithBedsRow) **pb.GetRoomsResponse_Room { room := roomRow.Room if _, processed := processedRooms[room.ID]; processed { return nil @@ -170,21 +219,42 @@ func (ServiceServer) GetRooms(ctx context.Context, req *pb.GetRoomsRequest) (*pb return &val }) + // check permissions + user := commonPerm.UserFromCtx(ctx) + checks := hwutil.Map(rooms, func(r *pb.GetRoomsResponse_Room) hwauthz.PermissionCheck { + return hwauthz.NewPermissionCheck(user, perm.RoomCanUserGet, perm.Room(uuid.MustParse(r.Id))) + }) + canGet, err := s.authz.BulkCheck(ctx, checks) + if err != nil { + return nil, err + } + rooms = hwutil.Filter(rooms, func(i int, _ *pb.GetRoomsResponse_Room) bool { + return canGet[i] + }) + + // return return &pb.GetRoomsResponse{ - Rooms: roomsResponse, + Rooms: rooms, }, nil } -func (ServiceServer) DeleteRoom(ctx context.Context, req *pb.DeleteRoomRequest) (*pb.DeleteRoomResponse, error) { +func (s ServiceServer) DeleteRoom(ctx context.Context, req *pb.DeleteRoomRequest) (*pb.DeleteRoomResponse, error) { roomRepo := room_repo.New(hwdb.GetDB()) - // TODO: Auth - + // parse inputs id, err := uuid.Parse(req.GetId()) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } + // check permission + user := commonPerm.UserFromCtx(ctx) + check := hwauthz.NewPermissionCheck(user, perm.RoomCanUserDelete, perm.Room(id)) + if err := s.authz.Must(ctx, check); err != nil { + return nil, err + } + + // do query err = roomRepo.DeleteRoom(ctx, id) err = hwdb.Error(ctx, err) if err != nil { @@ -193,10 +263,13 @@ func (ServiceServer) DeleteRoom(ctx context.Context, req *pb.DeleteRoomRequest) // TODO: Handle beds + // TODO: remove from spice (also beds) + + // return return &pb.DeleteRoomResponse{}, nil } -func (ServiceServer) GetRoomOverviewsByWard( +func (s ServiceServer) GetRoomOverviewsByWard( ctx context.Context, req *pb.GetRoomOverviewsByWardRequest, ) (*pb.GetRoomOverviewsByWardResponse, error) { @@ -223,7 +296,7 @@ func (ServiceServer) GetRoomOverviewsByWard( type rowType = room_repo.GetRoomsWithBedsAndPatientsAndTasksCountByWardRow - roomsResponse := hwutil.FlatMap(rows, + rooms := hwutil.FlatMap(rows, func(roomRow rowType) **pb.GetRoomOverviewsByWardResponse_Room { if _, roomProcessed := processedRooms[roomRow.RoomID]; roomProcessed { return nil @@ -267,6 +340,6 @@ func (ServiceServer) GetRoomOverviewsByWard( tracking.AddWardToRecentActivity(ctx, wardID.String()) return &pb.GetRoomOverviewsByWardResponse{ - Rooms: roomsResponse, + Rooms: rooms, }, nil } diff --git a/services/tasks-svc/internal/ward/perm/permission.go b/services/tasks-svc/internal/ward/perm/permission.go new file mode 100644 index 000000000..ad4ddded0 --- /dev/null +++ b/services/tasks-svc/internal/ward/perm/permission.go @@ -0,0 +1,29 @@ +package perm + +import ( + "hwauthz" + + "github.com/google/uuid" +) + +// Types + +type Ward uuid.UUID + +func (t Ward) Type() string { return "ward" } +func (t Ward) ID() string { return uuid.UUID(t).String() } + +// Direct Relations + +const WardOrganization hwauthz.Relation = "organization" + +// Permissions + +const OrganizationCanUserCreateWard hwauthz.Permission = "create_ward" + +const ( + WardCanUserGet hwauthz.Permission = "get" + WardCanUserUpdate hwauthz.Permission = "update" + WardCanUserDelete hwauthz.Permission = "delete" + WardCanUserCreateRoom hwauthz.Permission = "create_room" +) diff --git a/services/tasks-svc/internal/ward/ward.go b/services/tasks-svc/internal/ward/ward.go index 25207e60d..554d5bee3 100644 --- a/services/tasks-svc/internal/ward/ward.go +++ b/services/tasks-svc/internal/ward/ward.go @@ -3,10 +3,15 @@ package ward import ( "common" "context" + "fmt" pb "gen/services/tasks_svc/v1" + "hwauthz" + "hwauthz/commonPerm" "hwdb" "hwutil" + "tasks-svc/internal/ward/perm" + "github.com/google/uuid" zlog "github.com/rs/zerolog/log" "google.golang.org/grpc/codes" @@ -17,17 +22,31 @@ import ( ) type ServiceServer struct { + authz hwauthz.AuthZ pb.UnimplementedWardServiceServer } -func NewServiceServer() *ServiceServer { - return &ServiceServer{} +func NewServiceServer(authz hwauthz.AuthZ) *ServiceServer { + return &ServiceServer{ + authz: authz, + UnimplementedWardServiceServer: pb.UnimplementedWardServiceServer{}, + } } -func (ServiceServer) CreateWard(ctx context.Context, req *pb.CreateWardRequest) (*pb.CreateWardResponse, error) { +func (s ServiceServer) CreateWard(ctx context.Context, req *pb.CreateWardRequest) (*pb.CreateWardResponse, error) { log := zlog.Ctx(ctx) wardRepo := ward_repo.New(hwdb.GetDB()) + // check permissions + user := commonPerm.UserFromCtx(ctx) + organization := commonPerm.OrganizationFromCtx(ctx) + + check := hwauthz.NewPermissionCheck(user, perm.OrganizationCanUserCreateWard, organization) + if err := s.authz.Must(ctx, check); err != nil { + return nil, err + } + + // do query row, err := wardRepo.CreateWard(ctx, req.GetName()) err = hwdb.Error(ctx, err) if err != nil { @@ -41,22 +60,47 @@ func (ServiceServer) CreateWard(ctx context.Context, req *pb.CreateWardRequest) Str("wardID", wardID.String()). Msg("ward created") + // write relationship to permission graph + relationship := hwauthz.NewRelationship( + organization, + perm.WardOrganization, + perm.Ward(wardID), + ) + + _, err = s.authz. + Create(relationship). + Commit(ctx) + if err != nil { + return nil, fmt.Errorf("could not create spice relationship %s: %w", relationship.DebugString(), err) + } + + // add to "recently used" tracking.AddWardToRecentActivity(ctx, wardID.String()) + // return return &pb.CreateWardResponse{ Id: wardID.String(), Consistency: common.ConsistencyToken(consistency).String(), //nolint:gosec }, nil } -func (ServiceServer) GetWard(ctx context.Context, req *pb.GetWardRequest) (*pb.GetWardResponse, error) { +func (s ServiceServer) GetWard(ctx context.Context, req *pb.GetWardRequest) (*pb.GetWardResponse, error) { wardRepo := ward_repo.New(hwdb.GetDB()) + // parse input id, err := uuid.Parse(req.GetId()) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } + // check permission + user := commonPerm.UserFromCtx(ctx) + check := hwauthz.NewPermissionCheck(user, perm.WardCanUserGet, perm.Ward(id)) + if err := s.authz.Must(ctx, check); err != nil { + return nil, err + } + + // do query ward, err := hwdb.Optional(wardRepo.GetWardById)(ctx, id) if ward == nil { return nil, status.Error(codes.InvalidArgument, "id not found") @@ -66,6 +110,7 @@ func (ServiceServer) GetWard(ctx context.Context, req *pb.GetWardRequest) (*pb.G return nil, err } + // return return &pb.GetWardResponse{ Id: ward.ID.String(), Name: ward.Name, @@ -73,7 +118,7 @@ func (ServiceServer) GetWard(ctx context.Context, req *pb.GetWardRequest) (*pb.G }, nil } -func (ServiceServer) GetWards(ctx context.Context, req *pb.GetWardsRequest) (*pb.GetWardsResponse, error) { +func (s ServiceServer) GetWards(ctx context.Context, req *pb.GetWardsRequest) (*pb.GetWardsResponse, error) { wardRepo := ward_repo.New(hwdb.GetDB()) wards, err := wardRepo.GetWards(ctx) @@ -82,6 +127,19 @@ func (ServiceServer) GetWards(ctx context.Context, req *pb.GetWardsRequest) (*pb return nil, err } + user := commonPerm.UserFromCtx(ctx) + checks := hwutil.Map(wards, func(w ward_repo.Ward) hwauthz.PermissionCheck { + return hwauthz.NewPermissionCheck(user, perm.WardCanUserGet, perm.Ward(w.ID)) + }) + canGet, err := s.authz.BulkCheck(ctx, checks) + if err != nil { + return nil, err + } + + wards = hwutil.Filter(wards, func(i int, _ ward_repo.Ward) bool { + return canGet[i] + }) + return &pb.GetWardsResponse{ Wards: hwutil.Map(wards, func(ward ward_repo.Ward) *pb.GetWardsResponse_Ward { return &pb.GetWardsResponse_Ward{ @@ -93,15 +151,13 @@ func (ServiceServer) GetWards(ctx context.Context, req *pb.GetWardsRequest) (*pb }, nil } -func (ServiceServer) GetRecentWards( +func (s ServiceServer) GetRecentWards( ctx context.Context, _ *pb.GetRecentWardsRequest, ) (*pb.GetRecentWardsResponse, error) { wardRepo := ward_repo.New(hwdb.GetDB()) log := zlog.Ctx(ctx) - // TODO: Auth - recentWardIDsStr, err := tracking.GetRecentWardsForUser(ctx) if err != nil { return nil, status.Error(codes.Internal, err.Error()) @@ -117,6 +173,7 @@ func (ServiceServer) GetRecentWards( return &parsedUUID }) + // do query rows, err := wardRepo.GetWardsWithCounts(ctx, ward_repo.GetWardsWithCountsParams{ StatusTodo: int32(pb.TaskStatus_TASK_STATUS_TODO), StatusInProgress: int32(pb.TaskStatus_TASK_STATUS_IN_PROGRESS), @@ -128,6 +185,21 @@ func (ServiceServer) GetRecentWards( return nil, err } + // check permissions + user := commonPerm.UserFromCtx(ctx) + checks := hwutil.Map(rows, func(w ward_repo.GetWardsWithCountsRow) hwauthz.PermissionCheck { + return hwauthz.NewPermissionCheck(user, perm.WardCanUserGet, perm.Ward(w.Ward.ID)) + }) + canGet, err := s.authz.BulkCheck(ctx, checks) + if err != nil { + return nil, err + } + + rows = hwutil.Filter(rows, func(i int, _ ward_repo.GetWardsWithCountsRow) bool { + return canGet[i] + }) + + // return response := hwutil.Map(rows, func(row ward_repo.GetWardsWithCountsRow) *pb.GetRecentWardsResponse_Ward { return &pb.GetRecentWardsResponse_Ward{ Id: row.Ward.ID.String(), @@ -143,16 +215,23 @@ func (ServiceServer) GetRecentWards( return &pb.GetRecentWardsResponse{Wards: response}, nil } -func (ServiceServer) UpdateWard(ctx context.Context, req *pb.UpdateWardRequest) (*pb.UpdateWardResponse, error) { +func (s ServiceServer) UpdateWard(ctx context.Context, req *pb.UpdateWardRequest) (*pb.UpdateWardResponse, error) { wardRepo := ward_repo.New(hwdb.GetDB()) - // TODO: Auth - + // parse input id, 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.WardCanUserUpdate, perm.Ward(id)) + if err := s.authz.Must(ctx, check); err != nil { + return nil, err + } + + // do query consistency, err := wardRepo.UpdateWard(ctx, ward_repo.UpdateWardParams{ ID: id, Name: req.Name, @@ -162,22 +241,33 @@ func (ServiceServer) UpdateWard(ctx context.Context, req *pb.UpdateWardRequest) return nil, err } + // add to "recently used" tracking.AddWardToRecentActivity(ctx, id.String()) + // return return &pb.UpdateWardResponse{ Conflict: nil, // TODO Consistency: common.ConsistencyToken(consistency).String(), //nolint:gosec }, nil } -func (ServiceServer) DeleteWard(ctx context.Context, req *pb.DeleteWardRequest) (*pb.DeleteWardResponse, error) { +func (s ServiceServer) DeleteWard(ctx context.Context, req *pb.DeleteWardRequest) (*pb.DeleteWardResponse, error) { wardRepo := ward_repo.New(hwdb.GetDB()) + // parse input id, 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)) + if err := s.authz.Must(ctx, check); err != nil { + return nil, err + } + + // check if exists exists, err := wardRepo.ExistsWard(ctx, id) if !exists { return nil, nil @@ -187,14 +277,19 @@ func (ServiceServer) DeleteWard(ctx context.Context, req *pb.DeleteWardRequest) return nil, err } + // do query err = wardRepo.DeleteWard(ctx, id) err = hwdb.Error(ctx, err) if err != nil { return nil, err } + // TODO: remove from spice (also rooms and beds) + + // remove from "recently used" tracking.RemoveWardFromRecentActivity(ctx, id.String()) + // return return &pb.DeleteWardResponse{}, nil } @@ -215,6 +310,19 @@ func (s ServiceServer) GetWardOverviews( return nil, err } + user := commonPerm.UserFromCtx(ctx) + checks := hwutil.Map(rows, func(w ward_repo.GetWardsWithCountsRow) hwauthz.PermissionCheck { + return hwauthz.NewPermissionCheck(user, perm.WardCanUserGet, perm.Ward(w.Ward.ID)) + }) + canGet, err := s.authz.BulkCheck(ctx, checks) + if err != nil { + return nil, err + } + + rows = hwutil.Filter(rows, func(i int, _ ward_repo.GetWardsWithCountsRow) bool { + return canGet[i] + }) + resWards := hwutil.Map(rows, func(row ward_repo.GetWardsWithCountsRow) *pb.GetWardOverviewsResponse_Ward { return &pb.GetWardOverviewsResponse_Ward{ Id: row.Ward.ID.String(), diff --git a/services/tasks-svc/stories/PatientCRUD_test.go b/services/tasks-svc/stories/PatientCRUD_test.go index c4236958b..a20e7c1b9 100644 --- a/services/tasks-svc/stories/PatientCRUD_test.go +++ b/services/tasks-svc/stories/PatientCRUD_test.go @@ -76,6 +76,8 @@ func TestCreateUpdateGetPatient(t *testing.T) { require.NoError(t, err) assert.NotEqual(t, getPatientRes.Consistency, dischargeRes.Consistency) + hwtesting.WaitForProjectionsToSettle() + // // get discharged patient // diff --git a/services/tasks-svc/stories/WardCRUD_test.go b/services/tasks-svc/stories/WardCRUD_test.go index ed608bd20..a35e59b73 100644 --- a/services/tasks-svc/stories/WardCRUD_test.go +++ b/services/tasks-svc/stories/WardCRUD_test.go @@ -3,6 +3,9 @@ package stories import ( "context" pb "gen/services/tasks_svc/v1" + "hwauthz" + "hwauthz/commonPerm" + "hwauthz/spicedb" "hwtesting" "hwutil" "strconv" @@ -88,11 +91,23 @@ func prepareWards(t *testing.T, ctx context.Context, client pb.WardServiceClient } func TestGetRecentWards(t *testing.T) { + ctx := context.Background() userID := uuid.New() // new user for this test, to prevent interference with other tests + + // give user appropriate permissions + authz := spicedb.NewSpiceDBAuthZ() + _, err := authz.Create( + hwauthz.NewRelationship( + commonPerm.User(userID), + "member", + commonPerm.Organization(uuid.MustParse(hwtesting.FakeTokenOrganization)), + ), + ).Commit(ctx) + require.NoError(t, err) + wardClient := pb.NewWardServiceClient(hwtesting.GetGrpcConn(userID.String())) taskClient := pb.NewTaskServiceClient(hwtesting.GetGrpcConn(userID.String())) patientClient := pb.NewPatientServiceClient(hwtesting.GetGrpcConn(userID.String())) - ctx := context.Background() wardIds := prepareWards(t, ctx, wardClient, 11) consistencies := make(map[string]string) diff --git a/spicedb/bed.test.yaml b/spicedb/bed.test.yaml new file mode 100644 index 000000000..2f865da32 --- /dev/null +++ b/spicedb/bed.test.yaml @@ -0,0 +1,49 @@ + +relationships: + # Alice leads the A organization + - "organization:a#leader@user:alice" + # Bob leads the B organization + - "organization:b#leader@user:bob" + # Bob is also, explicitly, a member of the B organization + - "organization:b#member@user:bob" + # Charlie is a common member + - "organization:a#member@user:charlie" + - "organization:b#member@user:charlie" + # Clara is a common member, and leader of the A organization + - "organization:a#leader@user:clara" + - "organization:a#member@user:clara" + - "organization:b#member@user:clara" + # Oscar is only a member of one organization (A organization) + - "organization:a#member@user:oscar" + + # ward A belongs to org A, and B to B + - "ward:a#organization@organization:a" + - "ward:b#organization@organization:b" + + # room A belongs to ward A, and B to B + - "room:a#ward@ward:a" + - "room:b#ward@ward:b" + + # bed A belongs to room A, and B to B + - "bed:a#room@room:a" + - "bed:b#room@room:b" + +tests: + - name: all members can get any bed in their org (by transitivity) + check: + - "bed:a#get@user:alice" + - "bed:a#get@user:charlie" + - "bed:b#get@user:charlie" + - not: "bed:b#get@user:alice" + - name: all members can update any bed in their org (by transitivity) + check: + - "bed:a#update@user:alice" + - "bed:a#update@user:charlie" + - "bed:b#update@user:charlie" + - not: "bed:b#update@user:alice" + - name: all members can delete any bed in their org (by transitivity) + check: + - "bed:a#delete@user:alice" + - "bed:a#delete@user:charlie" + - "bed:b#delete@user:charlie" + - not: "bed:b#delete@user:alice" diff --git a/spicedb/bed.zed b/spicedb/bed.zed new file mode 100644 index 000000000..5a5c82d98 --- /dev/null +++ b/spicedb/bed.zed @@ -0,0 +1,9 @@ +definition bed { + // beds belong to a room + relation room: room; + + // those who can RUD the room, can RUD the bed + permission get = room->get; + permission update = room->update; + permission delete = room->delete; +} diff --git a/spicedb/organization.test.yaml b/spicedb/organization.test.yaml index 9cc545ca2..fd2cd0c34 100644 --- a/spicedb/organization.test.yaml +++ b/spicedb/organization.test.yaml @@ -66,3 +66,13 @@ tests: check: - "organization:a#create_property@user:alice" # alice is a leader - "organization:a#create_property@user:charlie" # charlie is a member + + - name: members can create new property_sets in their orgs + check: + - "organization:a#create_property_set@user:alice" # alice is a leader + - "organization:a#create_property_set@user:charlie" # charlie is a member + + - name: members can create new wards in their orgs + check: + - "organization:a#create_ward@user:alice" # alice is a leader + - "organization:a#create_ward@user:charlie" # charlie is a member diff --git a/spicedb/organization.zed b/spicedb/organization.zed index a22959073..61efffc03 100644 --- a/spicedb/organization.zed +++ b/spicedb/organization.zed @@ -13,6 +13,12 @@ definition organization { // all members can create new properties permission create_property = membership; + + // all members can create new property_sets + permission create_property_set = membership; + + // all members can create new wards + permission create_ward = membership; } definition organization_invite { diff --git a/spicedb/property.test.yaml b/spicedb/property.test.yaml index 265c18b77..8be288359 100644 --- a/spicedb/property.test.yaml +++ b/spicedb/property.test.yaml @@ -20,7 +20,12 @@ relationships: - "property:a#organization@organization:a" - "property:b#organization@organization:b" + # property_set S1 belongs to org A, and S2 to B + - "property_set:S1#organization@organization:a" + - "property_set:S2#organization@organization:b" + tests: + # properties - name: all members can get any prop in their org check: - "property:a#get@user:alice" @@ -33,3 +38,11 @@ tests: - "property:a#update@user:charlie" - "property:b#update@user:charlie" - not: "property:b#update@user:alice" + + # property_sets + - name: all members can get any set in their org + check: + - "property:a#get@user:alice" + - "property:a#get@user:charlie" + - "property:b#get@user:charlie" + - not: "property:b#get@user:alice" diff --git a/spicedb/property.zed b/spicedb/property.zed index 2ab1a46e2..39c9b1bac 100644 --- a/spicedb/property.zed +++ b/spicedb/property.zed @@ -6,3 +6,11 @@ definition property { permission get = organization->membership; permission update = organization->membership; } + +definition property_set { + // sets belong to an organization + relation organization: organization; + + // any member of the org can get the set + permission get = organization->membership; +} diff --git a/spicedb/room.test.yaml b/spicedb/room.test.yaml new file mode 100644 index 000000000..db331d147 --- /dev/null +++ b/spicedb/room.test.yaml @@ -0,0 +1,51 @@ + +relationships: + # Alice leads the A organization + - "organization:a#leader@user:alice" + # Bob leads the B organization + - "organization:b#leader@user:bob" + # Bob is also, explicitly, a member of the B organization + - "organization:b#member@user:bob" + # Charlie is a common member + - "organization:a#member@user:charlie" + - "organization:b#member@user:charlie" + # Clara is a common member, and leader of the A organization + - "organization:a#leader@user:clara" + - "organization:a#member@user:clara" + - "organization:b#member@user:clara" + # Oscar is only a member of one organization (A organization) + - "organization:a#member@user:oscar" + + # ward A belongs to org A, and B to B + - "ward:a#organization@organization:a" + - "ward:b#organization@organization:b" + + # room A belongs to ward A, and B to B + - "room:a#ward@ward:a" + - "room:b#ward@ward:b" + +tests: + - name: all members can get any room in their org (by transitivity) + check: + - "room:a#get@user:alice" + - "room:a#get@user:charlie" + - "room:b#get@user:charlie" + - not: "room:b#get@user:alice" + - name: all members can update any room in their org (by transitivity) + check: + - "room:a#update@user:alice" + - "room:a#update@user:charlie" + - "room:b#update@user:charlie" + - not: "room:b#update@user:alice" + - name: all members can delete any room in their org (by transitivity) + check: + - "room:a#delete@user:alice" + - "room:a#delete@user:charlie" + - "room:b#delete@user:charlie" + - not: "room:b#delete@user:alice" + - name: all members can create new beds in their org (by transitivity) + check: + - "room:a#create_bed@user:alice" + - "room:a#create_bed@user:charlie" + - "room:b#create_bed@user:charlie" + - not: "room:b#create_bed@user:alice" diff --git a/spicedb/room.zed b/spicedb/room.zed new file mode 100644 index 000000000..5890a0b07 --- /dev/null +++ b/spicedb/room.zed @@ -0,0 +1,12 @@ +definition room { + // room belong to a ward + relation ward: ward; + + // those who can RUD the ward, can RUD the room + permission get = ward->get; + permission update = ward->update; + permission delete = ward->delete; + + // those that can update the room, can create new beds in it + permission create_bed = update; +} diff --git a/spicedb/ward.test.yaml b/spicedb/ward.test.yaml new file mode 100644 index 000000000..01bca9fd4 --- /dev/null +++ b/spicedb/ward.test.yaml @@ -0,0 +1,41 @@ + +relationships: + # Alice leads the A organization + - "organization:a#leader@user:alice" + # Bob leads the B organization + - "organization:b#leader@user:bob" + # Bob is also, explicitly, a member of the B organization + - "organization:b#member@user:bob" + # Charlie is a common member + - "organization:a#member@user:charlie" + - "organization:b#member@user:charlie" + # Clara is a common member, and leader of the A organization + - "organization:a#leader@user:clara" + - "organization:a#member@user:clara" + - "organization:b#member@user:clara" + # Oscar is only a member of one organization (A organization) + - "organization:a#member@user:oscar" + + # ward A belongs to org A, and B to B + - "ward:a#organization@organization:a" + - "ward:b#organization@organization:b" + +tests: + - name: all members can get any ward in their org + check: + - "ward:a#get@user:alice" + - "ward:a#get@user:charlie" + - "ward:b#get@user:charlie" + - not: "ward:b#get@user:alice" + - name: all members can update any ward in their org + check: + - "ward:a#update@user:alice" + - "ward:a#update@user:charlie" + - "ward:b#update@user:charlie" + - not: "ward:b#update@user:alice" + - name: all members can delete any ward in their org + check: + - "ward:a#delete@user:alice" + - "ward:a#delete@user:charlie" + - "ward:b#delete@user:charlie" + - not: "ward:b#delete@user:alice" diff --git a/spicedb/ward.zed b/spicedb/ward.zed new file mode 100644 index 000000000..057057117 --- /dev/null +++ b/spicedb/ward.zed @@ -0,0 +1,12 @@ +definition ward { + // wards belong to an organization + relation organization: organization; + + // any member of the org can RUD the ward + permission get = organization->membership; + permission update = organization->membership; + permission delete = organization->membership; + + // any member of the org can create a new room + permission create_room = organization->membership; +}