diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
index e165f4fb..7815d732 100644
--- a/.github/workflows/tests.yaml
+++ b/.github/workflows/tests.yaml
@@ -19,6 +19,32 @@ jobs:
image: valkey/valkey:7.2
ports:
- "6379:6379"
+ postgres:
+ # Docker Hub image
+ image: postgres
+ # Provide the password for postgres
+ env:
+ POSTGRES_USER: postgres
+ POSTGRES_PASSWORD: postgres
+ ports:
+ - "5432:5432"
+ # Set health checks to wait until postgres has started
+ options: >-
+ --health-cmd pg_isready
+ --health-interval 10s
+ --health-timeout 5s
+ --health-retries 5
+ openfga:
+ image: openfga/openfga:latest
+ env:
+ OPENFGA_DATASTORE_ENGINE: postgres
+ OPENFGA_DATASTORE_URI: postgres://postgres:password@postgres:5432/postgres?sslmode=disable
+ OPENFGA_LOG_FORMAT: json
+ OPENFGA_AUTHN_METHOD: preshared
+ OPENFGA_AUTHN_PRESHARED_KEYS: test_token
+ ports:
+ - "8080:8080"
+ - "8081:8081"
steps:
- uses: actions/checkout@v5
- name: Set up Go
@@ -43,4 +69,8 @@ jobs:
DB_PORT: 3306
DB_SCHEMA: internal/database/mariadb/init/schema.sql
LOCAL_TEST_DB: true
+ AUTHZ_FGA_API_URL: http://localhost:8080
+ AUTHZ_FGA_API_TOKEN: test_token
+ AUTHZ_FGA_STORE_NAME: heureka_store
+ AUTHZ_MODEL_FILE_PATH: ./internal/openfga/model/model.fga
run: ginkgo -r -randomize-all -randomize-suites
diff --git a/.gitignore b/.gitignore
index 5097e78b..061ea2b0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -34,3 +34,5 @@ internal/mocks/*.go
internal/api/graphql/graph/model/models_gen.go
internal/api/graphql/graph/generated.go
+.vscode/launch.json
+.vscode/launch.json
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 46949909..12ab9e9c 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -4,6 +4,25 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
+ {
+ "name": "Debug component_handler_test.go",
+ "type": "go",
+ "request": "launch",
+ "mode": "test",
+ "cwd": "${workspaceFolder}",
+ "program": "${workspaceFolder}/internal/app/component/component_handler_test.go",
+ "envFile": "${workspaceFolder}/.env",
+ "args": []
+ },
+ {
+ "name": "Debug All Tests (ginkgo -r)",
+ "type": "go",
+ "request": "launch",
+ "mode": "test",
+ "program": "${workspaceFolder}",
+ "args": ["-ginkgo.v", "-ginkgo.r"], // -ginkgo.r for recursive, -ginkgo.v for verbose
+ "env": {}
+ },
{
"name": "Launch K8sScanner",
"type": "go",
diff --git a/.vscode/settings.json b/.vscode/settings.json
index d37dd768..c1b32ef8 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,4 +1,7 @@
{
"go.testEnvFile": "${workspaceFolder}/.testenv-devcontainer",
- "ginkgotools.ginkgoPath": "go run github.com/onsi/ginkgo/v2/ginkgo"
+ "ginkgotools.ginkgoPath": "go run github.com/onsi/ginkgo/v2/ginkgo",
+ "go.delveConfig": {
+ "showGlobalVariables": true,
+ },
}
diff --git a/docs/openfga_auth.md b/docs/openfga_auth.md
index 4777dd96..09226d6f 100644
--- a/docs/openfga_auth.md
+++ b/docs/openfga_auth.md
@@ -24,15 +24,50 @@ The interface consists of four main functions
- Checks if a given user has a given level of permission on a given resource (based on relation between user and resource)
- AddRelation(r RelationInput)
- Adds a specified relation between a given user and a given resource
+- RemoveRelationBulk (r []RelationInput)
+ - Remove all relations that match any given RelationInput as filters
- RemoveRelation(r RelationInput)
- - Removes a specified relation between a given user and a given resource (if such a relation exists)
+ - Removes a single relation between a given user and a given resource (if such a relation exists)
+- ListRelations(filters []RelationInput)
+ - Returns a list of relations that match any given RelationInput as filters
- ListAccessibleResources(p PermissionInput)
- Returns a list of all objects of a specified type that a given user has a given relation with
+- GetCurrentUser()
+ - Placeholder function to be implemented for future user context functionality
PermissionInput and RelationInput are structs defined in the interface that contain all the parameters for the above functions.
For more info on how OpenFGA handles users, objects, and relations: https://openfga.dev/docs/concepts
+## Handlers
+
+Using the event handling system, openfga tuples are modified based on the event handled. Based on the [Auth Model](https://github.com/cloudoperators/heureka/blob/main/internal/openfga/model/model.fga) defined, the OpenFGA tuples are implemented as follows here.
+
+| Event | Event Handler | Tuples Implemented |
+|-------------------------|-------------------------------|--------------------------------------------------------------------------------------------------|
+| AddOwnerToService | OnAddOwnerToService | add service - user |
+| RemoveOwnerFromService | OnRemoveOwnerFromService | remove service - user |
+| CreateService | OnServiceCreateAuthz | add role - service |
+| DeleteService | OnServiceDeleteAuthz | remove user - service
remove role - service
remove support_group - service
remove service - component_instance |
+| UpdateComponentVersion | OnComponentVersionUpdateAuthz | update component_version - component |
+| CreateComponentVersion | OnComponentVersionCreateAuthz | add role - component_version |
+| DeleteComponentVersion | OnComponentVersionDeleteAuthz | remove user - component_version
remove component_instance - component_version
remove role - component_version
remove component - component_version |
+| UpdateIssueMatch | OnIssueMatchUpdateAuthz | update issue_match - component_instance |
+| CreateIssueMatch | OnIssueMatchCreateAuthz | add role - issue_match |
+| DeleteIssueMatch | OnIssueMatchDeleteAuthz | delete user - issue_match
delete issue_match - component_instance
delete role - issue_match |
+| UpdateComponentInstance | OnComponentInstanceUpdateAuthz | update component_instance - component_version_id
update component_instance - service |
+| CreateComponentInstance | OnComponentInstanceCreateAuthz | add service - component_instance
add role - component_instance
add component_instance - component_version |
+| DeleteComponentInstance | OnComponentInstanceDeleteAuthz | delete user - component_instance
delete service - component_instance
delete role - component_instance
delete component_instance - component_version
delete component_instance - issue_match |
+| DeleteSupportGroup | OnSupportGroupDeleteAuthz | delete user - support_group
delete support_group - support_group
delete role - support_group
delete support_group - service |
+| CreateSupportGroup | OnSupportGroupCreateAuthz | add role - support_group |
+| AddServicetoSupportGroup| OnAddServiceToSupportGroup | add support_group - service |
+| RemoveServiceFromSupportGroup | OnRemoveServiceFromSupportGroup | remove support_group - service |
+| AddUsertoSupportGroup | OnAddUserToSupportGroup | add support_group - user |
+| RemoveUserFromSupportGroup | OnRemoveUserFromSupportGroup | remove support_group - user |
+| DeleteUser | OnUserDeleteAuthz | delete user - role
delete user - service
delete user - component_instance
delete user - support_group
delete user - issue_match
delete user - component_version
delete user - component |
+| CreateComponent | OnComponentCreateAuthz | add role - component |
+| DeleteComponent | OnComponentDeleteAuthz | delete user - component
delete component_version - component
delete role - component |
+
## Usage
The following four environment variables must be set to use OpenFGA
diff --git a/go.mod b/go.mod
index ec1edb84..f8808e3c 100644
--- a/go.mod
+++ b/go.mod
@@ -31,7 +31,8 @@ require (
github.com/sirupsen/logrus v1.9.3
github.com/square/go-jose/v3 v3.0.0-20200630053402-0a67ce9b0693
github.com/stretchr/testify v1.11.1
- github.com/valkey-io/valkey-go v1.0.67
+ github.com/valkey-io/valkey-go v1.0.66
+ github.com/vektah/gqlparser/v2 v2.5.30
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394
golang.org/x/text v0.30.0
golang.org/x/time v0.14.0
@@ -43,7 +44,6 @@ require (
github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // indirect
github.com/openfga/api/proto v0.0.0-20240905181937-3583905f61a6 // indirect
- github.com/vektah/gqlparser/v2 v2.5.30 // indirect
go.uber.org/multierr v1.11.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect
diff --git a/go.sum b/go.sum
index 26262834..8cc021be 100644
--- a/go.sum
+++ b/go.sum
@@ -289,8 +289,8 @@ github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU=
github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4=
-github.com/valkey-io/valkey-go v1.0.67 h1:QPaRcuBmazhyoWTxk7I2XcSALhoL7UhAReR5o/rh1Po=
-github.com/valkey-io/valkey-go v1.0.67/go.mod h1:bHmwjIEOrGq/ubOJfh5uMRs7Xj6mV3mQ/ZXUbmqpjqY=
+github.com/valkey-io/valkey-go v1.0.66 h1:DIEF1XpwbO78xK2sMTghYE3Bz6pePWJTNxKtgoAuA3A=
+github.com/valkey-io/valkey-go v1.0.66/go.mod h1:bHmwjIEOrGq/ubOJfh5uMRs7Xj6mV3mQ/ZXUbmqpjqY=
github.com/vektah/gqlparser/v2 v2.5.30 h1:EqLwGAFLIzt1wpx1IPpY67DwUujF1OfzgEyDsLrN6kE=
github.com/vektah/gqlparser/v2 v2.5.30/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
diff --git a/internal/app/activity/activity_handler_test.go b/internal/app/activity/activity_handler_test.go
index bc271f21..eb1635f9 100644
--- a/internal/app/activity/activity_handler_test.go
+++ b/internal/app/activity/activity_handler_test.go
@@ -32,7 +32,7 @@ var authz openfga.Authorization
var _ = BeforeSuite(func() {
db := mocks.NewMockDatabase(GinkgoT())
- er = event.NewEventRegistry(db)
+ er = event.NewEventRegistry(db, authz)
})
func activityFilter() *entity.ActivityFilter {
diff --git a/internal/app/common/test_config_setup.go b/internal/app/common/test_config_setup.go
new file mode 100644
index 00000000..d3acc5ef
--- /dev/null
+++ b/internal/app/common/test_config_setup.go
@@ -0,0 +1,19 @@
+package common
+
+import (
+ "os"
+
+ "github.com/cloudoperators/heureka/internal/util"
+)
+
+func GetTestConfig() *util.Config {
+ var modelFilePath = "./../../openfga/model/model.fga"
+
+ return &util.Config{
+ AuthzOpenFgaApiUrl: os.Getenv("AUTHZ_FGA_API_URL"),
+ AuthzOpenFgaApiToken: os.Getenv("AUTHZ_FGA_API_TOKEN"),
+ AuthzOpenFgaStoreName: os.Getenv("AUTHZ_FGA_STORE_NAME"),
+ AuthzModelFilePath: modelFilePath,
+ CurrentUser: "testuser",
+ }
+}
diff --git a/internal/app/component/component_handler.go b/internal/app/component/component_handler.go
index b9f80e9d..959871ff 100644
--- a/internal/app/component/component_handler.go
+++ b/internal/app/component/component_handler.go
@@ -41,10 +41,10 @@ type ComponentHandlerError struct {
}
func (e *ComponentHandlerError) Error() string {
- return fmt.Sprintf("ServiceHandlerError: %s", e.msg)
+ return fmt.Sprintf("ComponentHandlerError: %s", e.msg)
}
-func NewUserHandlerError(msg string) *ComponentHandlerError {
+func NewComponentHandlerError(msg string) *ComponentHandlerError {
return &ComponentHandlerError{msg: msg}
}
@@ -63,7 +63,7 @@ func (cs *componentHandler) ListComponents(filter *entity.ComponentFilter, optio
if err != nil {
l.Error(err)
- return nil, NewUserHandlerError("Error while filtering for Components")
+ return nil, NewComponentHandlerError("Error while filtering for Components")
}
if options.ShowPageInfo {
@@ -78,7 +78,7 @@ func (cs *componentHandler) ListComponents(filter *entity.ComponentFilter, optio
)
if err != nil {
l.Error(err)
- return nil, NewUserHandlerError("Error while getting all Ids")
+ return nil, NewComponentHandlerError("Error while getting all Ids")
}
pageInfo = common.GetPageInfoX(res, cursors, *filter.First, filter.After)
count = int64(len(cursors))
@@ -93,7 +93,7 @@ func (cs *componentHandler) ListComponents(filter *entity.ComponentFilter, optio
)
if err != nil {
l.Error(err)
- return nil, NewUserHandlerError("Error while total count of Components")
+ return nil, NewComponentHandlerError("Error while total count of Components")
}
}
@@ -123,7 +123,7 @@ func (cs *componentHandler) CreateComponent(component *entity.Component) (*entit
component.CreatedBy, err = common.GetCurrentUserId(cs.database)
if err != nil {
l.Error(err)
- return nil, NewUserHandlerError("Internal error while creating component (GetUserId).")
+ return nil, NewComponentHandlerError("Internal error while creating component (GetUserId).")
}
component.UpdatedBy = component.CreatedBy
@@ -132,18 +132,18 @@ func (cs *componentHandler) CreateComponent(component *entity.Component) (*entit
if err != nil {
l.Error(err)
- return nil, NewUserHandlerError("Internal error while creating component.")
+ return nil, NewComponentHandlerError("Internal error while creating component.")
}
if len(components.Elements) > 0 {
- return nil, NewUserHandlerError(fmt.Sprintf("Duplicated entry %s for ccrn.", component.CCRN))
+ return nil, NewComponentHandlerError(fmt.Sprintf("Duplicated entry %s for ccrn.", component.CCRN))
}
newComponent, err := cs.database.CreateComponent(component)
if err != nil {
l.Error(err)
- return nil, NewUserHandlerError("Internal error while creating component.")
+ return nil, NewComponentHandlerError("Internal error while creating component.")
}
cs.eventRegistry.PushEvent(&CreateComponentEvent{Component: newComponent})
@@ -161,14 +161,14 @@ func (cs *componentHandler) UpdateComponent(component *entity.Component) (*entit
component.UpdatedBy, err = common.GetCurrentUserId(cs.database)
if err != nil {
l.Error(err)
- return nil, NewUserHandlerError("Internal error while updating component (GetUserId).")
+ return nil, NewComponentHandlerError("Internal error while updating component (GetUserId).")
}
err = cs.database.UpdateComponent(component)
if err != nil {
l.Error(err)
- return nil, NewUserHandlerError("Internal error while updating component.")
+ return nil, NewComponentHandlerError("Internal error while updating component.")
}
lo := entity.NewListOptions()
@@ -176,12 +176,12 @@ func (cs *componentHandler) UpdateComponent(component *entity.Component) (*entit
if err != nil {
l.Error(err)
- return nil, NewUserHandlerError("Internal error while retrieving updated component.")
+ return nil, NewComponentHandlerError("Internal error while retrieving updated component.")
}
if len(componentResult.Elements) != 1 {
l.Error(err)
- return nil, NewUserHandlerError("Multiple components found.")
+ return nil, NewComponentHandlerError("Multiple components found.")
}
cs.eventRegistry.PushEvent(&UpdateComponentEvent{Component: component})
@@ -198,14 +198,14 @@ func (cs *componentHandler) DeleteComponent(id int64) error {
userId, err := common.GetCurrentUserId(cs.database)
if err != nil {
l.Error(err)
- return NewUserHandlerError("Internal error while deleting component (GetUserId).")
+ return NewComponentHandlerError("Internal error while deleting component (GetUserId).")
}
err = cs.database.DeleteComponent(id, userId)
if err != nil {
l.Error(err)
- return NewUserHandlerError("Internal error while deleting component.")
+ return NewComponentHandlerError("Internal error while deleting component.")
}
cs.eventRegistry.PushEvent(&DeleteComponentEvent{ComponentID: id})
@@ -229,7 +229,7 @@ func (cs *componentHandler) ListComponentCcrns(filter *entity.ComponentFilter, o
if err != nil {
l.Error(err)
- return nil, NewUserHandlerError("Internal error while retrieving componentCcrns.")
+ return nil, NewComponentHandlerError("Internal error while retrieving componentCcrns.")
}
cs.eventRegistry.PushEvent(&ListComponentCcrnsEvent{Filter: filter, Options: options, CCRNs: componentCcrns})
@@ -246,7 +246,7 @@ func (cs *componentHandler) GetComponentVulnerabilityCounts(filter *entity.Compo
counts, err := cs.database.CountComponentVulnerabilities(filter)
if err != nil {
l.Error(err)
- return nil, NewUserHandlerError("Internal error while retrieving issue severity counts.")
+ return nil, NewComponentHandlerError("Internal error while retrieving issue severity counts.")
}
cs.eventRegistry.PushEvent(&GetComponentIssueSeverityCountsEvent{Filter: filter, Counts: counts})
diff --git a/internal/app/component/component_handler_events.go b/internal/app/component/component_handler_events.go
index ede55410..927ab464 100644
--- a/internal/app/component/component_handler_events.go
+++ b/internal/app/component/component_handler_events.go
@@ -5,7 +5,11 @@ package component
import (
"github.com/cloudoperators/heureka/internal/app/event"
+ "github.com/cloudoperators/heureka/internal/database"
"github.com/cloudoperators/heureka/internal/entity"
+ appErrors "github.com/cloudoperators/heureka/internal/errors"
+ "github.com/cloudoperators/heureka/internal/openfga"
+ "github.com/sirupsen/logrus"
)
const (
@@ -69,3 +73,68 @@ type GetComponentIssueSeverityCountsEvent struct {
func (e *GetComponentIssueSeverityCountsEvent) Name() event.EventName {
return GetComponentIssueSeverityCountsEventName
}
+
+// OnComponentCreateAuthz is a handler for the CreateComponentEvent
+// It creates an OpenFGA relation tuple for the component and the current user
+func OnComponentCreateAuthz(db database.Database, e event.Event, authz openfga.Authorization) {
+ op := appErrors.Op("OnComponentCreateAuthz")
+
+ l := logrus.WithFields(logrus.Fields{
+ "event": "OnComponentCreateAuthz",
+ "payload": e,
+ })
+
+ if createEvent, ok := e.(*CreateComponentEvent); ok {
+ userId := openfga.UserIdFromInt(createEvent.Component.CreatedBy)
+
+ relations := []openfga.RelationInput{
+ {
+ UserType: openfga.TypeRole,
+ UserId: userId,
+ Relation: openfga.RelRole,
+ ObjectType: openfga.TypeComponent,
+ ObjectId: openfga.ObjectIdFromInt(createEvent.Component.Id),
+ },
+ }
+
+ err := authz.AddRelationBulk(relations)
+ if err != nil {
+ wrappedErr := appErrors.InternalError(string(op), "Component", "", err)
+ l.Error(wrappedErr)
+ }
+ } else {
+ err := NewComponentHandlerError("OnComponentCreateAuthz: triggered with wrong event type")
+ wrappedErr := appErrors.InternalError(string(op), "Component", "", err)
+ l.Error(wrappedErr)
+ }
+}
+
+// OnComponentDeleteAuthz is a handler for the DeleteComponentEvent
+func OnComponentDeleteAuthz(db database.Database, e event.Event, authz openfga.Authorization) {
+ op := appErrors.Op("OnComponentDeleteAuthz")
+
+ deleteInput := []openfga.RelationInput{}
+
+ l := logrus.WithFields(logrus.Fields{
+ "event": "OnComponentDeleteAuthz",
+ "payload": e,
+ })
+
+ if deleteEvent, ok := e.(*DeleteComponentEvent); ok {
+ deleteElement := openfga.RelationInput{
+ ObjectType: openfga.TypeComponent,
+ ObjectId: openfga.ObjectIdFromInt(deleteEvent.ComponentID),
+ }
+ deleteInput = append(deleteInput, deleteElement)
+
+ err := authz.RemoveRelationBulk(deleteInput)
+ if err != nil {
+ wrappedErr := appErrors.InternalError(string(op), "Component", "", err)
+ l.Error(wrappedErr)
+ }
+ } else {
+ err := NewComponentHandlerError("OnComponentDeleteAuthz: triggered with wrong event type")
+ wrappedErr := appErrors.InternalError(string(op), "Component", "", err)
+ l.Error(wrappedErr)
+ }
+}
diff --git a/internal/app/component/component_handler_test.go b/internal/app/component/component_handler_test.go
index b61f6b43..19f3d13b 100644
--- a/internal/app/component/component_handler_test.go
+++ b/internal/app/component/component_handler_test.go
@@ -16,6 +16,7 @@ import (
"github.com/cloudoperators/heureka/internal/entity/test"
"github.com/cloudoperators/heureka/internal/mocks"
"github.com/cloudoperators/heureka/internal/openfga"
+ "github.com/cloudoperators/heureka/internal/util"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/samber/lo"
@@ -27,12 +28,21 @@ func TestComponentHandler(t *testing.T) {
RunSpecs(t, "Component Service Test Suite")
}
-var er event.EventRegistry
-var authz openfga.Authorization
+var handlerContext common.HandlerContext
+var cfg *util.Config
var _ = BeforeSuite(func() {
+ cfg = common.GetTestConfig()
+ enableLogs := false
db := mocks.NewMockDatabase(GinkgoT())
- er = event.NewEventRegistry(db)
+ authz := openfga.NewAuthorizationHandler(cfg, enableLogs)
+ er := event.NewEventRegistry(db, authz)
+ handlerContext = common.HandlerContext{
+ DB: db,
+ EventReg: er,
+ Cache: cache.NewNoCache(),
+ Authz: authz,
+ }
})
func getComponentFilter() *entity.ComponentFilter {
@@ -48,24 +58,21 @@ func getComponentFilter() *entity.ComponentFilter {
var _ = Describe("When listing Components", Label("app", "ListComponents"), func() {
var (
+ er event.EventRegistry
db *mocks.MockDatabase
componentHandler c.ComponentHandler
filter *entity.ComponentFilter
options *entity.ListOptions
- handlerContext common.HandlerContext
)
BeforeEach(func() {
db = mocks.NewMockDatabase(GinkgoT())
+ er = event.NewEventRegistry(db, handlerContext.Authz)
options = entity.NewListOptions()
filter = getComponentFilter()
- handlerContext = common.HandlerContext{
- DB: db,
- EventReg: er,
- Cache: cache.NewNoCache(),
- Authz: authz,
- }
+ handlerContext.DB = db
+ handlerContext.EventReg = er
})
When("the list option does include the totalCount", func() {
@@ -126,15 +133,17 @@ var _ = Describe("When listing Components", Label("app", "ListComponents"), func
var _ = Describe("When creating Component", Label("app", "CreateComponent"), func() {
var (
+ er event.EventRegistry
db *mocks.MockDatabase
componentHandler c.ComponentHandler
component entity.Component
filter *entity.ComponentFilter
- handlerContext common.HandlerContext
+ p openfga.PermissionInput
)
BeforeEach(func() {
db = mocks.NewMockDatabase(GinkgoT())
+ er = event.NewEventRegistry(db, handlerContext.Authz)
component = test.NewFakeComponentEntity()
first := 10
after := ""
@@ -144,12 +153,17 @@ var _ = Describe("When creating Component", Label("app", "CreateComponent"), fun
After: &after,
},
}
- handlerContext = common.HandlerContext{
- DB: db,
- EventReg: er,
- Cache: cache.NewNoCache(),
- Authz: authz,
+
+ p = openfga.PermissionInput{
+ UserType: openfga.TypeRole,
+ UserId: "0",
+ ObjectId: "testcomponent",
+ ObjectType: openfga.TypeComponent,
+ Relation: openfga.RelRole,
}
+
+ handlerContext.DB = db
+ handlerContext.EventReg = er
})
It("creates component", func() {
@@ -166,19 +180,41 @@ var _ = Describe("When creating Component", Label("app", "CreateComponent"), fun
Expect(newComponent.Type).To(BeEquivalentTo(component.Type))
})
})
+
+ Context("when handling a CreateComponentEvent", func() {
+ Context("when new component is created", func() {
+ It("should add user resource relationship tuple in openfga", func() {
+ compFake := test.NewFakeComponentEntity()
+ createEvent := &c.CreateComponentEvent{
+ Component: &compFake,
+ }
+
+ // Use type assertion to convert a CreateServiceEvent into an Event
+ var event event.Event = createEvent
+ p.ObjectId = openfga.ObjectIdFromInt(createEvent.Component.Id)
+ // Simulate event
+ c.OnComponentCreateAuthz(db, event, handlerContext.Authz)
+
+ ok, err := handlerContext.Authz.CheckPermission(p)
+ Expect(err).To(BeNil(), "no error should be thrown")
+ Expect(ok).To(BeTrue(), "permission should be granted")
+ })
+ })
+ })
})
var _ = Describe("When updating Component", Label("app", "UpdateComponent"), func() {
var (
+ er event.EventRegistry
db *mocks.MockDatabase
componentHandler c.ComponentHandler
component entity.ComponentResult
filter *entity.ComponentFilter
- handlerContext common.HandlerContext
)
BeforeEach(func() {
db = mocks.NewMockDatabase(GinkgoT())
+ er = event.NewEventRegistry(db, handlerContext.Authz)
component = test.NewFakeComponentResult()
first := 10
after := ""
@@ -188,12 +224,9 @@ var _ = Describe("When updating Component", Label("app", "UpdateComponent"), fun
After: &after,
},
}
- handlerContext = common.HandlerContext{
- DB: db,
- EventReg: er,
- Cache: cache.NewNoCache(),
- Authz: authz,
- }
+
+ handlerContext.DB = db
+ handlerContext.EventReg = er
})
It("updates component", func() {
@@ -214,15 +247,16 @@ var _ = Describe("When updating Component", Label("app", "UpdateComponent"), fun
var _ = Describe("When deleting Component", Label("app", "DeleteComponent"), func() {
var (
+ er event.EventRegistry
db *mocks.MockDatabase
componentHandler c.ComponentHandler
id int64
filter *entity.ComponentFilter
- handlerContext common.HandlerContext
)
BeforeEach(func() {
db = mocks.NewMockDatabase(GinkgoT())
+ er = event.NewEventRegistry(db, handlerContext.Authz)
id = 1
first := 10
after := ""
@@ -232,12 +266,9 @@ var _ = Describe("When deleting Component", Label("app", "DeleteComponent"), fun
After: &after,
},
}
- handlerContext = common.HandlerContext{
- DB: db,
- EventReg: er,
- Cache: cache.NewNoCache(),
- Authz: authz,
- }
+
+ handlerContext.DB = db
+ handlerContext.EventReg = er
})
It("deletes component", func() {
@@ -254,4 +285,97 @@ var _ = Describe("When deleting Component", Label("app", "DeleteComponent"), fun
Expect(err).To(BeNil(), "no error should be thrown")
Expect(components.Elements).To(BeEmpty(), "no error should be thrown")
})
+
+ Context("when handling an DeleteComponentEvent", func() {
+ Context("when new component is deleted", func() {
+ It("should delete tuples related to that component in openfga", func() {
+ // Test OnComponentDeleteAuthz against all possible relations
+ compFake := test.NewFakeComponentEntity()
+ deleteEvent := &c.DeleteComponentEvent{
+ ComponentID: compFake.Id,
+ }
+ objectId := openfga.ObjectIdFromInt(deleteEvent.ComponentID)
+ relations := []openfga.RelationInput{
+ { // role - component: a role is assigned to the component
+ UserType: openfga.TypeRole,
+ UserId: openfga.IDRole,
+ ObjectId: objectId,
+ ObjectType: openfga.TypeComponent,
+ Relation: openfga.RelRole,
+ },
+ { // component_version - component: a component version is related to the component
+ UserType: openfga.TypeComponentVersion,
+ UserId: openfga.IDComponentVersion,
+ ObjectId: objectId,
+ ObjectType: openfga.TypeComponent,
+ Relation: openfga.RelComponentVersion,
+ },
+ { // user - component: a user can view the component
+ UserType: openfga.TypeUser,
+ UserId: openfga.IDUser,
+ ObjectId: objectId,
+ ObjectType: openfga.TypeComponent,
+ Relation: openfga.RelCanView,
+ },
+ }
+
+ handlerContext.Authz.AddRelationBulk(relations)
+
+ // get the number of relations before deletion
+ relCountBefore := 0
+ for _, r := range relations {
+ relationsList, err := handlerContext.Authz.ListRelations(r)
+ if err != nil {
+ Expect(err).To(BeNil(), "no error should be thrown")
+ }
+ relCountBefore += len(relationsList)
+ }
+ relationsCountBefore := relCountBefore
+ Expect(relationsCountBefore).To(BeEquivalentTo(len(relations)), "all relations should exist before deletion")
+
+ // check that relations were created
+ for _, r := range relations {
+ ok, err := handlerContext.Authz.CheckPermission(openfga.PermissionInput{
+ UserType: r.UserType,
+ UserId: r.UserId,
+ ObjectType: r.ObjectType,
+ ObjectId: r.ObjectId,
+ Relation: r.Relation,
+ })
+ Expect(err).To(BeNil(), "no error should be thrown")
+ Expect(ok).To(BeTrue(), "permission should be granted")
+ }
+
+ var event event.Event = deleteEvent
+ c.OnComponentDeleteAuthz(db, event, handlerContext.Authz)
+
+ // get the number of relations after deletion
+ relCountAfter := 0
+ for _, r := range relations {
+ relationsList, err := handlerContext.Authz.ListRelations(r)
+ if err != nil {
+ Expect(err).To(BeNil(), "no error should be thrown")
+ }
+ relCountAfter += len(relationsList)
+ }
+ relationsCountAfter := relCountAfter
+ Expect(relationsCountAfter < relationsCountBefore).To(BeTrue(), "less relations after deletion")
+ Expect(relationsCountAfter).To(BeEquivalentTo(0), "no relations should exist after deletion")
+
+ // verify that relations were deleted
+ for _, r := range relations {
+ ok, err := handlerContext.Authz.CheckPermission(openfga.PermissionInput{
+ UserType: r.UserType,
+ UserId: r.UserId,
+ ObjectType: r.ObjectType,
+ ObjectId: r.ObjectId,
+ Relation: r.Relation,
+ })
+ Expect(err).To(BeNil(), "no error should be thrown")
+ Expect(ok).To(BeFalse(), "permission should NOT be granted")
+ }
+ })
+ })
+ })
+
})
diff --git a/internal/app/component_instance/component_instance_handler.go b/internal/app/component_instance/component_instance_handler.go
index b56ca903..4408d474 100644
--- a/internal/app/component_instance/component_instance_handler.go
+++ b/internal/app/component_instance/component_instance_handler.go
@@ -31,6 +31,18 @@ type componentInstanceHandler struct {
logger *logrus.Logger
}
+type ComponentInstanceHandlerError struct {
+ msg string
+}
+
+func (e *ComponentInstanceHandlerError) Error() string {
+ return fmt.Sprintf("ComponentInstanceHandlerError: %s", e.msg)
+}
+
+func NewComponentInstanceHandlerError(msg string) *ComponentInstanceHandlerError {
+ return &ComponentInstanceHandlerError{msg: msg}
+}
+
func NewComponentInstanceHandler(handlerContext common.HandlerContext) ComponentInstanceHandler {
return &componentInstanceHandler{
database: handlerContext.DB,
diff --git a/internal/app/component_instance/component_instance_handler_events.go b/internal/app/component_instance/component_instance_handler_events.go
index 44c3e036..25161a45 100644
--- a/internal/app/component_instance/component_instance_handler_events.go
+++ b/internal/app/component_instance/component_instance_handler_events.go
@@ -5,7 +5,11 @@ package component_instance
import (
"github.com/cloudoperators/heureka/internal/app/event"
+ "github.com/cloudoperators/heureka/internal/database"
"github.com/cloudoperators/heureka/internal/entity"
+ appErrors "github.com/cloudoperators/heureka/internal/errors"
+ "github.com/cloudoperators/heureka/internal/openfga"
+ "github.com/sirupsen/logrus"
)
const (
@@ -158,3 +162,190 @@ type ListContextsEvent struct {
func (e *ListContextsEvent) Name() event.EventName {
return ListContextsEventName
}
+
+// OnComponentInstanceCreateAuthz is a handler for the CreateComponentInstanceEvent
+// It creates an OpenFGA relation tuple for the component instance and the current user
+func OnComponentInstanceCreateAuthz(db database.Database, e event.Event, authz openfga.Authorization) {
+ op := appErrors.Op("OnComponentInstanceCreateAuthz")
+
+ l := logrus.WithFields(logrus.Fields{
+ "event": "OnComponentInstanceCreateAuthz",
+ "payload": e,
+ })
+
+ if createEvent, ok := e.(*CreateComponentInstanceEvent); ok {
+ userId := openfga.UserIdFromInt(createEvent.ComponentInstance.CreatedBy)
+
+ relations := []openfga.RelationInput{
+ {
+ UserType: openfga.TypeRole,
+ UserId: userId,
+ Relation: openfga.RelRole,
+ ObjectType: openfga.TypeComponentInstance,
+ ObjectId: openfga.ObjectIdFromInt(createEvent.ComponentInstance.Id),
+ },
+ {
+ UserType: openfga.TypeService,
+ UserId: openfga.UserIdFromInt(createEvent.ComponentInstance.ServiceId),
+ Relation: openfga.RelRelatedService,
+ ObjectType: openfga.TypeComponentInstance,
+ ObjectId: openfga.ObjectIdFromInt(createEvent.ComponentInstance.Id),
+ },
+ {
+ UserType: openfga.TypeComponentInstance,
+ UserId: openfga.UserIdFromInt(createEvent.ComponentInstance.Id),
+ Relation: openfga.RelComponentInstance,
+ ObjectType: openfga.TypeComponentVersion,
+ ObjectId: openfga.ObjectIdFromInt(createEvent.ComponentInstance.ComponentVersionId),
+ },
+ }
+
+ err := authz.AddRelationBulk(relations)
+ if err != nil {
+ wrappedErr := appErrors.InternalError(string(op), "ComponentInstance", "", err)
+ l.Error(wrappedErr)
+ }
+ } else {
+ err := NewComponentInstanceHandlerError("OnComponentInstanceCreateAuthz: triggered with wrong event type")
+ wrappedErr := appErrors.InternalError(string(op), "ComponentInstance", "", err)
+ l.Error(wrappedErr)
+ }
+}
+
+// OnComponentInstanceUpdateAuthz is a handler for the UpdateComponentInstanceEvent
+// Fields that can be updated in Component Instance which affect tuple relations include:
+// componentinstance_component_version_id
+// componentinstance_service_id
+func OnComponentInstanceUpdateAuthz(db database.Database, e event.Event, authz openfga.Authorization) {
+ op := appErrors.Op("OnComponentInstanceUpdateAuthz")
+
+ l := logrus.WithFields(logrus.Fields{
+ "event": "OnComponentInstanceUpdateAuthz",
+ "payload": e,
+ })
+
+ if updateEvent, ok := e.(*UpdateComponentInstanceEvent); ok {
+ // check if the service relation needs to be updated by looking up existing relations
+ if updateEvent.ComponentInstance.ServiceId != 0 {
+ existingServiceRelations, err := authz.ListRelations(openfga.RelationInput{
+ UserType: openfga.TypeService,
+ UserId: openfga.UserIdFromInt(updateEvent.ComponentInstance.ServiceId),
+ Relation: openfga.RelRelatedService,
+ ObjectType: openfga.TypeComponentInstance,
+ ObjectId: openfga.ObjectIdFromInt(updateEvent.ComponentInstance.Id),
+ })
+ if err != nil {
+ wrappedErr := appErrors.InternalError(string(op), "ComponentInstance", "", err)
+ l.Error(wrappedErr)
+ return
+ }
+ // If no existing relation found, then the service relation needs to be updated
+ if len(existingServiceRelations) == 0 {
+ removeServiceInput := openfga.RelationInput{
+ Relation: openfga.RelRelatedService,
+ ObjectType: openfga.TypeComponentInstance,
+ ObjectId: openfga.ObjectIdFromInt(updateEvent.ComponentInstance.Id),
+ UserType: openfga.TypeService,
+ // UserId left empty to match any service
+ }
+ newServiceRelation := openfga.RelationInput{
+ UserType: openfga.TypeService,
+ UserId: openfga.UserIdFromInt(updateEvent.ComponentInstance.ServiceId),
+ Relation: openfga.RelRelatedService,
+ ObjectType: openfga.TypeComponentInstance,
+ ObjectId: openfga.ObjectIdFromInt(updateEvent.ComponentInstance.Id),
+ }
+ err := authz.UpdateRelation(newServiceRelation, removeServiceInput)
+ if err != nil {
+ wrappedErr := appErrors.InternalError(string(op), "ComponentInstance", "", err)
+ l.Error(wrappedErr)
+ }
+ }
+ }
+
+ // check if the component_version relation needs to be updated by looking up existing relations
+ if updateEvent.ComponentInstance.ComponentVersionId != 0 {
+ existingComponentVersionRelations, err := authz.ListRelations(openfga.RelationInput{
+ Relation: openfga.RelComponentInstance,
+ ObjectType: openfga.TypeComponentVersion,
+ ObjectId: openfga.ObjectIdFromInt(updateEvent.ComponentInstance.ComponentVersionId),
+ UserType: openfga.TypeComponentInstance,
+ UserId: openfga.UserIdFromInt(updateEvent.ComponentInstance.Id),
+ })
+ if err != nil {
+ wrappedErr := appErrors.InternalError(string(op), "ComponentInstance", "", err)
+ l.Error(wrappedErr)
+ return
+ }
+ // If no existing relation found, then the component_version relation needs to be updated
+ if len(existingComponentVersionRelations) == 0 {
+ removeComponentVersionInput := openfga.RelationInput{
+ UserType: openfga.TypeComponentInstance,
+ UserId: openfga.UserIdFromInt(updateEvent.ComponentInstance.Id),
+ Relation: openfga.RelComponentInstance,
+ ObjectType: openfga.TypeComponentVersion,
+ // ObjectId left empty to match any component_version
+ }
+ newComponentVersionRelation := openfga.RelationInput{
+ UserType: openfga.TypeComponentInstance,
+ UserId: openfga.UserIdFromInt(updateEvent.ComponentInstance.Id),
+ Relation: openfga.RelComponentInstance,
+ ObjectType: openfga.TypeComponentVersion,
+ ObjectId: openfga.ObjectIdFromInt(updateEvent.ComponentInstance.ComponentVersionId),
+ }
+ err = authz.UpdateRelation(newComponentVersionRelation, removeComponentVersionInput)
+ if err != nil {
+ wrappedErr := appErrors.InternalError(string(op), "ComponentInstance", "", err)
+ l.Error(wrappedErr)
+ }
+ }
+ }
+ } else {
+ err := NewComponentInstanceHandlerError("OnComponentInstanceUpdateAuthz: triggered with wrong event type")
+ wrappedErr := appErrors.InternalError(string(op), "ComponentInstance", "", err)
+ l.Error(wrappedErr)
+ }
+}
+
+// OnComponentInstanceDeleteAuthz is a handler for the DeleteComponentInstanceEvent
+// It creates an OpenFGA relation tuple for the component instance and the current user
+func OnComponentInstanceDeleteAuthz(db database.Database, e event.Event, authz openfga.Authorization) {
+ op := appErrors.Op("OnComponentInstanceDeleteAuthz")
+
+ deleteInput := []openfga.RelationInput{}
+
+ l := logrus.WithFields(logrus.Fields{
+ "event": "OnComponentInstanceDeleteAuthz",
+ "payload": e,
+ })
+
+ if deleteEvent, ok := e.(*DeleteComponentInstanceEvent); ok {
+ // Delete all tuples where object is the component_instance
+ deleteInput = append(deleteInput, openfga.RelationInput{
+ ObjectType: openfga.TypeComponentInstance,
+ ObjectId: openfga.ObjectIdFromInt(deleteEvent.ComponentInstanceID),
+ })
+
+ // Delete all tuples where user is the component_instance (includes relations to component version and issue match)
+ deleteInput = append(deleteInput, openfga.RelationInput{
+ UserType: openfga.TypeComponentInstance,
+ UserId: openfga.UserIdFromInt(deleteEvent.ComponentInstanceID),
+ ObjectType: openfga.TypeComponentVersion,
+ })
+ deleteInput = append(deleteInput, openfga.RelationInput{
+ UserType: openfga.TypeComponentInstance,
+ UserId: openfga.UserIdFromInt(deleteEvent.ComponentInstanceID),
+ ObjectType: openfga.TypeIssueMatch,
+ })
+
+ err := authz.RemoveRelationBulk(deleteInput)
+ if err != nil {
+ wrappedErr := appErrors.InternalError(string(op), "ComponentInstance", "", err)
+ l.Error(wrappedErr)
+ }
+ } else {
+ err := NewComponentInstanceHandlerError("OnComponentInstanceDeleteAuthz: triggered with wrong event type")
+ wrappedErr := appErrors.InternalError(string(op), "ComponentInstance", "", err)
+ l.Error(wrappedErr)
+ }
+}
diff --git a/internal/app/component_instance/component_instance_handler_test.go b/internal/app/component_instance/component_instance_handler_test.go
index 6ceac97f..8c700635 100644
--- a/internal/app/component_instance/component_instance_handler_test.go
+++ b/internal/app/component_instance/component_instance_handler_test.go
@@ -6,11 +6,11 @@ package component_instance_test
import (
"errors"
- "github.com/cloudoperators/heureka/internal/app/common"
-
"math"
+ "strconv"
"testing"
+ "github.com/cloudoperators/heureka/internal/app/common"
ci "github.com/cloudoperators/heureka/internal/app/component_instance"
"github.com/cloudoperators/heureka/internal/app/event"
"github.com/cloudoperators/heureka/internal/cache"
@@ -21,6 +21,7 @@ import (
appErrors "github.com/cloudoperators/heureka/internal/errors"
"github.com/cloudoperators/heureka/internal/mocks"
"github.com/cloudoperators/heureka/internal/openfga"
+ "github.com/cloudoperators/heureka/internal/util"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/samber/lo"
@@ -32,12 +33,17 @@ func TestComponentInstanceHandler(t *testing.T) {
RunSpecs(t, "Component Instance Service Test Suite")
}
-var er event.EventRegistry
-var authz openfga.Authorization
+var handlerContext common.HandlerContext
+var cfg *util.Config
var _ = BeforeSuite(func() {
- db := mocks.NewMockDatabase(GinkgoT())
- er = event.NewEventRegistry(db)
+ cfg = common.GetTestConfig()
+ enableLogs := false
+ authz := openfga.NewAuthorizationHandler(cfg, enableLogs)
+ handlerContext = common.HandlerContext{
+ Cache: cache.NewNoCache(),
+ Authz: authz,
+ }
})
func componentInstanceFilter() *entity.ComponentInstanceFilter {
@@ -53,23 +59,21 @@ func componentInstanceFilter() *entity.ComponentInstanceFilter {
var _ = Describe("When listing Component Instances", Label("app", "ListComponentInstances"), func() {
var (
+ er event.EventRegistry
db *mocks.MockDatabase
componentInstanceHandler ci.ComponentInstanceHandler
filter *entity.ComponentInstanceFilter
options *entity.ListOptions
- handlerContext common.HandlerContext
)
BeforeEach(func() {
db = mocks.NewMockDatabase(GinkgoT())
+ er = event.NewEventRegistry(db, handlerContext.Authz)
+
options = entity.NewListOptions()
filter = componentInstanceFilter()
- handlerContext = common.HandlerContext{
- DB: db,
- EventReg: er,
- Cache: cache.NewNoCache(),
- Authz: authz,
- }
+ handlerContext.DB = db
+ handlerContext.EventReg = er
})
When("the list option does include the totalCount", func() {
@@ -81,6 +85,7 @@ var _ = Describe("When listing Component Instances", Label("app", "ListComponent
})
It("shows the total count in the results", func() {
+ componentInstanceHandler = ci.NewComponentInstanceHandler(handlerContext)
componentInstanceHandler = ci.NewComponentInstanceHandler(handlerContext)
res, err := componentInstanceHandler.ListComponentInstances(filter, options)
Expect(err).To(BeNil(), "no error should be thrown")
@@ -115,6 +120,7 @@ var _ = Describe("When listing Component Instances", Label("app", "ListComponent
db.On("GetComponentInstances", filter, []entity.Order{}).Return(componentInstances, nil)
db.On("GetAllComponentInstanceCursors", filter, []entity.Order{}).Return(cursors, nil)
componentInstanceHandler = ci.NewComponentInstanceHandler(handlerContext)
+ componentInstanceHandler = ci.NewComponentInstanceHandler(handlerContext)
res, err := componentInstanceHandler.ListComponentInstances(filter, options)
Expect(err).To(BeNil(), "no error should be thrown")
Expect(*res.PageInfo.HasNextPage).To(BeEquivalentTo(hasNextPage), "correct hasNextPage indicator")
@@ -133,6 +139,7 @@ var _ = Describe("When listing Component Instances", Label("app", "ListComponent
dbError := errors.New("database connection failed")
db.On("GetComponentInstances", filter, []entity.Order{}).Return([]entity.ComponentInstanceResult{}, dbError)
+ componentInstanceHandler = ci.NewComponentInstanceHandler(handlerContext)
componentInstanceHandler = ci.NewComponentInstanceHandler(handlerContext)
result, err := componentInstanceHandler.ListComponentInstances(filter, options)
@@ -170,6 +177,7 @@ var _ = Describe("When listing Component Instances", Label("app", "ListComponent
cursorsError := errors.New("cursor database error")
db.On("GetAllComponentInstanceCursors", filter, []entity.Order{}).Return([]string{}, cursorsError)
+ componentInstanceHandler = ci.NewComponentInstanceHandler(handlerContext)
componentInstanceHandler = ci.NewComponentInstanceHandler(handlerContext)
result, err := componentInstanceHandler.ListComponentInstances(filter, options)
@@ -189,21 +197,28 @@ var _ = Describe("When listing Component Instances", Label("app", "ListComponent
var _ = Describe("When creating ComponentInstance", Label("app", "CreateComponentInstance"), func() {
var (
+ er event.EventRegistry
db *mocks.MockDatabase
componentInstanceHandler ci.ComponentInstanceHandler
componentInstance entity.ComponentInstance
- handlerContext common.HandlerContext
+ p openfga.PermissionInput
)
BeforeEach(func() {
db = mocks.NewMockDatabase(GinkgoT())
+ er = event.NewEventRegistry(db, handlerContext.Authz)
componentInstance = test.NewFakeComponentInstanceEntity()
- handlerContext = common.HandlerContext{
- DB: db,
- EventReg: er,
- Cache: cache.NewNoCache(),
- Authz: authz,
+
+ p = openfga.PermissionInput{
+ UserType: openfga.TypeRole,
+ UserId: "0",
+ ObjectId: "test_component_instance",
+ ObjectType: openfga.TypeComponentInstance,
+ Relation: openfga.RelRole,
}
+
+ handlerContext.DB = db
+ handlerContext.EventReg = er
})
Context("with valid input", func() {
@@ -211,6 +226,7 @@ var _ = Describe("When creating ComponentInstance", Label("app", "CreateComponen
db.On("GetAllUserIds", mock.Anything).Return([]int64{123}, nil)
db.On("CreateComponentInstance", mock.AnythingOfType("*entity.ComponentInstance")).Return(&componentInstance, nil)
+ componentInstanceHandler = ci.NewComponentInstanceHandler(handlerContext)
componentInstanceHandler = ci.NewComponentInstanceHandler(handlerContext)
// Ensure type is allowed if ParentId is set
componentInstance.Type = "RecordSet"
@@ -237,35 +253,24 @@ var _ = Describe("When creating ComponentInstance", Label("app", "CreateComponen
})
})
- Context("with valid input w/o Component Version", func() {
- It("creates componentInstance", func() {
- db.On("GetAllUserIds", mock.Anything).Return([]int64{123}, nil)
- db.On("CreateComponentInstance", mock.AnythingOfType("*entity.ComponentInstance")).Return(&componentInstance, nil)
-
- componentInstanceHandler = ci.NewComponentInstanceHandler(handlerContext)
- // Ensure type is allowed if ParentId is set
- componentInstance.Type = "RecordSet"
- componentInstance.ParentId = 1234
- // Set ComponentVersionId to 0 to test creation without it
- componentInstance.ComponentVersionId = 0
- newComponentInstance, err := componentInstanceHandler.CreateComponentInstance(&componentInstance, nil)
- Expect(err).To(BeNil(), "no error should be thrown")
- Expect(newComponentInstance.Id).NotTo(BeEquivalentTo(0))
- By("setting fields", func() {
- Expect(newComponentInstance.CCRN).To(BeEquivalentTo(componentInstance.CCRN))
- Expect(newComponentInstance.Region).To(BeEquivalentTo(componentInstance.Region))
- Expect(newComponentInstance.Cluster).To(BeEquivalentTo(componentInstance.Cluster))
- Expect(newComponentInstance.Namespace).To(BeEquivalentTo(componentInstance.Namespace))
- Expect(newComponentInstance.Domain).To(BeEquivalentTo(componentInstance.Domain))
- Expect(newComponentInstance.Project).To(BeEquivalentTo(componentInstance.Project))
- Expect(newComponentInstance.Pod).To(BeEquivalentTo(componentInstance.Pod))
- Expect(newComponentInstance.Container).To(BeEquivalentTo(componentInstance.Container))
- Expect(newComponentInstance.Type).To(BeEquivalentTo(componentInstance.Type))
- Expect(newComponentInstance.Context).To(BeEquivalentTo(componentInstance.Context))
- Expect(newComponentInstance.Count).To(BeEquivalentTo(componentInstance.Count))
- Expect(newComponentInstance.ComponentVersionId).To(BeEquivalentTo(componentInstance.ComponentVersionId))
- Expect(newComponentInstance.ServiceId).To(BeEquivalentTo(componentInstance.ServiceId))
- Expect(newComponentInstance.ParentId).To(BeEquivalentTo(componentInstance.ParentId))
+ Context("when handling a CreateComponentInstanceEvent", func() {
+ Context("when new component instance is created", func() {
+ It("should add user resource relationship tuple in openfga", func() {
+ ciFake := test.NewFakeComponentInstanceEntity()
+ createEvent := &ci.CreateComponentInstanceEvent{
+ ComponentInstance: &ciFake,
+ }
+
+ // Use type assertion to convert a CreateServiceEvent into an Event
+ var event event.Event = createEvent
+ resourceId := strconv.FormatInt(createEvent.ComponentInstance.Id, 10)
+ p.ObjectId = openfga.ObjectId(resourceId)
+ // Simulate event
+ ci.OnComponentInstanceCreateAuthz(db, event, handlerContext.Authz)
+
+ ok, err := handlerContext.Authz.CheckPermission(p)
+ Expect(err).To(BeNil(), "no error should be thrown")
+ Expect(ok).To(BeTrue(), "permission should be granted")
})
})
})
@@ -273,15 +278,16 @@ var _ = Describe("When creating ComponentInstance", Label("app", "CreateComponen
var _ = Describe("When updating ComponentInstance", Label("app", "UpdateComponentInstance"), func() {
var (
+ er event.EventRegistry
db *mocks.MockDatabase
componentInstanceHandler ci.ComponentInstanceHandler
componentInstance entity.ComponentInstanceResult
filter *entity.ComponentInstanceFilter
- handlerContext common.HandlerContext
)
BeforeEach(func() {
db = mocks.NewMockDatabase(GinkgoT())
+ er = event.NewEventRegistry(db, handlerContext.Authz)
componentInstance = test.NewFakeComponentInstanceResult()
first := 10
after := ""
@@ -291,18 +297,15 @@ var _ = Describe("When updating ComponentInstance", Label("app", "UpdateComponen
After: &after,
},
}
- handlerContext = common.HandlerContext{
- DB: db,
- EventReg: er,
- Cache: cache.NewNoCache(),
- Authz: authz,
- }
+ handlerContext.DB = db
+ handlerContext.EventReg = er
})
Context("with valid input", func() {
It("updates componentInstance", func() {
db.On("GetAllUserIds", mock.Anything).Return([]int64{123}, nil) // Changed: return actual user ID
db.On("UpdateComponentInstance", componentInstance.ComponentInstance).Return(nil)
componentInstanceHandler = ci.NewComponentInstanceHandler(handlerContext)
+ componentInstanceHandler = ci.NewComponentInstanceHandler(handlerContext)
componentInstance.Region = "NewRegion"
componentInstance.Cluster = "NewCluster"
componentInstance.Namespace = "NewNamespace"
@@ -336,19 +339,155 @@ var _ = Describe("When updating ComponentInstance", Label("app", "UpdateComponen
})
})
})
+
+ Context("when handling an UpdateComponentInstanceEvent", func() {
+ It("should update service and component_version relations in openfga", func() {
+ ciFake := test.NewFakeComponentInstanceEntity()
+ oldServiceId := int64(111)
+ newServiceId := int64(222)
+ oldComponentVersionId := int64(333)
+ newComponentVersionId := int64(444)
+
+ // Add initial relations
+ initialServiceRelation := openfga.RelationInput{
+ UserType: openfga.TypeService,
+ UserId: openfga.UserIdFromInt(oldServiceId),
+ Relation: openfga.RelRelatedService,
+ ObjectType: openfga.TypeComponentInstance,
+ ObjectId: openfga.ObjectIdFromInt(ciFake.Id),
+ }
+ initialComponentVersionRelation := openfga.RelationInput{
+ UserType: openfga.TypeComponentInstance,
+ UserId: openfga.UserIdFromInt(ciFake.Id),
+ Relation: openfga.RelComponentInstance,
+ ObjectType: openfga.TypeComponentVersion,
+ ObjectId: openfga.ObjectIdFromInt(oldComponentVersionId),
+ }
+
+ handlerContext.Authz.AddRelationBulk([]openfga.RelationInput{
+ initialServiceRelation,
+ initialComponentVersionRelation,
+ })
+
+ // Fake update event with new service and component_version ids
+ ciFake.ServiceId = newServiceId
+ ciFake.ComponentVersionId = newComponentVersionId
+ updateEvent := &ci.UpdateComponentInstanceEvent{
+ ComponentInstance: &ciFake,
+ }
+ var event event.Event = updateEvent
+
+ // Simulate event
+ ci.OnComponentInstanceUpdateAuthz(db, event, handlerContext.Authz)
+
+ // Check that the old relations are gone
+ remainingOldService, err := handlerContext.Authz.ListRelations(initialServiceRelation)
+ Expect(err).To(BeNil(), "no error should be thrown")
+ Expect(remainingOldService).To(BeEmpty(), "old service relation should be removed")
+
+ remainingOldComponentVersion, err := handlerContext.Authz.ListRelations(initialComponentVersionRelation)
+ Expect(err).To(BeNil(), "no error should be thrown")
+ Expect(remainingOldComponentVersion).To(BeEmpty(), "old component_version relation should be removed")
+
+ // Check that the new relations exist
+ newServiceRelation := openfga.RelationInput{
+ UserType: openfga.TypeService,
+ UserId: openfga.UserIdFromInt(newServiceId),
+ Relation: openfga.RelRelatedService,
+ ObjectType: openfga.TypeComponentInstance,
+ ObjectId: openfga.ObjectIdFromInt(ciFake.Id),
+ }
+ newComponentVersionRelation := openfga.RelationInput{
+ UserType: openfga.TypeComponentInstance,
+ UserId: openfga.UserIdFromInt(ciFake.Id),
+ Relation: openfga.RelComponentInstance,
+ ObjectType: openfga.TypeComponentVersion,
+ ObjectId: openfga.ObjectIdFromInt(newComponentVersionId),
+ }
+ remainingNewService, err := handlerContext.Authz.ListRelations(newServiceRelation)
+ Expect(err).To(BeNil(), "no error should be thrown")
+ Expect(remainingNewService).NotTo(BeEmpty(), "new service relation should exist")
+
+ remainingNewComponentVersion, err := handlerContext.Authz.ListRelations(newComponentVersionRelation)
+ Expect(err).To(BeNil(), "no error should be thrown")
+ Expect(remainingNewComponentVersion).NotTo(BeEmpty(), "new component_version relation should exist")
+ })
+
+ It("should update only the service relation in openfga", func() {
+ ciFake := test.NewFakeComponentInstanceEntity()
+ oldServiceId := int64(111)
+ newServiceId := int64(222)
+ oldComponentVersionId := int64(333)
+
+ // Add initial relations
+ initialServiceRelation := openfga.RelationInput{
+ UserType: openfga.TypeService,
+ UserId: openfga.UserIdFromInt(oldServiceId),
+ Relation: openfga.RelRelatedService,
+ ObjectType: openfga.TypeComponentInstance,
+ ObjectId: openfga.ObjectIdFromInt(ciFake.Id),
+ }
+ initialComponentVersionRelation := openfga.RelationInput{
+ UserType: openfga.TypeComponentInstance,
+ UserId: openfga.UserIdFromInt(ciFake.Id),
+ Relation: openfga.RelComponentInstance,
+ ObjectType: openfga.TypeComponentVersion,
+ ObjectId: openfga.ObjectIdFromInt(oldComponentVersionId),
+ }
+
+ handlerContext.Authz.AddRelationBulk([]openfga.RelationInput{
+ initialServiceRelation,
+ initialComponentVersionRelation,
+ })
+
+ // Fake update event with new service and component_version ids
+ ciFake.ServiceId = newServiceId
+ ciFake.ComponentVersionId = oldComponentVersionId // need to pass old id otherwise fake ci will use a new random id
+ updateEvent := &ci.UpdateComponentInstanceEvent{
+ ComponentInstance: &ciFake,
+ }
+ var event event.Event = updateEvent
+
+ // Simulate event
+ ci.OnComponentInstanceUpdateAuthz(db, event, handlerContext.Authz)
+
+ // Check that the old relations are gone
+ remainingOldService, err := handlerContext.Authz.ListRelations(initialServiceRelation)
+ Expect(err).To(BeNil(), "no error should be thrown")
+ Expect(remainingOldService).To(BeEmpty(), "old service relation should be removed")
+
+ // Check that the new relation exists
+ newServiceRelation := openfga.RelationInput{
+ UserType: openfga.TypeService,
+ UserId: openfga.UserIdFromInt(newServiceId),
+ Relation: openfga.RelRelatedService,
+ ObjectType: openfga.TypeComponentInstance,
+ ObjectId: openfga.ObjectIdFromInt(ciFake.Id),
+ }
+ remainingNewService, err := handlerContext.Authz.ListRelations(newServiceRelation)
+ Expect(err).To(BeNil(), "no error should be thrown")
+ Expect(remainingNewService).NotTo(BeEmpty(), "new service relation should exist")
+
+ // Check that the old component_version relation still exists
+ remainingOldComponentVersion, err := handlerContext.Authz.ListRelations(initialComponentVersionRelation)
+ Expect(err).To(BeNil(), "no error should be thrown")
+ Expect(remainingOldComponentVersion).ToNot(BeEmpty(), "old component_version relation should remain")
+ })
+ })
})
var _ = Describe("When deleting ComponentInstance", Label("app", "DeleteComponentInstance"), func() {
var (
+ er event.EventRegistry
db *mocks.MockDatabase
componentInstanceHandler ci.ComponentInstanceHandler
id int64
filter *entity.ComponentInstanceFilter
- handlerContext common.HandlerContext
)
BeforeEach(func() {
db = mocks.NewMockDatabase(GinkgoT())
+ er = event.NewEventRegistry(db, handlerContext.Authz)
id = 1
first := 10
after := ""
@@ -358,12 +497,8 @@ var _ = Describe("When deleting ComponentInstance", Label("app", "DeleteComponen
After: &after,
},
}
- handlerContext = common.HandlerContext{
- DB: db,
- EventReg: er,
- Cache: cache.NewNoCache(),
- Authz: authz,
- }
+ handlerContext.DB = db
+ handlerContext.EventReg = er
})
Context("with valid input", func() {
@@ -371,6 +506,7 @@ var _ = Describe("When deleting ComponentInstance", Label("app", "DeleteComponen
db.On("GetAllUserIds", mock.Anything).Return([]int64{123}, nil) // Changed: return actual user ID
db.On("DeleteComponentInstance", id, int64(123)).Return(nil) // Changed: specify exact user ID
componentInstanceHandler = ci.NewComponentInstanceHandler(handlerContext)
+ componentInstanceHandler = ci.NewComponentInstanceHandler(handlerContext)
db.On("GetComponentInstances", filter, []entity.Order{}).Return([]entity.ComponentInstanceResult{}, nil)
err := componentInstanceHandler.DeleteComponentInstance(id)
Expect(err).To(BeNil(), "no error should be thrown")
@@ -381,30 +517,133 @@ var _ = Describe("When deleting ComponentInstance", Label("app", "DeleteComponen
Expect(err).To(BeNil(), "no error should be thrown")
Expect(componentInstances.Elements).To(BeEmpty(), "component instance should be deleted")
})
+
+ Context("when handling a DeleteComponentInstanceEvent", func() {
+ Context("when new component instance is deleted", func() {
+ It("should delete tuples related to that component instance in openfga", func() {
+ // Test OnComponentInstanceDeleteAuthz against all possible relations
+ ciFake := test.NewFakeComponentInstanceEntity()
+ deleteEvent := &ci.DeleteComponentInstanceEvent{
+ ComponentInstanceID: ciFake.Id,
+ }
+ objectId := openfga.ObjectIdFromInt(deleteEvent.ComponentInstanceID)
+ userId := openfga.UserIdFromInt(deleteEvent.ComponentInstanceID)
+ relations := []openfga.RelationInput{
+ { // role - component_instance: a role is assigned to the component instance
+ UserType: openfga.TypeRole,
+ UserId: openfga.IDRole,
+ ObjectId: objectId,
+ ObjectType: openfga.TypeComponentInstance,
+ Relation: openfga.RelRole,
+ },
+ { // service - component_instance: a service is related to the component instance
+ UserType: openfga.TypeService,
+ UserId: openfga.IDService,
+ ObjectId: objectId,
+ ObjectType: openfga.TypeComponentInstance,
+ Relation: openfga.RelRelatedService,
+ },
+ { // user - component_instance: a user can view the component instance
+ UserType: openfga.TypeUser,
+ UserId: openfga.IDUser,
+ ObjectId: objectId,
+ ObjectType: openfga.TypeComponentInstance,
+ Relation: openfga.RelCanView,
+ },
+ { // component_instance - component_version: a component instance is related to a component version
+ UserType: openfga.TypeComponentInstance,
+ UserId: userId,
+ ObjectId: openfga.IDComponentVersion,
+ ObjectType: openfga.TypeComponentVersion,
+ Relation: openfga.RelComponentInstance,
+ },
+ { // component_instance - issue_match: a component instance is related to an issue match
+ UserType: openfga.TypeComponentInstance,
+ UserId: userId,
+ ObjectId: openfga.IDIssueMatch,
+ ObjectType: openfga.TypeIssueMatch,
+ Relation: openfga.RelComponentInstance,
+ },
+ }
+
+ handlerContext.Authz.AddRelationBulk(relations)
+
+ // get the number of relations before deletion
+ relCountBefore := 0
+ for _, r := range relations {
+ relations, err := handlerContext.Authz.ListRelations(r)
+ if err != nil {
+ Expect(err).To(BeNil(), "no error should be thrown")
+ }
+ relCountBefore += len(relations)
+ }
+ Expect(relCountBefore).To(Equal(len(relations)), "all relations should exist before deletion")
+
+ // check that relations were created
+ for _, r := range relations {
+ ok, err := handlerContext.Authz.CheckPermission(openfga.PermissionInput{
+ UserType: r.UserType,
+ UserId: r.UserId,
+ ObjectType: r.ObjectType,
+ ObjectId: r.ObjectId,
+ Relation: r.Relation,
+ })
+ Expect(err).To(BeNil(), "no error should be thrown")
+ Expect(ok).To(BeTrue(), "permission should be granted")
+ }
+
+ var event event.Event = deleteEvent
+ // Simulate event
+ ci.OnComponentInstanceDeleteAuthz(db, event, handlerContext.Authz)
+
+ // get the number of relations after deletion
+ relCountAfter := 0
+ for _, r := range relations {
+ relations, err := handlerContext.Authz.ListRelations(r)
+ if err != nil {
+ Expect(err).To(BeNil(), "no error should be thrown")
+ }
+ relCountAfter += len(relations)
+ }
+ // Expect(relCountAfter < relCountBefore).To(BeTrue(), "less relations after deletion")
+ Expect(relCountAfter).To(BeEquivalentTo(0), "no relations should exist after deletion")
+
+ // verify that relations were deleted
+ for _, r := range relations {
+ ok, err := handlerContext.Authz.CheckPermission(openfga.PermissionInput{
+ UserType: r.UserType,
+ UserId: r.UserId,
+ ObjectType: r.ObjectType,
+ ObjectId: r.ObjectId,
+ Relation: r.Relation,
+ })
+ Expect(err).To(BeNil(), "no error should be thrown")
+ Expect(ok).To(BeFalse(), "permission should NOT be granted")
+ }
+ })
+ })
+ })
})
})
var _ = Describe("When listing CCRN", Label("app", "ListCcrn"), func() {
var (
+ er event.EventRegistry
db *mocks.MockDatabase
componentInstanceHandler ci.ComponentInstanceHandler
filter *entity.ComponentInstanceFilter
options *entity.ListOptions
CCRN string
- handlerContext common.HandlerContext
)
BeforeEach(func() {
db = mocks.NewMockDatabase(GinkgoT())
+ er = event.NewEventRegistry(db, handlerContext.Authz)
options = entity.NewListOptions()
filter = componentInstanceFilter()
CCRN = "ca9d963d-b441-4167-b08d-086e76186653"
- handlerContext = common.HandlerContext{
- DB: db,
- EventReg: er,
- Cache: cache.NewNoCache(),
- Authz: authz,
- }
+ handlerContext.DB = db
+ handlerContext.EventReg = er
})
When("no filters are used", func() {
@@ -413,6 +652,7 @@ var _ = Describe("When listing CCRN", Label("app", "ListCcrn"), func() {
})
It("it return the results", func() {
+ componentInstanceHandler = ci.NewComponentInstanceHandler(handlerContext)
componentInstanceHandler = ci.NewComponentInstanceHandler(handlerContext)
res, err := componentInstanceHandler.ListCcrns(filter, options)
Expect(err).To(BeNil(), "no error should be thrown")
@@ -429,6 +669,7 @@ var _ = Describe("When listing CCRN", Label("app", "ListCcrn"), func() {
db.On("GetCcrn", filter).Return([]string{CCRN}, nil)
})
It("returns filtered CCRN according to the CCRN type", func() {
+ componentInstanceHandler = ci.NewComponentInstanceHandler(handlerContext)
componentInstanceHandler = ci.NewComponentInstanceHandler(handlerContext)
res, err := componentInstanceHandler.ListCcrns(filter, options)
Expect(err).To(BeNil(), "no error should be thrown")
@@ -443,6 +684,7 @@ var _ = Describe("When listing CCRN", Label("app", "ListCcrn"), func() {
dbError := errors.New("database connection failed")
db.On("GetCcrn", filter).Return([]string{}, dbError)
+ componentInstanceHandler = ci.NewComponentInstanceHandler(handlerContext)
componentInstanceHandler = ci.NewComponentInstanceHandler(handlerContext)
result, err := componentInstanceHandler.ListCcrns(filter, options)
diff --git a/internal/app/component_version/component_version_handler_events.go b/internal/app/component_version/component_version_handler_events.go
index 9dbdc5d9..07bee1c5 100644
--- a/internal/app/component_version/component_version_handler_events.go
+++ b/internal/app/component_version/component_version_handler_events.go
@@ -4,8 +4,14 @@
package component_version
import (
+ "strconv"
+
"github.com/cloudoperators/heureka/internal/app/event"
+ "github.com/cloudoperators/heureka/internal/database"
"github.com/cloudoperators/heureka/internal/entity"
+ appErrors "github.com/cloudoperators/heureka/internal/errors"
+ "github.com/cloudoperators/heureka/internal/openfga"
+ "github.com/sirupsen/logrus"
)
const (
@@ -48,3 +54,118 @@ type DeleteComponentVersionEvent struct {
func (e *DeleteComponentVersionEvent) Name() event.EventName {
return DeleteComponentVersionEventName
}
+
+// OnComponentVersionCreateAuthz is a handler for the CreateComponentVersionEvent
+// It creates an OpenFGA relation tuple for the component version and the current user
+func OnComponentVersionCreateAuthz(db database.Database, e event.Event, authz openfga.Authorization) {
+ op := appErrors.Op("OnComponentVersionCreateAuthz")
+
+ l := logrus.WithFields(logrus.Fields{
+ "event": "OnComponentVersionCreateAuthz",
+ "payload": e,
+ })
+
+ if createEvent, ok := e.(*CreateComponentVersionEvent); ok {
+ userId := openfga.UserIdFromInt(createEvent.ComponentVersion.CreatedBy)
+
+ relations := []openfga.RelationInput{
+ {
+ UserType: openfga.TypeRole,
+ UserId: userId,
+ Relation: openfga.RelRole,
+ ObjectType: openfga.TypeComponentVersion,
+ ObjectId: openfga.ObjectIdFromInt(createEvent.ComponentVersion.Id),
+ },
+ }
+
+ err := authz.AddRelationBulk(relations)
+ if err != nil {
+ wrappedErr := appErrors.InternalError(string(op), "ComponentVersion", "", err)
+ l.Error(wrappedErr)
+ }
+ } else {
+ err := NewComponentVersionHandlerError("OnComponentVersionCreateAuthz: triggered with wrong event type")
+ wrappedErr := appErrors.InternalError(string(op), "ComponentVersion", "", err)
+ l.Error(wrappedErr)
+ }
+}
+
+// OnComponentVersionUpdateAuthz is a handler for the UpdateComponentVersionEvent
+// Fields that can be updated in Component Version which affect tuple relations include:
+// componentversion_component_id
+func OnComponentVersionUpdateAuthz(db database.Database, e event.Event, authz openfga.Authorization) {
+ op := appErrors.Op("OnComponentVersionUpdateAuthz")
+
+ l := logrus.WithFields(logrus.Fields{
+ "event": "OnComponentVersionUpdateAuthz",
+ "payload": e,
+ })
+
+ if updateEvent, ok := e.(*UpdateComponentVersionEvent); ok {
+ newComponentId := strconv.FormatInt(updateEvent.ComponentVersion.ComponentId, 10)
+
+ if newComponentId != "" {
+ // Remove any existing relation where this component_version is connected to any component
+ removeInput := openfga.RelationInput{
+ UserType: openfga.TypeComponentVersion,
+ UserId: openfga.UserIdFromInt(updateEvent.ComponentVersion.Id),
+ Relation: openfga.RelComponentVersion,
+ ObjectType: openfga.TypeComponent,
+ // ObjectId left empty to match any component
+ }
+ newRelation := openfga.RelationInput{
+ UserType: openfga.TypeComponentVersion,
+ UserId: openfga.UserIdFromInt(updateEvent.ComponentVersion.Id),
+ Relation: openfga.RelComponentVersion,
+ ObjectType: openfga.TypeComponent,
+ ObjectId: openfga.ObjectIdFromInt(updateEvent.ComponentVersion.ComponentId),
+ }
+ err := authz.UpdateRelation(newRelation, removeInput)
+ if err != nil {
+ wrappedErr := appErrors.InternalError(string(op), "ComponentVersion", "", err)
+ l.Error(wrappedErr)
+ }
+ }
+ } else {
+ err := NewComponentVersionHandlerError("OnComponentVersionUpdateAuthz: triggered with wrong event type")
+ wrappedErr := appErrors.InternalError(string(op), "ComponentVersion", "", err)
+ l.Error(wrappedErr)
+ }
+}
+
+// OnComponentVersionDeleteAuthz is a handler for the DeleteComponentVersionEvent
+func OnComponentVersionDeleteAuthz(db database.Database, e event.Event, authz openfga.Authorization) {
+ op := appErrors.Op("OnComponentVersionDeleteAuthz")
+
+ deleteInput := []openfga.RelationInput{}
+
+ l := logrus.WithFields(logrus.Fields{
+ "event": "OnComponentVersionDeleteAuthz",
+ "payload": e,
+ })
+
+ if deleteEvent, ok := e.(*DeleteComponentVersionEvent); ok {
+ // Delete all tuples where object is the component_version
+ deleteInput = append(deleteInput, openfga.RelationInput{
+ ObjectType: openfga.TypeComponentVersion,
+ ObjectId: openfga.ObjectIdFromInt(deleteEvent.ComponentVersionID),
+ })
+
+ // Delete all tuples where user is the component_version
+ deleteInput = append(deleteInput, openfga.RelationInput{
+ UserType: openfga.TypeComponentVersion,
+ UserId: openfga.UserIdFromInt(deleteEvent.ComponentVersionID),
+ ObjectType: openfga.TypeComponent,
+ })
+
+ err := authz.RemoveRelationBulk(deleteInput)
+ if err != nil {
+ wrappedErr := appErrors.InternalError(string(op), "ComponentVersion", "", err)
+ l.Error(wrappedErr)
+ }
+ } else {
+ err := NewComponentVersionHandlerError("OnComponentVersionDeleteAuthz: triggered with wrong event type")
+ wrappedErr := appErrors.InternalError(string(op), "ComponentVersion", "", err)
+ l.Error(wrappedErr)
+ }
+}
diff --git a/internal/app/component_version/component_version_handler_test.go b/internal/app/component_version/component_version_handler_test.go
index 6a8065e8..39c71874 100644
--- a/internal/app/component_version/component_version_handler_test.go
+++ b/internal/app/component_version/component_version_handler_test.go
@@ -5,6 +5,7 @@ package component_version_test
import (
"math"
+ "strconv"
"testing"
"github.com/cloudoperators/heureka/internal/app/common"
@@ -16,6 +17,7 @@ import (
"github.com/cloudoperators/heureka/internal/entity/test"
"github.com/cloudoperators/heureka/internal/mocks"
"github.com/cloudoperators/heureka/internal/openfga"
+ "github.com/cloudoperators/heureka/internal/util"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/samber/lo"
@@ -27,12 +29,17 @@ func TestComponentVersionHandler(t *testing.T) {
RunSpecs(t, "Component Version Service Test Suite")
}
-var er event.EventRegistry
-var authz openfga.Authorization
+var handlerContext common.HandlerContext
+var cfg *util.Config
var _ = BeforeSuite(func() {
- db := mocks.NewMockDatabase(GinkgoT())
- er = event.NewEventRegistry(db)
+ cfg = common.GetTestConfig()
+ enableLogs := false
+ authz := openfga.NewAuthorizationHandler(cfg, enableLogs)
+ handlerContext = common.HandlerContext{
+ Cache: cache.NewNoCache(),
+ Authz: authz,
+ }
})
func getComponentVersionFilter() *entity.ComponentVersionFilter {
@@ -46,23 +53,21 @@ func getComponentVersionFilter() *entity.ComponentVersionFilter {
var _ = Describe("When listing ComponentVersions", Label("app", "ListComponentVersions"), func() {
var (
- db *mocks.MockDatabase
- cvHandler cv.ComponentVersionHandler
- filter *entity.ComponentVersionFilter
- options *entity.ListOptions
- handlerContext common.HandlerContext
+ er event.EventRegistry
+ db *mocks.MockDatabase
+ cvHandler cv.ComponentVersionHandler
+ filter *entity.ComponentVersionFilter
+ options *entity.ListOptions
)
BeforeEach(func() {
db = mocks.NewMockDatabase(GinkgoT())
+ er = event.NewEventRegistry(db, handlerContext.Authz)
+
options = entity.NewListOptions()
filter = getComponentVersionFilter()
- handlerContext = common.HandlerContext{
- DB: db,
- EventReg: er,
- Cache: cache.NewNoCache(),
- Authz: authz,
- }
+ handlerContext.DB = db
+ handlerContext.EventReg = er
})
When("the list option does include the totalCount", func() {
@@ -222,21 +227,28 @@ var _ = Describe("When listing ComponentVersions", Label("app", "ListComponentVe
var _ = Describe("When creating ComponentVersion", Label("app", "CreateComponentVersion"), func() {
var (
+ er event.EventRegistry
db *mocks.MockDatabase
componenVersionService cv.ComponentVersionHandler
componentVersion entity.ComponentVersion
- handlerContext common.HandlerContext
+ p openfga.PermissionInput
)
BeforeEach(func() {
db = mocks.NewMockDatabase(GinkgoT())
+ er = event.NewEventRegistry(db, handlerContext.Authz)
componentVersion = test.NewFakeComponentVersionEntity()
- handlerContext = common.HandlerContext{
- DB: db,
- EventReg: er,
- Cache: cache.NewNoCache(),
- Authz: authz,
+
+ p = openfga.PermissionInput{
+ UserType: openfga.TypeRole,
+ UserId: "0",
+ ObjectId: "",
+ ObjectType: openfga.TypeComponentVersion,
+ Relation: openfga.TypeRole,
}
+
+ handlerContext.DB = db
+ handlerContext.EventReg = er
})
It("creates componentVersion", func() {
@@ -252,19 +264,42 @@ var _ = Describe("When creating ComponentVersion", Label("app", "CreateComponent
Expect(newComponentVersion.Tag).To(BeEquivalentTo(componentVersion.Tag))
})
})
+
+ Context("when handling a CreateComponentInstanceEvent", func() {
+ Context("when new component instance is created", func() {
+ It("should add user resource relationship tuple in openfga", func() {
+ cvFake := test.NewFakeComponentVersionEntity()
+ createEvent := &cv.CreateComponentVersionEvent{
+ ComponentVersion: &cvFake,
+ }
+
+ // Use type assertion to convert a CreateServiceEvent into an Event
+ var event event.Event = createEvent
+ resourceId := strconv.FormatInt(createEvent.ComponentVersion.Id, 10)
+ p.ObjectId = openfga.ObjectId(resourceId)
+ // Simulate event
+ cv.OnComponentVersionCreateAuthz(db, event, handlerContext.Authz)
+
+ ok, err := handlerContext.Authz.CheckPermission(p)
+ Expect(err).To(BeNil(), "no error should be thrown")
+ Expect(ok).To(BeTrue(), "permission should be granted")
+ })
+ })
+ })
})
var _ = Describe("When updating ComponentVersion", Label("app", "UpdateComponentVersion"), func() {
var (
+ er event.EventRegistry
db *mocks.MockDatabase
componenVersionService cv.ComponentVersionHandler
componentVersion entity.ComponentVersionResult
filter *entity.ComponentVersionFilter
- handlerContext common.HandlerContext
)
BeforeEach(func() {
db = mocks.NewMockDatabase(GinkgoT())
+ er = event.NewEventRegistry(db, handlerContext.Authz)
componentVersion = test.NewFakeComponentVersionResult()
first := 10
after := ""
@@ -274,12 +309,8 @@ var _ = Describe("When updating ComponentVersion", Label("app", "UpdateComponent
After: &after,
},
}
- handlerContext = common.HandlerContext{
- DB: db,
- EventReg: er,
- Cache: cache.NewNoCache(),
- Authz: authz,
- }
+ handlerContext.DB = db
+ handlerContext.EventReg = er
})
It("updates componentVersion", func() {
@@ -298,19 +329,66 @@ var _ = Describe("When updating ComponentVersion", Label("app", "UpdateComponent
Expect(updatedComponentVersion.Tag).To(BeEquivalentTo(componentVersion.Tag))
})
})
+
+ Context("when handling an UpdateComponentVersionEvent", func() {
+ It("should update the component relation tuple in openfga", func() {
+ cvFake := test.NewFakeComponentVersionEntity()
+ oldComponentId := int64(12345)
+ newComponentId := int64(67890)
+
+ // Add an initial relation: component_version -> old component
+ initialRelation := openfga.RelationInput{
+ UserType: "component_version",
+ UserId: openfga.UserIdFromInt(cvFake.Id),
+ Relation: "component_version",
+ ObjectType: "component",
+ ObjectId: openfga.ObjectIdFromInt(oldComponentId),
+ }
+ // Bulk add instead of single add
+ handlerContext.Authz.AddRelationBulk([]openfga.RelationInput{initialRelation})
+
+ // Prepare the update event with the new component id
+ cvFake.ComponentId = newComponentId
+ updateEvent := &cv.UpdateComponentVersionEvent{
+ ComponentVersion: &cvFake,
+ }
+ var event event.Event = updateEvent
+
+ // Simulate event
+ cv.OnComponentVersionUpdateAuthz(db, event, handlerContext.Authz)
+
+ // Check that the old relation is gone
+ remainingOld, err := handlerContext.Authz.ListRelations(initialRelation)
+ Expect(err).To(BeNil(), "no error should be thrown")
+ Expect(remainingOld).To(BeEmpty(), "old relation should be removed")
+
+ // Check that the new relation exists
+ newRelation := openfga.RelationInput{
+ UserType: openfga.TypeComponentVersion,
+ UserId: openfga.UserIdFromInt(cvFake.Id),
+ Relation: openfga.RelComponentVersion,
+ ObjectType: openfga.TypeComponent,
+ ObjectId: openfga.ObjectIdFromInt(newComponentId),
+ }
+ remainingNew, err := handlerContext.Authz.ListRelations(newRelation)
+ Expect(err).To(BeNil(), "no error should be thrown")
+ Expect(remainingNew).NotTo(BeEmpty(), "new relation should exist")
+ })
+ })
})
var _ = Describe("When deleting ComponentVersion", Label("app", "DeleteComponentVersion"), func() {
var (
+ er event.EventRegistry
db *mocks.MockDatabase
componenVersionService cv.ComponentVersionHandler
id int64
filter *entity.ComponentVersionFilter
- handlerContext common.HandlerContext
)
BeforeEach(func() {
db = mocks.NewMockDatabase(GinkgoT())
+ er = event.NewEventRegistry(db, handlerContext.Authz)
id = 1
first := 10
after := ""
@@ -320,12 +398,8 @@ var _ = Describe("When deleting ComponentVersion", Label("app", "DeleteComponent
After: &after,
},
}
- handlerContext = common.HandlerContext{
- DB: db,
- EventReg: er,
- Cache: cache.NewNoCache(),
- Authz: authz,
- }
+ handlerContext.DB = db
+ handlerContext.EventReg = er
})
It("deletes componentVersion", func() {
@@ -342,4 +416,103 @@ var _ = Describe("When deleting ComponentVersion", Label("app", "DeleteComponent
Expect(err).To(BeNil(), "no error should be thrown")
Expect(componentVersions.Elements).To(BeEmpty(), "no error should be thrown")
})
+
+ Context("when handling a DeleteComponentVersionEvent", func() {
+ Context("when new component version is deleted", func() {
+ It("should delete tuples related to that component version in openfga", func() {
+ // Test OnComponentVersionDeleteAuthz against all possible relations
+ cvFake := test.NewFakeComponentVersionEntity()
+ deleteEvent := &cv.DeleteComponentVersionEvent{
+ ComponentVersionID: cvFake.Id,
+ }
+ objectId := openfga.ObjectIdFromInt(deleteEvent.ComponentVersionID)
+ userId := openfga.UserIdFromInt(deleteEvent.ComponentVersionID)
+ relations := []openfga.RelationInput{
+ { // user - component_version: a user can view the component version
+ UserType: openfga.TypeUser,
+ UserId: openfga.IDUser,
+ ObjectId: objectId,
+ ObjectType: openfga.TypeComponentVersion,
+ Relation: openfga.RelCanView,
+ },
+ { // component_instance - component_version: a component instance is related to the component version
+ UserType: openfga.TypeComponentInstance,
+ UserId: openfga.IDComponentInstance,
+ ObjectId: objectId,
+ ObjectType: openfga.TypeComponentVersion,
+ Relation: openfga.RelComponentInstance,
+ },
+ { // role - component_version: a role is assigned to the component version
+ UserType: openfga.TypeRole,
+ UserId: openfga.IDRole,
+ ObjectId: objectId,
+ ObjectType: openfga.TypeComponentVersion,
+ Relation: openfga.RelRole,
+ },
+ { // component_version - component: a component version is related to a component
+ UserType: openfga.TypeComponentVersion,
+ UserId: userId,
+ ObjectId: openfga.IDComponent,
+ ObjectType: openfga.TypeComponent,
+ Relation: openfga.RelComponentVersion,
+ },
+ }
+
+ handlerContext.Authz.AddRelationBulk(relations)
+
+ // get the number of relations before deletion
+ relCountBefore := 0
+ for _, r := range relations {
+ relations, err := handlerContext.Authz.ListRelations(r)
+ if err != nil {
+ Expect(err).To(BeNil(), "no error should be thrown")
+ }
+ relCountBefore += len(relations)
+ }
+ Expect(relCountBefore).To(Equal(len(relations)), "all relations should exist before deletion")
+
+ // check that relations were created
+ for _, r := range relations {
+ ok, err := handlerContext.Authz.CheckPermission(openfga.PermissionInput{
+ UserType: r.UserType,
+ UserId: r.UserId,
+ ObjectType: r.ObjectType,
+ ObjectId: r.ObjectId,
+ Relation: r.Relation,
+ })
+ Expect(err).To(BeNil(), "no error should be thrown")
+ Expect(ok).To(BeTrue(), "permission should be granted")
+ }
+
+ var event event.Event = deleteEvent
+ // Simulate event
+ cv.OnComponentVersionDeleteAuthz(db, event, handlerContext.Authz)
+
+ // get the number of relations after deletion
+ relCountAfter := 0
+ for _, r := range relations {
+ relations, err := handlerContext.Authz.ListRelations(r)
+ if err != nil {
+ Expect(err).To(BeNil(), "no error should be thrown")
+ }
+ relCountAfter += len(relations)
+ }
+ Expect(relCountAfter < relCountBefore).To(BeTrue(), "less relations after deletion")
+ Expect(relCountAfter).To(BeEquivalentTo(0), "no relations should exist after deletion")
+
+ // verify that relations were deleted
+ for _, r := range relations {
+ ok, err := handlerContext.Authz.CheckPermission(openfga.PermissionInput{
+ UserType: r.UserType,
+ UserId: r.UserId,
+ ObjectType: r.ObjectType,
+ ObjectId: r.ObjectId,
+ Relation: r.Relation,
+ })
+ Expect(err).To(BeNil(), "no error should be thrown")
+ Expect(ok).To(BeFalse(), "permission should NOT be granted")
+ }
+ })
+ })
+ })
})
diff --git a/internal/app/event/event_registry.go b/internal/app/event/event_registry.go
index 272c8fd8..83ad1f45 100644
--- a/internal/app/event/event_registry.go
+++ b/internal/app/event/event_registry.go
@@ -8,16 +8,18 @@ import (
"sync"
"github.com/cloudoperators/heureka/internal/database"
+ "github.com/cloudoperators/heureka/internal/openfga"
+ "github.com/cloudoperators/heureka/internal/util"
)
type EventHandler interface {
- HandleEvent(database.Database, Event)
+ HandleEvent(database.Database, Event, openfga.Authorization)
}
-type EventHandlerFunc func(database.Database, Event)
+type EventHandlerFunc func(database.Database, Event, openfga.Authorization)
-func (f EventHandlerFunc) HandleEvent(db database.Database, e Event) {
- f(db, e)
+func (f EventHandlerFunc) HandleEvent(db database.Database, e Event, authz openfga.Authorization) {
+ f(db, e, authz)
}
type EventRegistry interface {
@@ -33,6 +35,8 @@ type eventRegistry struct {
wg sync.WaitGroup
mu sync.Mutex
workerCount int
+ authz openfga.Authorization
+ cfg *util.Config
}
func (er *eventRegistry) RegisterEventHandler(event EventName, handler EventHandler) {
@@ -86,7 +90,7 @@ func (er *eventRegistry) PushEvent(event Event) {
}
}
-func NewEventRegistry(db database.Database) EventRegistry {
+func NewEventRegistry(db database.Database, authz openfga.Authorization) EventRegistry {
initialBufferSize := 1024 // Start with a larger buffer
workerCount := 4
er := &eventRegistry{
@@ -94,6 +98,7 @@ func NewEventRegistry(db database.Database) EventRegistry {
ch: make(chan Event, initialBufferSize),
db: db,
workerCount: workerCount,
+ authz: authz,
}
return er
@@ -138,6 +143,6 @@ func (er *eventRegistry) processEvent(event Event) {
er.mu.Unlock()
for _, handler := range handlers {
- handler.HandleEvent(er.db, event)
+ handler.HandleEvent(er.db, event, er.authz)
}
}
diff --git a/internal/app/event/event_registry_test.go b/internal/app/event/event_registry_test.go
index dab3c1a4..9128de3e 100644
--- a/internal/app/event/event_registry_test.go
+++ b/internal/app/event/event_registry_test.go
@@ -10,11 +10,13 @@ import (
"time"
"fmt"
+ "sync"
+
"github.com/cloudoperators/heureka/internal/database"
"github.com/cloudoperators/heureka/internal/mocks"
+ "github.com/cloudoperators/heureka/internal/openfga"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
- "sync"
)
func TestEventRegistry(t *testing.T) {
@@ -40,11 +42,12 @@ var _ = Describe("EventRegistry", Label("app", "event", "EventRegistry"), func()
db *mocks.MockDatabase
ctx context.Context
cancel context.CancelFunc
+ authz openfga.Authorization
)
BeforeEach(func() {
db = mocks.NewMockDatabase(GinkgoT())
- er = NewEventRegistry(db)
+ er = NewEventRegistry(db, authz)
ctx, cancel = context.WithCancel(context.Background())
})
@@ -54,7 +57,7 @@ var _ = Describe("EventRegistry", Label("app", "event", "EventRegistry"), func()
It("should register and handle events", func() {
var eventHandled int32
- handler := func(db database.Database, e Event) {
+ handler := func(db database.Database, e Event, authz openfga.Authorization) {
atomic.AddInt32(&eventHandled, 1)
}
@@ -69,7 +72,7 @@ var _ = Describe("EventRegistry", Label("app", "event", "EventRegistry"), func()
It("should handle multiple events", func() {
var eventHandled int32
- handler := func(db database.Database, e Event) {
+ handler := func(db database.Database, e Event, authz openfga.Authorization) {
atomic.AddInt32(&eventHandled, 1)
}
er.Run(ctx)
@@ -86,7 +89,7 @@ var _ = Describe("EventRegistry", Label("app", "event", "EventRegistry"), func()
const numEvents = 10000
var eventHandled int64
- handler := func(db database.Database, e Event) {
+ handler := func(db database.Database, e Event, authz openfga.Authorization) {
time.Sleep(2 * time.Millisecond) //important to ensure that the events are shortly blocking and force channel resizing
atomic.AddInt64(&eventHandled, 1)
}
@@ -129,7 +132,7 @@ var _ = Describe("EventRegistry", Label("app", "event", "EventRegistry"), func()
const numEvents = 150 // More than channel capacity
var eventHandled int64
- handler := func(db database.Database, e Event) {
+ handler := func(db database.Database, e Event, authz openfga.Authorization) {
time.Sleep(10 * time.Millisecond) // Slow handler
atomic.AddInt64(&eventHandled, 1)
}
@@ -163,7 +166,7 @@ var _ = Describe("EventRegistry", Label("app", "event", "EventRegistry"), func()
It("should handle multiple times when multiple handlers are registered", func() {
var eventHandled int32
- handler := func(db database.Database, e Event) {
+ handler := func(db database.Database, e Event, authz openfga.Authorization) {
atomic.AddInt32(&eventHandled, 1)
}
@@ -181,7 +184,7 @@ var _ = Describe("EventRegistry", Label("app", "event", "EventRegistry"), func()
It("should stop processing on context cancel", func() {
var eventHandled int32
- handler := func(db database.Database, e Event) {
+ handler := func(db database.Database, e Event, authz openfga.Authorization) {
atomic.AddInt32(&eventHandled, 1)
}
diff --git a/internal/app/evidence/evidence_handler_test.go b/internal/app/evidence/evidence_handler_test.go
index 30128614..73a219f6 100644
--- a/internal/app/evidence/evidence_handler_test.go
+++ b/internal/app/evidence/evidence_handler_test.go
@@ -30,7 +30,7 @@ var authz openfga.Authorization
var _ = BeforeSuite(func() {
db := mocks.NewMockDatabase(GinkgoT())
- er = event.NewEventRegistry(db)
+ er = event.NewEventRegistry(db, authz)
})
func evidenceFilter() *entity.EvidenceFilter {
diff --git a/internal/app/heureka.go b/internal/app/heureka.go
index d6cf4239..d7c771df 100644
--- a/internal/app/heureka.go
+++ b/internal/app/heureka.go
@@ -71,7 +71,7 @@ func NewHeurekaApp(ctx context.Context, wg *sync.WaitGroup, db database.Database
profiler := profiler.NewProfiler(cfg.CpuProfilerFilePath)
profiler.Start()
- er := event.NewEventRegistry(db)
+ er := event.NewEventRegistry(db, authz)
handlerContext := common.HandlerContext{
DB: db,
@@ -112,6 +112,7 @@ func NewHeurekaApp(ctx context.Context, wg *sync.WaitGroup, db database.Database
}
heureka.SubscribeHandlers()
+ heureka.SubscribeAuthzHandlers()
return heureka
}
@@ -140,29 +141,58 @@ func NewAppCache(ctx context.Context, wg *sync.WaitGroup, cfg util.Config) cache
}
func (h *HeurekaApp) SubscribeHandlers() {
- // Event handlers for Components
- h.eventRegistry.RegisterEventHandler(
- component_instance.CreateComponentInstanceEventName,
- event.EventHandlerFunc(issue_match.OnComponentInstanceCreate),
- )
-
- // Event handlers for Services
- h.eventRegistry.RegisterEventHandler(
- service.CreateServiceEventName,
- event.EventHandlerFunc(service.OnServiceCreate),
- )
-
- // Event handlers for IssueRepositories
- h.eventRegistry.RegisterEventHandler(
- issue_repository.CreateIssueRepositoryEventName,
- event.EventHandlerFunc(issue_repository.OnIssueRepositoryCreate),
- )
-
- // Event handlers for ComponentVersion attachments to Issues
- h.eventRegistry.RegisterEventHandler(
- issue.AddComponentVersionToIssueEventName,
- event.EventHandlerFunc(issue.OnComponentVersionAttachmentToIssue),
- )
+ // Register handlers to follow up on create events and link entities together
+ handlers := []struct {
+ eventName event.EventName
+ handler event.EventHandlerFunc
+ }{
+ {component_instance.CreateComponentInstanceEventName, event.EventHandlerFunc(issue_match.OnComponentInstanceCreate)},
+ {service.CreateServiceEventName, event.EventHandlerFunc(service.OnServiceCreate)},
+ {issue_repository.CreateIssueRepositoryEventName, event.EventHandlerFunc(issue_repository.OnIssueRepositoryCreate)},
+ {issue.AddComponentVersionToIssueEventName, event.EventHandlerFunc(issue.OnComponentVersionAttachmentToIssue)},
+ }
+
+ for _, hdl := range handlers {
+ h.eventRegistry.RegisterEventHandler(hdl.eventName, hdl.handler)
+ }
+}
+
+func (h *HeurekaApp) SubscribeAuthzHandlers() {
+ // Register handlers to update, create, and delete authz relations in openfga
+ authzHandlers := []struct {
+ eventName event.EventName
+ handler event.EventHandlerFunc
+ }{
+ // Create events
+ {service.CreateServiceEventName, event.EventHandlerFunc(service.OnServiceCreateAuthz)},
+ {component_instance.CreateComponentInstanceEventName, event.EventHandlerFunc(component_instance.OnComponentInstanceCreateAuthz)},
+ {component_version.CreateComponentVersionEventName, event.EventHandlerFunc(component_version.OnComponentVersionCreateAuthz)},
+ {support_group.CreateSupportGroupEventName, event.EventHandlerFunc(support_group.OnSupportGroupCreateAuthz)},
+ {component.CreateComponentEventName, event.EventHandlerFunc(component.OnComponentCreateAuthz)},
+ {issue_match.CreateIssueMatchEventName, event.EventHandlerFunc(issue_match.OnIssueMatchCreateAuthz)},
+ // Delete events
+ {user.DeleteUserEventName, event.EventHandlerFunc(user.OnUserDeleteAuthz)},
+ {service.DeleteServiceEventName, event.EventHandlerFunc(service.OnServiceDeleteAuthz)},
+ {component_instance.DeleteComponentInstanceEventName, event.EventHandlerFunc(component_instance.OnComponentInstanceDeleteAuthz)},
+ {component_version.DeleteComponentVersionEventName, event.EventHandlerFunc(component_version.OnComponentVersionDeleteAuthz)},
+ {support_group.DeleteSupportGroupEventName, event.EventHandlerFunc(support_group.OnSupportGroupDeleteAuthz)},
+ {component.DeleteComponentEventName, event.EventHandlerFunc(component.OnComponentDeleteAuthz)},
+ {issue_match.DeleteIssueMatchEventName, event.EventHandlerFunc(issue_match.OnIssueMatchDeleteAuthz)},
+ // Update events
+ {component_version.UpdateComponentVersionEventName, event.EventHandlerFunc(component_version.OnComponentVersionUpdateAuthz)},
+ {issue_match.UpdateIssueMatchEventName, event.EventHandlerFunc(issue_match.OnIssueMatchUpdateAuthz)},
+ {component_instance.UpdateComponentInstanceEventName, event.EventHandlerFunc(component_instance.OnComponentInstanceUpdateAuthz)},
+ {support_group.AddServiceToSupportGroupEventName, event.EventHandlerFunc(support_group.OnAddServiceToSupportGroup)},
+ {support_group.RemoveServiceFromSupportGroupEventName, event.EventHandlerFunc(support_group.OnRemoveServiceFromSupportGroup)},
+ {support_group.AddUserToSupportGroupEventName, event.EventHandlerFunc(support_group.OnAddUserToSupportGroup)},
+ {support_group.RemoveUserFromSupportGroupEventName, event.EventHandlerFunc(support_group.OnRemoveUserFromSupportGroup)},
+ {service.AddOwnerToServiceEventName, event.EventHandlerFunc(service.OnAddOwnerToService)},
+ {service.RemoveOwnerFromServiceEventName, event.EventHandlerFunc(service.OnRemoveOwnerFromService)},
+ }
+
+ for _, handler := range authzHandlers {
+ h.eventRegistry.RegisterEventHandler(handler.eventName, handler.handler)
+ }
}
func (h *HeurekaApp) Shutdown() error {
diff --git a/internal/app/issue/issue_handler_events.go b/internal/app/issue/issue_handler_events.go
index c3b5a250..7f3ea0aa 100644
--- a/internal/app/issue/issue_handler_events.go
+++ b/internal/app/issue/issue_handler_events.go
@@ -11,6 +11,7 @@ import (
"github.com/cloudoperators/heureka/internal/app/shared"
"github.com/cloudoperators/heureka/internal/database"
"github.com/cloudoperators/heureka/internal/entity"
+ "github.com/cloudoperators/heureka/internal/openfga"
"github.com/sirupsen/logrus"
)
@@ -108,7 +109,7 @@ func (e *GetIssueSeverityCountsEvent) Name() event.EventName {
// OnComponentVersionAttachmentToIssue is an event handler whenever a ComponentVersion
// is attached to an Issue.
-func OnComponentVersionAttachmentToIssue(db database.Database, e event.Event) {
+func OnComponentVersionAttachmentToIssue(db database.Database, e event.Event, authz openfga.Authorization) {
l := logrus.WithFields(logrus.Fields{
"event": "OnComponentVersionAttachmentToIssue",
"payload": e,
diff --git a/internal/app/issue/issue_handler_events_test.go b/internal/app/issue/issue_handler_events_test.go
index 98ac0d3f..594289f9 100644
--- a/internal/app/issue/issue_handler_events_test.go
+++ b/internal/app/issue/issue_handler_events_test.go
@@ -4,6 +4,7 @@ package issue_test
import (
"github.com/cloudoperators/heureka/internal/app/issue"
+ "github.com/cloudoperators/heureka/internal/openfga"
"github.com/cloudoperators/heureka/internal/entity"
"github.com/cloudoperators/heureka/internal/entity/test"
@@ -37,6 +38,7 @@ var _ = Describe("OnComponentVersionAttachmentToIssue", Label("app", "ComponentV
issueVariant entity.IssueVariant
serviceIssueVariant entity.ServiceIssueVariant
event *issue.AddComponentVersionToIssueEvent
+ authz openfga.Authorization
)
BeforeEach(func() {
@@ -106,7 +108,7 @@ var _ = Describe("OnComponentVersionAttachmentToIssue", Label("app", "ComponentV
db.On("CreateIssueMatch", matchIssueMatch(expectedMatch)).Return(expectedMatch, nil)
// Emit event
- issue.OnComponentVersionAttachmentToIssue(db, event)
+ issue.OnComponentVersionAttachmentToIssue(db, event, authz)
// Assert expectations
db.AssertExpectations(GinkgoT())
@@ -125,7 +127,7 @@ var _ = Describe("OnComponentVersionAttachmentToIssue", Label("app", "ComponentV
IssueId: []*int64{&issueEntity.Id},
}, []entity.Order{}).Return([]entity.IssueMatchResult{existingMatch}, nil)
- issue.OnComponentVersionAttachmentToIssue(db, event)
+ issue.OnComponentVersionAttachmentToIssue(db, event, authz)
db.AssertNotCalled(GinkgoT(), "CreateIssueMatch", mock.Anything)
})
})
diff --git a/internal/app/issue/issue_handler_test.go b/internal/app/issue/issue_handler_test.go
index 0c2ddeff..54d67d91 100644
--- a/internal/app/issue/issue_handler_test.go
+++ b/internal/app/issue/issue_handler_test.go
@@ -37,7 +37,7 @@ var authz openfga.Authorization
var _ = BeforeSuite(func() {
db := mocks.NewMockDatabase(GinkgoT())
- er = event.NewEventRegistry(db)
+ er = event.NewEventRegistry(db, authz)
})
var _ = Describe("When getting a single Issue", Label("app", "GetIssue", "errors"), func() {
diff --git a/internal/app/issue_match/issue_match_handler_events.go b/internal/app/issue_match/issue_match_handler_events.go
index a82baf77..88dd6ef7 100644
--- a/internal/app/issue_match/issue_match_handler_events.go
+++ b/internal/app/issue_match/issue_match_handler_events.go
@@ -4,6 +4,7 @@
package issue_match
import (
+ "strconv"
"time"
"github.com/cloudoperators/heureka/internal/app/component_instance"
@@ -11,6 +12,8 @@ import (
"github.com/cloudoperators/heureka/internal/app/shared"
"github.com/cloudoperators/heureka/internal/database"
"github.com/cloudoperators/heureka/internal/entity"
+ appErrors "github.com/cloudoperators/heureka/internal/errors"
+ "github.com/cloudoperators/heureka/internal/openfga"
"github.com/sirupsen/logrus"
)
@@ -86,7 +89,7 @@ func (e *RemoveEvidenceFromIssueMatchEvent) Name() event.EventName {
return RemoveEvidenceFromIssueMatchEventName
}
-func OnComponentInstanceCreate(db database.Database, event event.Event) {
+func OnComponentInstanceCreate(db database.Database, event event.Event, authz openfga.Authorization) {
if createEvent, ok := event.(*component_instance.CreateComponentInstanceEvent); ok {
OnComponentVersionAssignmentToComponentInstance(db, createEvent.ComponentInstance.Id, createEvent.ComponentInstance.ComponentVersionId)
}
@@ -209,3 +212,125 @@ func OnComponentVersionAssignmentToComponentInstance(db database.Database, compo
}
}
}
+
+// OnIssueMatchCreateAuthz is a handler for the CreateIssueMatchEvent
+// It creates an OpenFGA relation tuple for the issue match and the current user
+func OnIssueMatchCreateAuthz(db database.Database, e event.Event, authz openfga.Authorization) {
+ op := appErrors.Op("OnIssueMatchCreateAuthz")
+
+ l := logrus.WithFields(logrus.Fields{
+ "event": "OnIssueMatchCreateAuthz",
+ "payload": e,
+ })
+
+ if createEvent, ok := e.(*CreateIssueMatchEvent); ok {
+ userId := openfga.UserIdFromInt(createEvent.IssueMatch.CreatedBy)
+
+ relations := []openfga.RelationInput{
+ {
+ UserType: openfga.TypeRole,
+ UserId: userId,
+ Relation: openfga.RelRole,
+ ObjectType: openfga.TypeIssueMatch,
+ ObjectId: openfga.ObjectIdFromInt(createEvent.IssueMatch.Id),
+ },
+ }
+
+ err := authz.AddRelationBulk(relations)
+ if err != nil {
+ wrappedErr := appErrors.InternalError(string(op), "IssueMatch", "", err)
+ l.Error(wrappedErr)
+ }
+ } else {
+ err := NewIssueMatchHandlerError("OnIssueMatchCreateAuthz: triggered with wrong event type")
+ wrappedErr := appErrors.InternalError(string(op), "IssueMatch", "", err)
+ l.Error(wrappedErr)
+ }
+}
+
+// OnIssueMatchUpdateAuthz is a handler for the UpdateIssueMatchEvent
+// Fields that can be updated in Issue Match which affect tuple relations include:
+// issuematch_component_instance_id
+func OnIssueMatchUpdateAuthz(db database.Database, e event.Event, authz openfga.Authorization) {
+ op := appErrors.Op("OnIssueMatchUpdateAuthz")
+
+ l := logrus.WithFields(logrus.Fields{
+ "event": "OnIssueMatchUpdateAuthz",
+ "payload": e,
+ })
+
+ if updateEvent, ok := e.(*UpdateIssueMatchEvent); ok {
+ newComponentInstanceId := strconv.FormatInt(updateEvent.IssueMatch.ComponentInstanceId, 10)
+
+ if newComponentInstanceId != "" {
+ // Remove any existing relation where this issue_match is connected to any component_instance
+ removeInput := openfga.RelationInput{
+ UserType: openfga.TypeComponentInstance,
+ Relation: openfga.TypeComponentInstance,
+ ObjectType: openfga.TypeIssueMatch,
+ ObjectId: openfga.ObjectIdFromInt(updateEvent.IssueMatch.Id),
+ }
+ err := authz.RemoveRelationBulk([]openfga.RelationInput{removeInput})
+ if err != nil {
+ wrappedErr := appErrors.InternalError(string(op), "IssueMatch", "", err)
+ l.Error(wrappedErr)
+ }
+
+ // Add the new relation to the new component_instance
+ newRelation := openfga.RelationInput{
+ UserType: openfga.TypeComponentInstance,
+ UserId: openfga.UserId(newComponentInstanceId),
+ Relation: openfga.TypeComponentInstance,
+ ObjectType: openfga.TypeIssueMatch,
+ ObjectId: openfga.ObjectIdFromInt(updateEvent.IssueMatch.Id),
+ }
+
+ err = authz.AddRelationBulk([]openfga.RelationInput{newRelation})
+ if err != nil {
+ wrappedErr := appErrors.InternalError(string(op), "IssueMatch", "", err)
+ l.Error(wrappedErr)
+ }
+ }
+ } else {
+ err := NewIssueMatchHandlerError("OnIssueMatchUpdateAuthz: triggered with wrong event type")
+ wrappedErr := appErrors.InternalError(string(op), "IssueMatch", "", err)
+ l.Error(wrappedErr)
+ }
+}
+
+// OnIssueMatchDeleteAuthz is a handler for the DeleteIssueMatchEvent
+func OnIssueMatchDeleteAuthz(db database.Database, e event.Event, authz openfga.Authorization) {
+ op := appErrors.Op("OnIssueMatchDeleteAuthz")
+
+ deleteInput := []openfga.RelationInput{}
+
+ l := logrus.WithFields(logrus.Fields{
+ "event": "OnIssueMatchDeleteAuthz",
+ "payload": e,
+ })
+
+ if deleteEvent, ok := e.(*DeleteIssueMatchEvent); ok {
+ // Delete all tuples where object is the issue_match
+ deleteInput = append(deleteInput, openfga.RelationInput{
+ ObjectType: openfga.TypeIssueMatch,
+ ObjectId: openfga.ObjectIdFromInt(deleteEvent.IssueMatchID),
+ })
+
+ // Delete all tuples where user is the issue_match
+ // deleteInput = append(deleteInput, openfga.RelationInput{
+ // UserType: openfga.TypeIssueMatch,
+ // UserId: openfga.UserIdFromInt(deleteEvent.IssueMatchID),
+ // ObjectType: ,
+ // })
+
+ err := authz.RemoveRelationBulk(deleteInput)
+ if err != nil {
+ wrappedErr := appErrors.InternalError(string(op), "IssueMatch", "", err)
+ l.Error(wrappedErr)
+ }
+ } else {
+ err := NewIssueMatchHandlerError("OnIssueMatchDeleteAuthz: triggered with wrong event type")
+ wrappedErr := appErrors.InternalError(string(op), "IssueMatch", "", err)
+ l.Error(wrappedErr)
+ }
+}
diff --git a/internal/app/issue_match/issue_match_handler_test.go b/internal/app/issue_match/issue_match_handler_test.go
index ccf5de4c..22b43b4b 100644
--- a/internal/app/issue_match/issue_match_handler_test.go
+++ b/internal/app/issue_match/issue_match_handler_test.go
@@ -17,6 +17,7 @@ import (
"github.com/cloudoperators/heureka/internal/app/severity"
"github.com/cloudoperators/heureka/internal/database/mariadb"
"github.com/cloudoperators/heureka/internal/openfga"
+ "github.com/cloudoperators/heureka/internal/util"
"github.com/samber/lo"
@@ -34,12 +35,17 @@ func TestIssueMatchHandler(t *testing.T) {
RunSpecs(t, "IssueMatch Service Test Suite")
}
-var er event.EventRegistry
-var authz openfga.Authorization
+var handlerContext common.HandlerContext
+var cfg *util.Config
var _ = BeforeSuite(func() {
- db := mocks.NewMockDatabase(GinkgoT())
- er = event.NewEventRegistry(db)
+ cfg = common.GetTestConfig()
+ enableLogs := false
+ authz := openfga.NewAuthorizationHandler(cfg, enableLogs)
+ handlerContext = common.HandlerContext{
+ Cache: cache.NewNoCache(),
+ Authz: authz,
+ }
})
func getIssueMatchFilter() *entity.IssueMatchFilter {
@@ -60,23 +66,20 @@ func getIssueMatchFilter() *entity.IssueMatchFilter {
var _ = Describe("When listing IssueMatches", Label("app", "ListIssueMatches"), func() {
var (
+ er event.EventRegistry
db *mocks.MockDatabase
issueMatchHandler im.IssueMatchHandler
filter *entity.IssueMatchFilter
options *entity.ListOptions
- handlerContext common.HandlerContext
)
BeforeEach(func() {
db = mocks.NewMockDatabase(GinkgoT())
+ er = event.NewEventRegistry(db, handlerContext.Authz)
options = entity.NewListOptions()
filter = getIssueMatchFilter()
- handlerContext = common.HandlerContext{
- DB: db,
- EventReg: er,
- Cache: cache.NewNoCache(),
- Authz: authz,
- }
+ handlerContext.DB = db
+ handlerContext.EventReg = er
})
When("the list option does include the totalCount", func() {
@@ -187,6 +190,7 @@ var _ = Describe("When listing IssueMatches", Label("app", "ListIssueMatches"),
var _ = Describe("When creating IssueMatch", Label("app", "CreateIssueMatch"), func() {
var (
+ er event.EventRegistry
db *mocks.MockDatabase
issueMatchHandler im.IssueMatchHandler
issueMatch entity.IssueMatch
@@ -197,11 +201,12 @@ var _ = Describe("When creating IssueMatch", Label("app", "CreateIssueMatch"), f
ss severity.SeverityHandler
ivs issue_variant.IssueVariantHandler
rs issue_repository.IssueRepositoryHandler
- handlerContext common.HandlerContext
+ p openfga.PermissionInput
)
BeforeEach(func() {
db = mocks.NewMockDatabase(GinkgoT())
+ er = event.NewEventRegistry(db, handlerContext.Authz)
issueMatch = test.NewFakeIssueMatch()
ivFilter = entity.NewIssueVariantFilter()
irFilter = entity.NewIssueRepositoryFilter()
@@ -211,12 +216,18 @@ var _ = Describe("When creating IssueMatch", Label("app", "CreateIssueMatch"), f
ivFilter.After = &after
irFilter.First = &first
irFilter.After = &after
- handlerContext = common.HandlerContext{
- DB: db,
- EventReg: er,
- Cache: cache.NewNoCache(),
- Authz: authz,
+
+ p = openfga.PermissionInput{
+ UserType: openfga.TypeRole,
+ UserId: "0",
+ ObjectId: "test_issue_match",
+ ObjectType: openfga.TypeIssueMatch,
+ Relation: openfga.TypeRole,
}
+
+ handlerContext.DB = db
+ handlerContext.EventReg = er
+
rs = issue_repository.NewIssueRepositoryHandler(handlerContext)
ivs = issue_variant.NewIssueVariantHandler(handlerContext, rs)
ss = severity.NewSeverityHandler(handlerContext, ivs)
@@ -249,19 +260,42 @@ var _ = Describe("When creating IssueMatch", Label("app", "CreateIssueMatch"), f
Expect(newIssueMatch.Severity.Value).To(BeEquivalentTo(issueMatch.Severity.Value))
})
})
+
+ Context("when handling a CreateIssueMatchEvent", func() {
+ Context("when new issue match is created", func() {
+ It("should add user resource relationship tuple in openfga", func() {
+ imFake := test.NewFakeIssueMatch()
+ createEvent := &im.CreateIssueMatchEvent{
+ IssueMatch: &imFake,
+ }
+
+ // Use type assertion to convert a CreateServiceEvent into an Event
+ var event event.Event = createEvent
+ p.ObjectId = openfga.ObjectIdFromInt(createEvent.IssueMatch.Id)
+ // Simulate event
+ im.OnIssueMatchCreateAuthz(db, event, handlerContext.Authz)
+
+ ok, err := handlerContext.Authz.CheckPermission(p)
+ Expect(err).To(BeNil(), "no error should be thrown")
+ Expect(ok).To(BeTrue(), "permission should be granted")
+ })
+ })
+ })
})
var _ = Describe("When updating IssueMatch", Label("app", "UpdateIssueMatch"), func() {
var (
+ er event.EventRegistry
db *mocks.MockDatabase
issueMatchHandler im.IssueMatchHandler
issueMatch entity.IssueMatchResult
filter *entity.IssueMatchFilter
- handlerContext common.HandlerContext
)
BeforeEach(func() {
db = mocks.NewMockDatabase(GinkgoT())
+ er = event.NewEventRegistry(db, handlerContext.Authz)
+
issueMatch = test.NewFakeIssueMatchResult()
first := 10
after := ""
@@ -271,12 +305,8 @@ var _ = Describe("When updating IssueMatch", Label("app", "UpdateIssueMatch"), f
After: &after,
},
}
- handlerContext = common.HandlerContext{
- DB: db,
- EventReg: er,
- Cache: cache.NewNoCache(),
- Authz: authz,
- }
+ handlerContext.DB = db
+ handlerContext.EventReg = er
})
It("updates issueMatch", func() {
@@ -304,20 +334,68 @@ var _ = Describe("When updating IssueMatch", Label("app", "UpdateIssueMatch"), f
Expect(updatedIssueMatch.Severity.Value).To(BeEquivalentTo(issueMatch.Severity.Value))
})
})
+
+ Context("when handling an UpdateIssueMatchEvent", func() {
+ It("should update the component_instance relation tuple in openfga", func() {
+ imFake := test.NewFakeIssueMatch()
+ oldComponentInstanceId := int64(12345)
+ newComponentInstanceId := int64(67890)
+
+ // Add an initial relation: issue_match -> old component_instance
+ initialRelation := openfga.RelationInput{
+ UserType: openfga.TypeComponentInstance,
+ UserId: openfga.UserIdFromInt(oldComponentInstanceId),
+ Relation: openfga.RelComponentInstance,
+ ObjectType: openfga.TypeIssueMatch,
+ ObjectId: openfga.ObjectIdFromInt(imFake.Id),
+ }
+
+ handlerContext.Authz.AddRelationBulk([]openfga.RelationInput{initialRelation})
+
+ // Prepare the update event with the new component_instance id
+ imFake.ComponentInstanceId = newComponentInstanceId
+ updateEvent := &im.UpdateIssueMatchEvent{
+ IssueMatch: &imFake,
+ }
+ var event event.Event = updateEvent
+
+ // Simulate event
+ im.OnIssueMatchUpdateAuthz(db, event, handlerContext.Authz)
+
+ // Check that the old relation is gone
+ remainingOld, err := handlerContext.Authz.ListRelations(initialRelation)
+ Expect(err).To(BeNil(), "no error should be thrown")
+ Expect(remainingOld).To(BeEmpty(), "old relation should be removed")
+
+ // Check that the new relation exists
+ newRelation := openfga.RelationInput{
+ UserType: openfga.TypeComponentInstance,
+ UserId: openfga.UserIdFromInt(newComponentInstanceId),
+ Relation: openfga.RelComponentInstance,
+ ObjectType: openfga.TypeIssueMatch,
+ ObjectId: openfga.ObjectIdFromInt(imFake.Id),
+ }
+ remainingNew, err := handlerContext.Authz.ListRelations(newRelation)
+ Expect(err).To(BeNil(), "no error should be thrown")
+ Expect(remainingNew).NotTo(BeEmpty(), "new relation should exist")
+ })
+ })
})
var _ = Describe("When deleting IssueMatch", Label("app", "DeleteIssueMatch"), func() {
var (
+ er event.EventRegistry
db *mocks.MockDatabase
issueMatchHandler im.IssueMatchHandler
id int64
filter *entity.IssueMatchFilter
options *entity.ListOptions
- handlerContext common.HandlerContext
)
BeforeEach(func() {
db = mocks.NewMockDatabase(GinkgoT())
+ er = event.NewEventRegistry(db, handlerContext.Authz)
+
id = 1
first := 10
after := ""
@@ -328,12 +406,8 @@ var _ = Describe("When deleting IssueMatch", Label("app", "DeleteIssueMatch"), f
},
}
options = entity.NewListOptions()
- handlerContext = common.HandlerContext{
- DB: db,
- EventReg: er,
- Cache: cache.NewNoCache(),
- Authz: authz,
- }
+ handlerContext.DB = db
+ handlerContext.EventReg = er
})
It("deletes issueMatch", func() {
@@ -349,20 +423,113 @@ var _ = Describe("When deleting IssueMatch", Label("app", "DeleteIssueMatch"), f
Expect(err).To(BeNil(), "no error should be thrown")
Expect(issueMatches.Elements).To(BeEmpty(), "no error should be thrown")
})
+
+ Context("when handling a DeleteIssueMatchEvent", func() {
+ Context("when new issue match is deleted", func() {
+ It("should delete tuples related to that issuematch in openfga", func() {
+ // Test OnIssueMatchDeleteAuthz against all possible relations
+ imFake := test.NewFakeIssueMatch()
+ deleteEvent := &im.DeleteIssueMatchEvent{
+ IssueMatchID: imFake.Id,
+ }
+ objectId := openfga.ObjectIdFromInt(deleteEvent.IssueMatchID)
+ relations := []openfga.RelationInput{
+ { // user - issue_match: a user can view the issue match
+ UserType: openfga.TypeUser,
+ UserId: openfga.IDUser,
+ ObjectId: objectId,
+ ObjectType: openfga.TypeIssueMatch,
+ Relation: openfga.RelCanView,
+ },
+ { // component_instance - issue_match: a component instance is related to the issue match
+ UserType: openfga.TypeComponentInstance,
+ UserId: openfga.IDComponentInstance,
+ ObjectId: objectId,
+ ObjectType: openfga.TypeIssueMatch,
+ Relation: openfga.RelComponentInstance,
+ },
+ { // role - issue_match: a role is assigned to the issue match
+ UserType: openfga.TypeRole,
+ UserId: openfga.IDRole,
+ ObjectId: objectId,
+ ObjectType: openfga.TypeIssueMatch,
+ Relation: openfga.RelRole,
+ },
+ }
+
+ handlerContext.Authz.AddRelationBulk(relations)
+
+ // get the number of relations before deletion
+ relCountBefore := 0
+ for _, r := range relations {
+ relations, err := handlerContext.Authz.ListRelations(r)
+ if err != nil {
+ Expect(err).To(BeNil(), "no error should be thrown")
+ }
+ relCountBefore += len(relations)
+ }
+ Expect(relCountBefore).To(Equal(len(relations)), "all relations should exist before deletion")
+
+ // check that relations were created
+ for _, r := range relations {
+ ok, err := handlerContext.Authz.CheckPermission(openfga.PermissionInput{
+ UserType: r.UserType,
+ UserId: r.UserId,
+ ObjectType: r.ObjectType,
+ ObjectId: r.ObjectId,
+ Relation: r.Relation,
+ })
+ Expect(err).To(BeNil(), "no error should be thrown")
+ Expect(ok).To(BeTrue(), "permission should be granted")
+ }
+
+ var event event.Event = deleteEvent
+ // Simulate event
+ im.OnIssueMatchDeleteAuthz(db, event, handlerContext.Authz)
+
+ // get the number of relations after deletion
+ relCountAfter := 0
+ for _, r := range relations {
+ relations, err := handlerContext.Authz.ListRelations(r)
+ if err != nil {
+ Expect(err).To(BeNil(), "no error should be thrown")
+ }
+ relCountAfter += len(relations)
+ }
+ Expect(relCountAfter < relCountBefore).To(BeTrue(), "less relations after deletion")
+ Expect(relCountAfter).To(BeEquivalentTo(0), "no relations should exist after deletion")
+
+ // verify that relations were deleted
+ for _, r := range relations {
+ ok, err := handlerContext.Authz.CheckPermission(openfga.PermissionInput{
+ UserType: r.UserType,
+ UserId: r.UserId,
+ ObjectType: r.ObjectType,
+ ObjectId: r.ObjectId,
+ Relation: r.Relation,
+ })
+ Expect(err).To(BeNil(), "no error should be thrown")
+ Expect(ok).To(BeFalse(), "permission should NOT be granted")
+ }
+ })
+ })
+ })
})
var _ = Describe("When modifying relationship of evidence and issueMatch", Label("app", "EvidenceIssueMatchRelationship"), func() {
var (
+ er event.EventRegistry
db *mocks.MockDatabase
issueMatchHandler im.IssueMatchHandler
evidence entity.Evidence
issueMatch entity.IssueMatchResult
filter *entity.IssueMatchFilter
- handlerContext common.HandlerContext
)
BeforeEach(func() {
db = mocks.NewMockDatabase(GinkgoT())
+ er = event.NewEventRegistry(db, handlerContext.Authz)
+
issueMatch = test.NewFakeIssueMatchResult()
evidence = test.NewFakeEvidenceEntity()
first := 10
@@ -374,12 +541,8 @@ var _ = Describe("When modifying relationship of evidence and issueMatch", Label
},
Id: []*int64{&issueMatch.Id},
}
- handlerContext = common.HandlerContext{
- DB: db,
- EventReg: er,
- Cache: cache.NewNoCache(),
- Authz: authz,
- }
+ handlerContext.DB = db
+ handlerContext.EventReg = er
})
It("adds evidence to issueMatch", func() {
diff --git a/internal/app/issue_match_change/issue_match_change_handler_test.go b/internal/app/issue_match_change/issue_match_change_handler_test.go
index 28cea300..b9065db3 100644
--- a/internal/app/issue_match_change/issue_match_change_handler_test.go
+++ b/internal/app/issue_match_change/issue_match_change_handler_test.go
@@ -30,7 +30,7 @@ var authz openfga.Authorization
var _ = BeforeSuite(func() {
db := mocks.NewMockDatabase(GinkgoT())
- er = event.NewEventRegistry(db)
+ er = event.NewEventRegistry(db, authz)
})
func getIssueMatchChangeFilter() *entity.IssueMatchChangeFilter {
diff --git a/internal/app/issue_repository/issue_repository_handler_events.go b/internal/app/issue_repository/issue_repository_handler_events.go
index 02a4cc33..6e7b73eb 100644
--- a/internal/app/issue_repository/issue_repository_handler_events.go
+++ b/internal/app/issue_repository/issue_repository_handler_events.go
@@ -7,6 +7,8 @@ import (
"github.com/cloudoperators/heureka/internal/app/event"
"github.com/cloudoperators/heureka/internal/database"
"github.com/cloudoperators/heureka/internal/entity"
+ appErrors "github.com/cloudoperators/heureka/internal/errors"
+ "github.com/cloudoperators/heureka/internal/openfga"
"github.com/sirupsen/logrus"
)
@@ -53,7 +55,9 @@ func (e *DeleteIssueRepositoryEvent) Name() event.EventName {
// OnIssueRepositoryCreate is a handler for the CreateIssueRepositoryEvent
// Is adding the default priority for the default issue repository
-func OnIssueRepositoryCreate(db database.Database, e event.Event) {
+func OnIssueRepositoryCreate(db database.Database, e event.Event, authz openfga.Authorization) {
+ op := appErrors.Op("OnIssueRepositoryCreate")
+
defaultPrio := db.GetDefaultIssuePriority()
l := logrus.WithFields(logrus.Fields{
@@ -90,7 +94,9 @@ func OnIssueRepositoryCreate(db database.Database, e event.Event) {
}
}
} else {
- l.Error("Wrong event")
+ err := NewIssueRepositoryHandlerError("OnIssueRepositoryCreate: triggered with wrong event type")
+ wrappedErr := appErrors.InternalError(string(op), "IssueRepository", "", err)
+ l.Error(wrappedErr)
}
}
diff --git a/internal/app/issue_repository/issue_repository_handler_test.go b/internal/app/issue_repository/issue_repository_handler_test.go
index 3158ee32..ac629d94 100644
--- a/internal/app/issue_repository/issue_repository_handler_test.go
+++ b/internal/app/issue_repository/issue_repository_handler_test.go
@@ -30,7 +30,7 @@ var authz openfga.Authorization
var _ = BeforeSuite(func() {
db := mocks.NewMockDatabase(GinkgoT())
- er = event.NewEventRegistry(db)
+ er = event.NewEventRegistry(db, authz)
})
func getIssueRepositoryFilter() *entity.IssueRepositoryFilter {
@@ -178,7 +178,7 @@ var _ = Describe("When creating IssueRepository", Label("app", "CreateIssueRepos
})
It("adds the issue repository to all services", func() {
- ir.OnIssueRepositoryCreate(db, event)
+ ir.OnIssueRepositoryCreate(db, event, authz)
db.AssertCalled(GinkgoT(), "AddIssueRepositoryToService", int64(1), int64(1), int64(100))
db.AssertCalled(GinkgoT(), "AddIssueRepositoryToService", int64(2), int64(1), int64(100))
diff --git a/internal/app/issue_variant/issue_variant_handler_test.go b/internal/app/issue_variant/issue_variant_handler_test.go
index 0a626d9f..7c3ca20e 100644
--- a/internal/app/issue_variant/issue_variant_handler_test.go
+++ b/internal/app/issue_variant/issue_variant_handler_test.go
@@ -28,10 +28,11 @@ func TestIssueVariantHandler(t *testing.T) {
}
var er event.EventRegistry
+var authz openfga.Authorization
var _ = BeforeSuite(func() {
db := mocks.NewMockDatabase(GinkgoT())
- er = event.NewEventRegistry(db)
+ er = event.NewEventRegistry(db, authz)
})
diff --git a/internal/app/scanner_run/scanner_run_test.go b/internal/app/scanner_run/scanner_run_test.go
index 102bfe23..27a507b1 100644
--- a/internal/app/scanner_run/scanner_run_test.go
+++ b/internal/app/scanner_run/scanner_run_test.go
@@ -9,6 +9,7 @@ import (
"github.com/cloudoperators/heureka/internal/app/common"
"github.com/cloudoperators/heureka/internal/app/event"
"github.com/cloudoperators/heureka/internal/entity/test"
+ "github.com/cloudoperators/heureka/internal/openfga"
"github.com/cloudoperators/heureka/internal/entity"
"github.com/cloudoperators/heureka/internal/mocks"
@@ -22,10 +23,11 @@ func TestServiceHandler(t *testing.T) {
}
var er event.EventRegistry
+var authz openfga.Authorization
var _ = BeforeSuite(func() {
db := mocks.NewMockDatabase(GinkgoT())
- er = event.NewEventRegistry(db)
+ er = event.NewEventRegistry(db, authz)
})
var sre *entity.ScannerRun
diff --git a/internal/app/service/service_handler_events.go b/internal/app/service/service_handler_events.go
index 0fb39694..74656105 100644
--- a/internal/app/service/service_handler_events.go
+++ b/internal/app/service/service_handler_events.go
@@ -7,6 +7,8 @@ import (
"github.com/cloudoperators/heureka/internal/app/event"
"github.com/cloudoperators/heureka/internal/database"
"github.com/cloudoperators/heureka/internal/entity"
+ appErrors "github.com/cloudoperators/heureka/internal/errors"
+ "github.com/cloudoperators/heureka/internal/openfga"
"github.com/sirupsen/logrus"
)
@@ -136,7 +138,9 @@ func (e *RemoveIssueRepositoryFromServiceEvent) Name() event.EventName {
// OnServiceCreate is a handler for the CreateServiceEvent
// Is creating a single default priority for the default issue repository
-func OnServiceCreate(db database.Database, e event.Event) {
+func OnServiceCreate(db database.Database, e event.Event, authz openfga.Authorization) {
+ op := appErrors.Op("OnServiceCreate")
+
defaultPrio := db.GetDefaultIssuePriority()
defaultRepoName := db.GetDefaultRepositoryName()
@@ -173,6 +177,144 @@ func OnServiceCreate(db database.Database, e event.Event) {
l.WithField("event-step", "AddIssueRepositoryToService").WithError(err).Error("Error while adding issue repository to service")
}
} else {
- l.Error("Wrong event")
+ err := NewServiceHandlerError("OnServiceCreate: triggered with wrong event type")
+ wrappedErr := appErrors.InternalError(string(op), "Service", "", err)
+ l.Error(wrappedErr)
+ }
+}
+
+// OnServiceCreateAuthz is a handler for the CreateServiceEvent
+// It creates an OpenFGA relation tuple for the service and the current user
+func OnServiceCreateAuthz(db database.Database, e event.Event, authz openfga.Authorization) {
+ op := appErrors.Op("OnServiceCreateAuthz")
+
+ l := logrus.WithFields(logrus.Fields{
+ "event": "OnServiceCreateAuthz",
+ "payload": e,
+ })
+
+ if createEvent, ok := e.(*CreateServiceEvent); ok {
+ userId := openfga.UserIdFromInt(createEvent.Service.BaseService.CreatedBy)
+
+ relations := []openfga.RelationInput{
+ {
+ UserType: openfga.TypeRole,
+ UserId: userId,
+ Relation: openfga.RelRole,
+ ObjectType: openfga.TypeService,
+ ObjectId: openfga.ObjectIdFromInt(createEvent.Service.Id),
+ },
+ }
+
+ err := authz.AddRelationBulk(relations)
+ if err != nil {
+ wrappedErr := appErrors.InternalError(string(op), "Service", "", err)
+ l.Error(wrappedErr)
+ }
+ } else {
+ err := NewServiceHandlerError("OnServiceCreateAuthz: triggered with wrong event type")
+ wrappedErr := appErrors.InternalError(string(op), "Service", "", err)
+ l.Error(wrappedErr)
+ }
+}
+
+// OnServiceDeleteAuthz is a handler for the DeleteServiceEvent
+// It deletes all OpenFGA relation tuples containing that service
+func OnServiceDeleteAuthz(db database.Database, e event.Event, authz openfga.Authorization) {
+ op := appErrors.Op("OnServiceDeleteAuthz")
+
+ deleteInput := []openfga.RelationInput{}
+
+ l := logrus.WithFields(logrus.Fields{
+ "event": "OnServiceDeleteAuthz",
+ "payload": e,
+ })
+
+ if deleteEvent, ok := e.(*DeleteServiceEvent); ok {
+ // Delete all tuples where object is the service
+ deleteInput = append(deleteInput, openfga.RelationInput{
+ ObjectType: openfga.TypeService,
+ ObjectId: openfga.ObjectIdFromInt(deleteEvent.ServiceID),
+ })
+
+ // Delete all tuples where user is the service
+ deleteInput = append(deleteInput, openfga.RelationInput{
+ UserType: openfga.TypeService,
+ UserId: openfga.UserIdFromInt(deleteEvent.ServiceID),
+ ObjectType: openfga.TypeComponentInstance,
+ })
+
+ err := authz.RemoveRelationBulk(deleteInput)
+ if err != nil {
+ wrappedErr := appErrors.InternalError(string(op), "Service", "", err)
+ l.Error(wrappedErr)
+ }
+ } else {
+ err := NewServiceHandlerError("OnServiceDeleteAuthz: triggered with wrong event type")
+ wrappedErr := appErrors.InternalError(string(op), "Service", "", err)
+ l.Error(wrappedErr)
+ }
+}
+
+// OnAddOwnerToService is a handler for the AddOwnerToServiceEvent
+// It creates an OpenFGA relation tuple between the owner and the service
+func OnAddOwnerToService(db database.Database, e event.Event, authz openfga.Authorization) {
+ op := appErrors.Op("OnAddOwnerToService")
+
+ l := logrus.WithFields(logrus.Fields{
+ "event": "OnAddOwnerToService",
+ "payload": e,
+ })
+
+ if addEvent, ok := e.(*AddOwnerToServiceEvent); ok {
+ relations := []openfga.RelationInput{
+ {
+ UserType: openfga.TypeUser,
+ UserId: openfga.UserIdFromInt(addEvent.OwnerID),
+ ObjectType: openfga.TypeService,
+ ObjectId: openfga.ObjectIdFromInt(addEvent.ServiceID),
+ Relation: openfga.RelOwner,
+ },
+ }
+
+ err := authz.AddRelationBulk(relations)
+ if err != nil {
+ wrappedErr := appErrors.InternalError(string(op), "Service", "", err)
+ l.Error(wrappedErr)
+ }
+ } else {
+ err := NewServiceHandlerError("OnAddOwnerToService: triggered with wrong event type")
+ wrappedErr := appErrors.InternalError(string(op), "Service", "", err)
+ l.Error(wrappedErr)
+ }
+}
+
+// OnRemoveOwnerFromService is a handler for the RemoveOwnerFromServiceEvent
+// It removes the OpenFGA relation tuple between the owner and the service
+func OnRemoveOwnerFromService(db database.Database, e event.Event, authz openfga.Authorization) {
+ op := appErrors.Op("OnRemoveOwnerFromService")
+
+ l := logrus.WithFields(logrus.Fields{
+ "event": "OnRemoveOwnerFromService",
+ "payload": e,
+ })
+
+ if removeEvent, ok := e.(*RemoveOwnerFromServiceEvent); ok {
+ rel := openfga.RelationInput{
+ UserType: openfga.TypeUser,
+ UserId: openfga.UserIdFromInt(removeEvent.OwnerID),
+ ObjectType: openfga.TypeService,
+ ObjectId: openfga.ObjectIdFromInt(removeEvent.ServiceID),
+ Relation: openfga.RelOwner,
+ }
+ err := authz.RemoveRelation(rel)
+ if err != nil {
+ wrappedErr := appErrors.InternalError(string(op), "Service", "", err)
+ l.Error(wrappedErr)
+ }
+ } else {
+ err := NewServiceHandlerError("OnRemoveOwnerFromService: triggered with wrong event type")
+ wrappedErr := appErrors.InternalError(string(op), "Service", "", err)
+ l.Error(wrappedErr)
}
}
diff --git a/internal/app/service/service_handler_test.go b/internal/app/service/service_handler_test.go
index b678e2c7..2283ccc8 100644
--- a/internal/app/service/service_handler_test.go
+++ b/internal/app/service/service_handler_test.go
@@ -13,6 +13,7 @@ import (
s "github.com/cloudoperators/heureka/internal/app/service"
"github.com/cloudoperators/heureka/internal/database/mariadb"
"github.com/cloudoperators/heureka/internal/openfga"
+ "github.com/cloudoperators/heureka/internal/util"
"github.com/cloudoperators/heureka/internal/cache"
"github.com/cloudoperators/heureka/internal/entity"
@@ -29,12 +30,21 @@ func TestServiceHandler(t *testing.T) {
RunSpecs(t, "Service Service Test Suite")
}
-var er event.EventRegistry
-var authz openfga.Authorization
+var handlerContext common.HandlerContext
+var cfg *util.Config
var _ = BeforeSuite(func() {
+ cfg = common.GetTestConfig()
+ enableLogs := false
db := mocks.NewMockDatabase(GinkgoT())
- er = event.NewEventRegistry(db)
+ authz := openfga.NewAuthorizationHandler(cfg, enableLogs)
+ er := event.NewEventRegistry(db, authz)
+ handlerContext = common.HandlerContext{
+ DB: db,
+ EventReg: er,
+ Cache: cache.NewNoCache(),
+ Authz: authz,
+ }
})
func getServiceFilter() *entity.ServiceFilter {
@@ -52,24 +62,20 @@ func getServiceFilter() *entity.ServiceFilter {
var _ = Describe("When listing Services", Label("app", "ListServices"), func() {
var (
+ er event.EventRegistry
db *mocks.MockDatabase
serviceHandler s.ServiceHandler
filter *entity.ServiceFilter
options *entity.ListOptions
- handlerContext common.HandlerContext
)
BeforeEach(func() {
db = mocks.NewMockDatabase(GinkgoT())
+ er = event.NewEventRegistry(db, handlerContext.Authz)
options = entity.NewListOptions()
filter = getServiceFilter()
- cache := cache.NewNoCache()
- handlerContext = common.HandlerContext{
- DB: db,
- EventReg: er,
- Cache: cache,
- Authz: authz,
- }
+ handlerContext.DB = db
+ handlerContext.EventReg = er
})
When("the list option does include the totalCount", func() {
@@ -229,11 +235,13 @@ var _ = Describe("When creating Service", Label("app", "CreateService"), func()
serviceHandler s.ServiceHandler
service entity.Service
filter *entity.ServiceFilter
- handlerContext common.HandlerContext
+ p openfga.PermissionInput
+ er event.EventRegistry
)
BeforeEach(func() {
db = mocks.NewMockDatabase(GinkgoT())
+ er = event.NewEventRegistry(db, handlerContext.Authz)
service = test.NewFakeServiceEntity()
first := 10
after := ""
@@ -243,13 +251,17 @@ var _ = Describe("When creating Service", Label("app", "CreateService"), func()
After: &after,
},
}
- cache := cache.NewNoCache()
- handlerContext = common.HandlerContext{
- DB: db,
- EventReg: er,
- Cache: cache,
- Authz: authz,
+
+ p = openfga.PermissionInput{
+ UserType: openfga.TypeRole,
+ UserId: "0",
+ ObjectId: openfga.IDService,
+ ObjectType: openfga.TypeService,
+ Relation: openfga.RelRole,
}
+
+ handlerContext.DB = db
+ handlerContext.EventReg = er
})
It("creates service", func() {
@@ -295,7 +307,7 @@ var _ = Describe("When creating Service", Label("app", "CreateService"), func()
db.On("AddIssueRepositoryToService", createEvent.Service.Id, repo.Id, int64(defaultPrio)).Return(nil)
// Simulate event
- s.OnServiceCreate(db, event)
+ s.OnServiceCreate(db, event, handlerContext.Authz)
// Check AddIssueRepositoryToService was called
db.AssertCalled(GinkgoT(), "AddIssueRepositoryToService", createEvent.Service.Id, repo.Id, int64(defaultPrio))
@@ -309,7 +321,7 @@ var _ = Describe("When creating Service", Label("app", "CreateService"), func()
// Use type assertion to convert
var event event.Event = invalidEvent
- s.OnServiceCreate(db, event)
+ s.OnServiceCreate(db, event, handlerContext.Authz)
// These functions should not be called in case of a different event
db.AssertNotCalled(GinkgoT(), "GetIssueRepositories")
@@ -333,26 +345,48 @@ var _ = Describe("When creating Service", Label("app", "CreateService"), func()
Name: []*string{&defaultRepoName},
}).Return([]entity.IssueRepository{}, nil)
- s.OnServiceCreate(db, event)
+ s.OnServiceCreate(db, event, handlerContext.Authz)
db.AssertNotCalled(GinkgoT(), "AddIssueRepositoryToService")
// TODO: we could also check for the error message here
})
})
})
+
+ Context("when handling a CreateServiceEvent authz", func() {
+ Context("when new service is created", func() {
+ It("should add user resource relationship tuple in openfga", func() {
+ srv := test.NewFakeServiceEntity()
+ createEvent := &s.CreateServiceEvent{
+ Service: &srv,
+ }
+
+ // Use type assertion to convert a CreateServiceEvent into an Event
+ var event event.Event = createEvent
+ p.ObjectId = openfga.ObjectIdFromInt(createEvent.Service.Id)
+ // Simulate event
+ s.OnServiceCreateAuthz(db, event, handlerContext.Authz)
+
+ ok, err := handlerContext.Authz.CheckPermission(p)
+ Expect(err).To(BeNil(), "no error should be thrown")
+ Expect(ok).To(BeTrue(), "permission should be granted")
+ })
+ })
+ })
})
var _ = Describe("When updating Service", Label("app", "UpdateService"), func() {
var (
+ er event.EventRegistry
db *mocks.MockDatabase
serviceHandler s.ServiceHandler
service entity.ServiceResult
filter *entity.ServiceFilter
- handlerContext common.HandlerContext
)
BeforeEach(func() {
db = mocks.NewMockDatabase(GinkgoT())
+ er = event.NewEventRegistry(db, handlerContext.Authz)
service = test.NewFakeServiceResult()
first := 10
after := ""
@@ -362,13 +396,8 @@ var _ = Describe("When updating Service", Label("app", "UpdateService"), func()
After: &after,
},
}
- cache := cache.NewNoCache()
- handlerContext = common.HandlerContext{
- DB: db,
- EventReg: er,
- Cache: cache,
- Authz: authz,
- }
+ handlerContext.DB = db
+ handlerContext.EventReg = er
})
It("updates service", func() {
@@ -388,15 +417,16 @@ var _ = Describe("When updating Service", Label("app", "UpdateService"), func()
var _ = Describe("When deleting Service", Label("app", "DeleteService"), func() {
var (
+ er event.EventRegistry
db *mocks.MockDatabase
serviceHandler s.ServiceHandler
id int64
filter *entity.ServiceFilter
- handlerContext common.HandlerContext
)
BeforeEach(func() {
db = mocks.NewMockDatabase(GinkgoT())
+ er = event.NewEventRegistry(db, handlerContext.Authz)
id = 1
first := 10
after := ""
@@ -406,13 +436,8 @@ var _ = Describe("When deleting Service", Label("app", "DeleteService"), func()
After: &after,
},
}
- cache := cache.NewNoCache()
- handlerContext = common.HandlerContext{
- DB: db,
- EventReg: er,
- Cache: cache,
- Authz: authz,
- }
+ handlerContext.DB = db
+ handlerContext.EventReg = er
})
It("deletes service", func() {
@@ -429,20 +454,122 @@ var _ = Describe("When deleting Service", Label("app", "DeleteService"), func()
Expect(err).To(BeNil(), "no error should be thrown")
Expect(services.Elements).To(BeEmpty(), "no services should be found")
})
+
+ Context("when handling a DeleteServiceEvent", func() {
+ Context("when new service is deleted", func() {
+ It("should delete tuples related to that service in openfga", func() {
+ // Test OnServiceDeleteAuthz against all possible relations
+ srv := test.NewFakeServiceEntity()
+ deleteEvent := &s.DeleteServiceEvent{
+ ServiceID: srv.Id,
+ }
+ objectId := openfga.ObjectIdFromInt(deleteEvent.ServiceID)
+ userId := openfga.UserIdFromInt(deleteEvent.ServiceID)
+
+ relations := []openfga.RelationInput{
+ { // user - service: a user can view the service
+ UserType: openfga.TypeUser,
+ UserId: openfga.IDUser,
+ ObjectId: objectId,
+ ObjectType: openfga.TypeService,
+ Relation: openfga.RelCanView,
+ },
+ { // role - service: a role is assigned to the service
+ UserType: openfga.TypeRole,
+ UserId: openfga.IDRole,
+ ObjectId: objectId,
+ ObjectType: openfga.TypeService,
+ Relation: openfga.RelRole,
+ },
+ { // support group - service: a support group is related to the service
+ UserType: openfga.TypeSupportGroup,
+ UserId: openfga.IDSupportGroup,
+ ObjectId: objectId,
+ ObjectType: openfga.TypeService,
+ Relation: openfga.RelSupportGroup,
+ },
+ { // service - component_instance: a service is related to a component instance
+ UserType: openfga.TypeService,
+ UserId: userId,
+ ObjectId: openfga.IDComponentInstance,
+ ObjectType: openfga.RelComponentInstance,
+ Relation: openfga.RelRelatedService,
+ },
+ }
+
+ handlerContext.Authz.AddRelationBulk(relations)
+
+ // get the number of relations before deletion
+ relCountBefore := 0
+ for _, r := range relations {
+ relations, err := handlerContext.Authz.ListRelations(r)
+ if err != nil {
+ Expect(err).To(BeNil(), "no error should be thrown")
+ }
+ relCountBefore += len(relations)
+ }
+ Expect(relCountBefore).To(Equal(len(relations)), "all relations should exist before deletion")
+
+ // check that relations were created
+ for _, r := range relations {
+ ok, err := handlerContext.Authz.CheckPermission(openfga.PermissionInput{
+ UserType: r.UserType,
+ UserId: r.UserId,
+ ObjectType: r.ObjectType,
+ ObjectId: r.ObjectId,
+ Relation: r.Relation,
+ })
+ Expect(err).To(BeNil(), "no error should be thrown")
+ Expect(ok).To(BeTrue(), "permission should be granted")
+ }
+
+ var event event.Event = deleteEvent
+ // Simulate event
+ s.OnServiceDeleteAuthz(db, event, handlerContext.Authz)
+
+ // get the number of relations after deletion
+ relCountAfter := 0
+ for _, r := range relations {
+ relations, err := handlerContext.Authz.ListRelations(r)
+ if err != nil {
+ Expect(err).To(BeNil(), "no error should be thrown")
+ }
+ relCountAfter += len(relations)
+ }
+ Expect(relCountAfter < relCountBefore).To(BeTrue(), "less relations after deletion")
+ Expect(relCountAfter).To(BeEquivalentTo(0), "no relations should exist after deletion")
+
+ // verify that relations were deleted
+ for _, r := range relations {
+ ok, err := handlerContext.Authz.CheckPermission(openfga.PermissionInput{
+ UserType: r.UserType,
+ UserId: r.UserId,
+ ObjectType: r.ObjectType,
+ ObjectId: r.ObjectId,
+ Relation: r.Relation,
+ })
+ Expect(err).To(BeNil(), "no error should be thrown")
+ Expect(ok).To(BeFalse(), "permission should NOT be granted")
+ }
+ })
+ })
+ })
})
var _ = Describe("When modifying owner and Service", Label("app", "OwnerService"), func() {
var (
db *mocks.MockDatabase
+ er event.EventRegistry
serviceHandler s.ServiceHandler
service entity.ServiceResult
owner entity.User
filter *entity.ServiceFilter
- handlerContext common.HandlerContext
+ p openfga.PermissionInput
)
BeforeEach(func() {
db = mocks.NewMockDatabase(GinkgoT())
+ er = event.NewEventRegistry(db, handlerContext.Authz)
service = test.NewFakeServiceResult()
owner = test.NewFakeUserEntity()
first := 10
@@ -454,12 +581,15 @@ var _ = Describe("When modifying owner and Service", Label("app", "OwnerService"
},
Id: []*int64{&service.Id},
}
- cache := cache.NewNoCache()
- handlerContext = common.HandlerContext{
- DB: db,
- EventReg: er,
- Cache: cache,
- Authz: authz,
+ handlerContext.DB = db
+ handlerContext.EventReg = er
+
+ p = openfga.PermissionInput{
+ UserType: openfga.TypeUser,
+ UserId: "",
+ ObjectType: openfga.TypeService,
+ ObjectId: "",
+ Relation: openfga.RelOwner,
}
})
@@ -472,6 +602,26 @@ var _ = Describe("When modifying owner and Service", Label("app", "OwnerService"
Expect(service).NotTo(BeNil(), "service should be returned")
})
+ Context("when handling an AddOwnerToServiceEvent", func() {
+ It("should add the owner-service relation tuple in openfga", func() {
+ serviceFake := test.NewFakeServiceResult()
+ ownerFake := test.NewFakeUserEntity()
+ addEvent := &s.AddOwnerToServiceEvent{
+ ServiceID: serviceFake.Id,
+ OwnerID: ownerFake.Id,
+ }
+
+ var event event.Event = addEvent
+ s.OnAddOwnerToService(db, event, handlerContext.Authz)
+
+ p.ObjectId = openfga.ObjectIdFromInt(addEvent.ServiceID)
+ p.UserId = openfga.UserIdFromInt(addEvent.OwnerID)
+ ok, err := handlerContext.Authz.CheckPermission(p)
+ Expect(err).To(BeNil(), "no error should be thrown")
+ Expect(ok).To(BeTrue(), "permission should be granted")
+ })
+ })
+
It("removes owner from service", func() {
db.On("RemoveOwnerFromService", service.Id, owner.Id).Return(nil)
db.On("GetServices", filter, []entity.Order{}).Return([]entity.ServiceResult{service}, nil)
@@ -480,21 +630,52 @@ var _ = Describe("When modifying owner and Service", Label("app", "OwnerService"
Expect(err).To(BeNil(), "no error should be thrown")
Expect(service).NotTo(BeNil(), "service should be returned")
})
+
+ Context("when handling a RemoveOwnerFromServiceEvent", func() {
+ It("should remove the owner-service relation tuple in openfga", func() {
+ serviceFake := test.NewFakeServiceResult()
+ ownerFake := test.NewFakeUserEntity()
+ removeEvent := &s.RemoveOwnerFromServiceEvent{
+ ServiceID: serviceFake.Id,
+ OwnerID: ownerFake.Id,
+ }
+ serviceId := openfga.ObjectIdFromInt(removeEvent.ServiceID)
+ ownerId := openfga.UserIdFromInt(removeEvent.OwnerID)
+
+ rel := openfga.RelationInput{
+ UserType: openfga.TypeUser,
+ UserId: ownerId,
+ ObjectType: openfga.TypeService,
+ ObjectId: serviceId,
+ Relation: openfga.RelOwner,
+ }
+
+ handlerContext.Authz.AddRelationBulk([]openfga.RelationInput{rel})
+
+ var event event.Event = removeEvent
+ s.OnRemoveOwnerFromService(db, event, handlerContext.Authz)
+
+ remaining, err := handlerContext.Authz.ListRelations(rel)
+ Expect(err).To(BeNil(), "no error should be thrown")
+ Expect(remaining).To(BeEmpty(), "relation should not exist after removal")
+ })
+ })
})
var _ = Describe("When modifying relationship of issueRepository and Service", Label("app", "IssueRepositoryHandlerRelationship"), func() {
var (
+ er event.EventRegistry
db *mocks.MockDatabase
serviceHandler s.ServiceHandler
service entity.ServiceResult
issueRepository entity.IssueRepository
filter *entity.ServiceFilter
priority int64
- handlerContext common.HandlerContext
)
BeforeEach(func() {
db = mocks.NewMockDatabase(GinkgoT())
+ er = event.NewEventRegistry(db, handlerContext.Authz)
service = test.NewFakeServiceResult()
issueRepository = test.NewFakeIssueRepositoryEntity()
first := 10
@@ -507,13 +688,8 @@ var _ = Describe("When modifying relationship of issueRepository and Service", L
Id: []*int64{&service.Id},
}
priority = 1
- cache := cache.NewNoCache()
- handlerContext = common.HandlerContext{
- DB: db,
- EventReg: er,
- Cache: cache,
- Authz: authz,
- }
+ handlerContext.DB = db
+ handlerContext.EventReg = er
})
It("adds issueRepository to service", func() {
@@ -537,26 +713,22 @@ var _ = Describe("When modifying relationship of issueRepository and Service", L
var _ = Describe("When listing serviceCcrns", Label("app", "ListServicesCcrns"), func() {
var (
+ er event.EventRegistry
db *mocks.MockDatabase
serviceHandler s.ServiceHandler
filter *entity.ServiceFilter
options *entity.ListOptions
name string
- handlerContext common.HandlerContext
)
BeforeEach(func() {
db = mocks.NewMockDatabase(GinkgoT())
+ er = event.NewEventRegistry(db, handlerContext.Authz)
options = entity.NewListOptions()
filter = getServiceFilter()
name = "f1"
- cache := cache.NewNoCache()
- handlerContext = common.HandlerContext{
- DB: db,
- EventReg: er,
- Cache: cache,
- Authz: authz,
- }
+ handlerContext.DB = db
+ handlerContext.EventReg = er
})
When("no filters are used", func() {
@@ -591,26 +763,22 @@ var _ = Describe("When listing serviceCcrns", Label("app", "ListServicesCcrns"),
var _ = Describe("When listing serviceDomains", Label("app", "ListServicesDomains"), func() {
var (
+ er event.EventRegistry
db *mocks.MockDatabase
serviceHandler s.ServiceHandler
filter *entity.ServiceFilter
options *entity.ListOptions
domain string
- handlerContext common.HandlerContext
)
BeforeEach(func() {
db = mocks.NewMockDatabase(GinkgoT())
+ er = event.NewEventRegistry(db, handlerContext.Authz)
options = entity.NewListOptions()
filter = getServiceFilter()
domain = "f1"
- cache := cache.NewNoCache()
- handlerContext = common.HandlerContext{
- DB: db,
- EventReg: er,
- Cache: cache,
- Authz: authz,
- }
+ handlerContext.DB = db
+ handlerContext.EventReg = er
})
When("no filters are used", func() {
@@ -645,26 +813,22 @@ var _ = Describe("When listing serviceDomains", Label("app", "ListServicesDomain
var _ = Describe("When listing serviceRegions", Label("app", "ListServiceRegions"), func() {
var (
+ er event.EventRegistry
db *mocks.MockDatabase
serviceHandler s.ServiceHandler
filter *entity.ServiceFilter
options *entity.ListOptions
region string
- handlerContext common.HandlerContext
)
BeforeEach(func() {
db = mocks.NewMockDatabase(GinkgoT())
+ er = event.NewEventRegistry(db, handlerContext.Authz)
options = entity.NewListOptions()
filter = getServiceFilter()
region = "f1"
- cache := cache.NewNoCache()
- handlerContext = common.HandlerContext{
- DB: db,
- EventReg: er,
- Cache: cache,
- Authz: authz,
- }
+ handlerContext.DB = db
+ handlerContext.EventReg = er
})
When("no filters are used", func() {
diff --git a/internal/app/severity/severity_handler_test.go b/internal/app/severity/severity_handler_test.go
index 607de83e..f268ccdf 100644
--- a/internal/app/severity/severity_handler_test.go
+++ b/internal/app/severity/severity_handler_test.go
@@ -29,10 +29,11 @@ func TestSeverityHandler(t *testing.T) {
}
var er event.EventRegistry
+var authz openfga.Authorization
var _ = BeforeSuite(func() {
db := mocks.NewMockDatabase(GinkgoT())
- er = event.NewEventRegistry(db)
+ er = event.NewEventRegistry(db, authz)
})
func severityFilter() *entity.SeverityFilter {
diff --git a/internal/app/support_group/support_group_handler_events.go b/internal/app/support_group/support_group_handler_events.go
index bdd17bd5..54e8b0ae 100644
--- a/internal/app/support_group/support_group_handler_events.go
+++ b/internal/app/support_group/support_group_handler_events.go
@@ -5,7 +5,11 @@ package support_group
import (
"github.com/cloudoperators/heureka/internal/app/event"
+ "github.com/cloudoperators/heureka/internal/database"
"github.com/cloudoperators/heureka/internal/entity"
+ appErrors "github.com/cloudoperators/heureka/internal/errors"
+ "github.com/cloudoperators/heureka/internal/openfga"
+ "github.com/sirupsen/logrus"
)
const (
@@ -109,3 +113,218 @@ type ListSupportGroupCcrnsEvent struct {
func (e *ListSupportGroupCcrnsEvent) Name() event.EventName {
return ListSupportGroupCcrnsEventName
}
+
+// OnSupportGroupCreateAuthz is a handler for the CreateSupportGroupEvent
+// It creates an OpenFGA relation tuple for the support group and the current user
+func OnSupportGroupCreateAuthz(db database.Database, e event.Event, authz openfga.Authorization) {
+ op := appErrors.Op("OnSupportGroupCreateAuthz")
+
+ l := logrus.WithFields(logrus.Fields{
+ "event": "OnSupportGroupCreateAuthz",
+ "payload": e,
+ })
+
+ if createEvent, ok := e.(*CreateSupportGroupEvent); ok {
+ userId := openfga.UserIdFromInt(createEvent.SupportGroup.CreatedBy)
+
+ relations := []openfga.RelationInput{
+ {
+ UserType: openfga.TypeRole,
+ UserId: userId,
+ Relation: openfga.RelRole,
+ ObjectType: openfga.TypeSupportGroup,
+ ObjectId: openfga.ObjectIdFromInt(createEvent.SupportGroup.Id),
+ },
+ }
+
+ err := authz.AddRelationBulk(relations)
+ if err != nil {
+ wrappedErr := appErrors.InternalError(string(op), "SupportGroup", "", err)
+ l.Error(wrappedErr)
+ }
+ } else {
+ err := NewSupportGroupHandlerError("OnSupportGroupCreateAuthz: triggered with wrong event type")
+ wrappedErr := appErrors.InternalError(string(op), "SupportGroup", "", err)
+ l.Error(wrappedErr)
+ }
+}
+
+// OnServiceDeleteAuthz is a handler for the DeleteServiceEvent
+func OnSupportGroupDeleteAuthz(db database.Database, e event.Event, authz openfga.Authorization) {
+ op := appErrors.Op("OnSupportGroupDeleteAuthz")
+
+ deleteInput := []openfga.RelationInput{}
+
+ l := logrus.WithFields(logrus.Fields{
+ "event": "OnSupportGroupDeleteAuthz",
+ "payload": e,
+ })
+
+ if deleteEvent, ok := e.(*DeleteSupportGroupEvent); ok {
+ // remove its relation to itself first to prevent duplicate tuple api errors in the bulk deletes
+ selfRelation := openfga.RelationInput{
+ UserType: openfga.TypeSupportGroup,
+ UserId: openfga.UserIdFromInt(deleteEvent.SupportGroupID),
+ ObjectType: openfga.TypeSupportGroup,
+ ObjectId: openfga.ObjectIdFromInt(deleteEvent.SupportGroupID),
+ Relation: openfga.RelSupportGroup,
+ }
+ err := authz.RemoveRelation(selfRelation)
+ if err != nil {
+ wrappedErr := appErrors.InternalError(string(op), "SupportGroup", "", err)
+ l.Error(wrappedErr)
+ }
+
+ // Delete all tuples where object is the support_group
+ deleteInput = append(deleteInput, openfga.RelationInput{
+ ObjectType: openfga.TypeSupportGroup,
+ ObjectId: openfga.ObjectIdFromInt(deleteEvent.SupportGroupID),
+ })
+
+ // Delete all tuples where user is the support_group
+ deleteInput = append(deleteInput, openfga.RelationInput{
+ UserType: openfga.TypeSupportGroup,
+ UserId: openfga.UserIdFromInt(deleteEvent.SupportGroupID),
+ ObjectType: openfga.TypeService,
+ })
+ deleteInput = append(deleteInput, openfga.RelationInput{
+ UserType: openfga.TypeSupportGroup,
+ UserId: openfga.UserIdFromInt(deleteEvent.SupportGroupID),
+ ObjectType: openfga.TypeSupportGroup,
+ })
+
+ err = authz.RemoveRelationBulk(deleteInput)
+ if err != nil {
+ wrappedErr := appErrors.InternalError(string(op), "SupportGroup", "", err)
+ l.Error(wrappedErr)
+ }
+ } else {
+ err := NewSupportGroupHandlerError("OnSupportGroupDeleteAuthz: triggered with wrong event type")
+ wrappedErr := appErrors.InternalError(string(op), "SupportGroup", "", err)
+ l.Error(wrappedErr)
+ }
+}
+
+// OnAddServiceToSupportGroup is a handler for the AddServiceToSupportGroupEvent
+// It creates an OpenFGA relation tuple between the support group and the service
+func OnAddServiceToSupportGroup(db database.Database, e event.Event, authz openfga.Authorization) {
+ op := appErrors.Op("OnAddServiceToSupportGroup")
+
+ l := logrus.WithFields(logrus.Fields{
+ "event": "OnAddServiceToSupportGroup",
+ "payload": e,
+ })
+
+ if addEvent, ok := e.(*AddServiceToSupportGroupEvent); ok {
+ relations := []openfga.RelationInput{
+ {
+ UserType: openfga.TypeSupportGroup,
+ UserId: openfga.UserIdFromInt(addEvent.SupportGroupID),
+ ObjectType: openfga.TypeService,
+ ObjectId: openfga.ObjectIdFromInt(addEvent.ServiceID),
+ Relation: openfga.RelSupportGroup,
+ },
+ }
+ err := authz.AddRelationBulk(relations)
+ if err != nil {
+ wrappedErr := appErrors.InternalError(string(op), "SupportGroup", "", err)
+ l.Error(wrappedErr)
+ }
+ } else {
+ err := NewSupportGroupHandlerError("OnAddServiceToSupportGroup: triggered with wrong event type")
+ wrappedErr := appErrors.InternalError(string(op), "SupportGroup", "", err)
+ l.Error(wrappedErr)
+ }
+}
+
+// OnRemoveServiceFromSupportGroup is a handler for the RemoveServiceFromSupportGroupEvent
+// It removes the OpenFGA relation tuple between the support group and the service
+func OnRemoveServiceFromSupportGroup(db database.Database, e event.Event, authz openfga.Authorization) {
+ op := appErrors.Op("OnRemoveServiceFromSupportGroup")
+
+ l := logrus.WithFields(logrus.Fields{
+ "event": "OnRemoveServiceFromSupportGroup",
+ "payload": e,
+ })
+
+ if removeEvent, ok := e.(*RemoveServiceFromSupportGroupEvent); ok {
+ rel := openfga.RelationInput{
+ UserType: openfga.TypeSupportGroup,
+ UserId: openfga.UserIdFromInt(removeEvent.SupportGroupID),
+ ObjectType: openfga.TypeService,
+ ObjectId: openfga.ObjectIdFromInt(removeEvent.ServiceID),
+ Relation: openfga.RelSupportGroup,
+ }
+ err := authz.RemoveRelation(rel)
+ if err != nil {
+ wrappedErr := appErrors.InternalError(string(op), "SupportGroup", "", err)
+ l.Error(wrappedErr)
+ }
+ } else {
+ err := NewSupportGroupHandlerError("OnRemoveServiceFromSupportGroup: triggered with wrong event type")
+ wrappedErr := appErrors.InternalError(string(op), "SupportGroup", "", err)
+ l.Error(wrappedErr)
+ }
+}
+
+// OnAddUserToSupportGroup is a handler for the AddUserToSupportGroupEvent
+// It creates an OpenFGA relation tuple between the user and the support group
+func OnAddUserToSupportGroup(db database.Database, e event.Event, authz openfga.Authorization) {
+ op := appErrors.Op("OnAddUserToSupportGroup")
+
+ l := logrus.WithFields(logrus.Fields{
+ "event": "OnAddUserToSupportGroup",
+ "payload": e,
+ })
+
+ if addEvent, ok := e.(*AddUserToSupportGroupEvent); ok {
+ relations := []openfga.RelationInput{
+ {
+ UserType: openfga.TypeUser,
+ UserId: openfga.UserIdFromInt(addEvent.UserID),
+ ObjectType: openfga.TypeSupportGroup,
+ ObjectId: openfga.ObjectIdFromInt(addEvent.SupportGroupID),
+ Relation: openfga.RelMember,
+ },
+ }
+ err := authz.AddRelationBulk(relations)
+ if err != nil {
+ wrappedErr := appErrors.InternalError(string(op), "SupportGroup", "", err)
+ l.Error(wrappedErr)
+ }
+ } else {
+ err := NewSupportGroupHandlerError("OnAddUserToSupportGroup: triggered with wrong event type")
+ wrappedErr := appErrors.InternalError(string(op), "SupportGroup", "", err)
+ l.Error(wrappedErr)
+ }
+}
+
+// OnRemoveUserFromSupportGroup is a handler for the RemoveUserFromSupportGroupEvent
+// It removes the OpenFGA relation tuple between the user and the support group
+func OnRemoveUserFromSupportGroup(db database.Database, e event.Event, authz openfga.Authorization) {
+ op := appErrors.Op("OnRemoveUserFromSupportGroup")
+
+ l := logrus.WithFields(logrus.Fields{
+ "event": "OnRemoveUserFromSupportGroup",
+ "payload": e,
+ })
+
+ if removeEvent, ok := e.(*RemoveUserFromSupportGroupEvent); ok {
+ rel := openfga.RelationInput{
+ UserType: openfga.TypeUser,
+ UserId: openfga.UserIdFromInt(removeEvent.UserID),
+ ObjectType: openfga.TypeSupportGroup,
+ ObjectId: openfga.ObjectIdFromInt(removeEvent.SupportGroupID),
+ Relation: openfga.RelMember,
+ }
+ err := authz.RemoveRelation(rel)
+ if err != nil {
+ wrappedErr := appErrors.InternalError(string(op), "SupportGroup", "", err)
+ l.Error(wrappedErr)
+ }
+ } else {
+ err := NewSupportGroupHandlerError("OnRemoveUserFromSupportGroup: triggered with wrong event type")
+ wrappedErr := appErrors.InternalError(string(op), "SupportGroup", "", err)
+ l.Error(wrappedErr)
+ }
+}
diff --git a/internal/app/support_group/support_group_handler_test.go b/internal/app/support_group/support_group_handler_test.go
index c3621cdc..f4c2dd65 100644
--- a/internal/app/support_group/support_group_handler_test.go
+++ b/internal/app/support_group/support_group_handler_test.go
@@ -10,11 +10,13 @@ import (
"github.com/cloudoperators/heureka/internal/app/common"
"github.com/cloudoperators/heureka/internal/app/event"
sg "github.com/cloudoperators/heureka/internal/app/support_group"
+ "github.com/cloudoperators/heureka/internal/cache"
"github.com/cloudoperators/heureka/internal/database/mariadb"
"github.com/cloudoperators/heureka/internal/entity"
"github.com/cloudoperators/heureka/internal/entity/test"
"github.com/cloudoperators/heureka/internal/mocks"
"github.com/cloudoperators/heureka/internal/openfga"
+ "github.com/cloudoperators/heureka/internal/util"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/samber/lo"
@@ -26,12 +28,17 @@ func TestSupportGroupHandler(t *testing.T) {
RunSpecs(t, "Support Group Service Test Suite")
}
-var er event.EventRegistry
-var authz openfga.Authorization
+var handlerContext common.HandlerContext
+var cfg *util.Config
var _ = BeforeSuite(func() {
- db := mocks.NewMockDatabase(GinkgoT())
- er = event.NewEventRegistry(db)
+ cfg = common.GetTestConfig()
+ enableLogs := false
+ authz := openfga.NewAuthorizationHandler(cfg, enableLogs)
+ handlerContext = common.HandlerContext{
+ Cache: cache.NewNoCache(),
+ Authz: authz,
+ }
})
func getSupportGroupFilter() *entity.SupportGroupFilter {
@@ -45,12 +52,12 @@ func getSupportGroupFilter() *entity.SupportGroupFilter {
var _ = Describe("When listing SupportGroups", Label("app", "ListSupportGroups"), func() {
var (
+ er event.EventRegistry
db *mocks.MockDatabase
supportGroupHandler sg.SupportGroupHandler
filter *entity.SupportGroupFilter
options *entity.ListOptions
order []entity.Order
- handlerContext common.HandlerContext
)
BeforeEach(func() {
@@ -58,11 +65,9 @@ var _ = Describe("When listing SupportGroups", Label("app", "ListSupportGroups")
options = entity.NewListOptions()
filter = getSupportGroupFilter()
order = []entity.Order{}
- handlerContext = common.HandlerContext{
- DB: db,
- EventReg: er,
- Authz: authz,
- }
+ er = event.NewEventRegistry(db, handlerContext.Authz)
+ handlerContext.DB = db
+ handlerContext.EventReg = er
})
When("the list option does include the totalCount", func() {
@@ -125,12 +130,13 @@ var _ = Describe("When listing SupportGroups", Label("app", "ListSupportGroups")
var _ = Describe("When creating SupportGroup", Label("app", "CreateSupportGroup"), func() {
var (
+ er event.EventRegistry
db *mocks.MockDatabase
supportGroupHandler sg.SupportGroupHandler
supportGroup entity.SupportGroup
filter *entity.SupportGroupFilter
order []entity.Order
- handlerContext common.HandlerContext
+ p openfga.PermissionInput
)
BeforeEach(func() {
@@ -145,11 +151,17 @@ var _ = Describe("When creating SupportGroup", Label("app", "CreateSupportGroup"
After: &after,
},
}
- handlerContext = common.HandlerContext{
- DB: db,
- EventReg: er,
- Authz: authz,
+
+ p = openfga.PermissionInput{
+ UserType: "role",
+ UserId: "0",
+ ObjectId: "",
+ ObjectType: "support_group",
+ Relation: "role",
}
+ er = event.NewEventRegistry(db, handlerContext.Authz)
+ handlerContext.DB = db
+ handlerContext.EventReg = er
})
It("creates supportGroup", func() {
@@ -165,16 +177,38 @@ var _ = Describe("When creating SupportGroup", Label("app", "CreateSupportGroup"
Expect(newSupportGroup.CCRN).To(BeEquivalentTo(supportGroup.CCRN))
})
})
+
+ Context("when handling a CreateComponentInstanceEvent", func() {
+ Context("when new component instance is created", func() {
+ It("should add user resource relationship tuple in openfga", func() {
+ sgFake := test.NewFakeSupportGroupEntity()
+ createEvent := &sg.CreateSupportGroupEvent{
+ SupportGroup: &sgFake,
+ }
+
+ // Use type assertion to convert a CreateServiceEvent into an Event
+ var event event.Event = createEvent
+ p.ObjectId = openfga.ObjectIdFromInt(createEvent.SupportGroup.Id)
+
+ // Simulate event
+ sg.OnSupportGroupCreateAuthz(db, event, handlerContext.Authz)
+
+ ok, err := handlerContext.Authz.CheckPermission(p)
+ Expect(err).To(BeNil(), "no error should be thrown")
+ Expect(ok).To(BeTrue(), "permission should be granted")
+ })
+ })
+ })
})
var _ = Describe("When updating SupportGroup", Label("app", "UpdateSupportGroup"), func() {
var (
+ er event.EventRegistry
db *mocks.MockDatabase
supportGroupHandler sg.SupportGroupHandler
supportGroup entity.SupportGroupResult
filter *entity.SupportGroupFilter
order []entity.Order
- handlerContext common.HandlerContext
)
BeforeEach(func() {
@@ -189,11 +223,9 @@ var _ = Describe("When updating SupportGroup", Label("app", "UpdateSupportGroup"
After: &after,
},
}
- handlerContext = common.HandlerContext{
- DB: db,
- EventReg: er,
- Authz: authz,
- }
+ er = event.NewEventRegistry(db, handlerContext.Authz)
+ handlerContext.DB = db
+ handlerContext.EventReg = er
})
It("updates supportGroup", func() {
@@ -213,13 +245,13 @@ var _ = Describe("When updating SupportGroup", Label("app", "UpdateSupportGroup"
var _ = Describe("When deleting SupportGroup", Label("app", "DeleteSupportGroup"), func() {
var (
+ er event.EventRegistry
db *mocks.MockDatabase
supportGroupHandler sg.SupportGroupHandler
id int64
filter *entity.SupportGroupFilter
order []entity.Order
listOptions *entity.ListOptions
- handlerContext common.HandlerContext
)
BeforeEach(func() {
@@ -235,11 +267,9 @@ var _ = Describe("When deleting SupportGroup", Label("app", "DeleteSupportGroup"
After: &after,
},
}
- handlerContext = common.HandlerContext{
- DB: db,
- EventReg: er,
- Authz: authz,
- }
+ er = event.NewEventRegistry(db, handlerContext.Authz)
+ handlerContext.DB = db
+ handlerContext.EventReg = er
})
It("deletes supportGroup", func() {
@@ -255,17 +285,117 @@ var _ = Describe("When deleting SupportGroup", Label("app", "DeleteSupportGroup"
Expect(err).To(BeNil(), "no error should be thrown")
Expect(supportGroups.Elements).To(BeEmpty(), "no error should be thrown")
})
+
+ Context("when handling a DeleteSupportGroupEvent", func() {
+ Context("when new support group is deleted", func() {
+ It("should delete tuples related to that support group in openfga", func() {
+ // Test OnSupportGroupDeleteAuthz against all possible relations
+ sgFake := test.NewFakeSupportGroupEntity()
+ deleteEvent := &sg.DeleteSupportGroupEvent{
+ SupportGroupID: sgFake.Id,
+ }
+ objectId := openfga.ObjectIdFromInt(deleteEvent.SupportGroupID)
+ userId := openfga.UserIdFromInt(deleteEvent.SupportGroupID)
+
+ relations := []openfga.RelationInput{
+ { // user - support_group
+ UserType: openfga.TypeUser,
+ UserId: openfga.IDUser,
+ ObjectId: objectId,
+ ObjectType: openfga.TypeSupportGroup,
+ Relation: openfga.RelMember,
+ },
+ { // support_group - support_group
+ UserType: openfga.TypeSupportGroup,
+ UserId: userId,
+ ObjectId: objectId,
+ ObjectType: openfga.TypeSupportGroup,
+ Relation: openfga.RelSupportGroup,
+ },
+ { // role - support_group
+ UserType: openfga.TypeRole,
+ UserId: openfga.IDRole,
+ ObjectId: objectId,
+ ObjectType: openfga.TypeSupportGroup,
+ Relation: openfga.RelRole,
+ },
+ { // support_group - service
+ UserType: openfga.TypeSupportGroup,
+ UserId: userId,
+ ObjectId: openfga.IDService,
+ ObjectType: openfga.TypeService,
+ Relation: openfga.RelSupportGroup,
+ },
+ }
+
+ handlerContext.Authz.AddRelationBulk(relations)
+
+ // get the number of relations before deletion
+ relCountBefore := 0
+ for _, r := range relations {
+ relations, err := handlerContext.Authz.ListRelations(r)
+ if err != nil {
+ Expect(err).To(BeNil(), "no error should be thrown")
+ }
+ relCountBefore += len(relations)
+ }
+ Expect(relCountBefore).To(Equal(len(relations)), "all relations should exist before deletion")
+
+ // check that relations were created
+ for _, r := range relations {
+ ok, err := handlerContext.Authz.CheckPermission(openfga.PermissionInput{
+ UserType: r.UserType,
+ UserId: r.UserId,
+ ObjectType: r.ObjectType,
+ ObjectId: r.ObjectId,
+ Relation: r.Relation,
+ })
+ Expect(err).To(BeNil(), "no error should be thrown")
+ Expect(ok).To(BeTrue(), "permission should be granted")
+ }
+
+ var event event.Event = deleteEvent
+ // Simulate event
+ sg.OnSupportGroupDeleteAuthz(db, event, handlerContext.Authz)
+
+ // get the number of relations after deletion
+ relCountAfter := 0
+ for _, r := range relations {
+ relations, err := handlerContext.Authz.ListRelations(r)
+ if err != nil {
+ Expect(err).To(BeNil(), "no error should be thrown")
+ }
+ relCountAfter += len(relations)
+ }
+ Expect(relCountAfter < relCountBefore).To(BeTrue(), "less relations after deletion")
+ Expect(relCountAfter).To(BeEquivalentTo(0), "no relations should exist after deletion")
+
+ // verify that relations were deleted
+ for _, r := range relations {
+ ok, err := handlerContext.Authz.CheckPermission(openfga.PermissionInput{
+ UserType: r.UserType,
+ UserId: r.UserId,
+ ObjectType: r.ObjectType,
+ ObjectId: r.ObjectId,
+ Relation: r.Relation,
+ })
+ Expect(err).To(BeNil(), "no error should be thrown")
+ Expect(ok).To(BeFalse(), "permission should NOT be granted")
+ }
+ })
+ })
+ })
})
var _ = Describe("When modifying relationship of Service and SupportGroup", Label("app", "ServiceSupportGroupRelationship"), func() {
var (
+ er event.EventRegistry
db *mocks.MockDatabase
supportGroupHandler sg.SupportGroupHandler
service entity.Service
supportGroup entity.SupportGroupResult
filter *entity.SupportGroupFilter
order []entity.Order
- handlerContext common.HandlerContext
)
BeforeEach(func() {
@@ -282,11 +412,9 @@ var _ = Describe("When modifying relationship of Service and SupportGroup", Labe
},
Id: []*int64{&supportGroup.Id},
}
- handlerContext = common.HandlerContext{
- DB: db,
- EventReg: er,
- Authz: authz,
- }
+ er = event.NewEventRegistry(db, handlerContext.Authz)
+ handlerContext.DB = db
+ handlerContext.EventReg = er
})
It("adds service to supportGroup", func() {
@@ -298,6 +426,34 @@ var _ = Describe("When modifying relationship of Service and SupportGroup", Labe
Expect(supportGroup).NotTo(BeNil(), "supportGroup should be returned")
})
+ Context("when handling an AddServiceToSupportGroupEvent", func() {
+ It("should add the service-supportGroup relation tuple in openfga", func() {
+ sgFake := test.NewFakeSupportGroupResult()
+ serviceFake := test.NewFakeServiceEntity()
+ addEvent := &sg.AddServiceToSupportGroupEvent{
+ SupportGroupID: sgFake.Id,
+ ServiceID: serviceFake.Id,
+ }
+ supportGroupId := openfga.UserIdFromInt(addEvent.SupportGroupID)
+ serviceId := openfga.ObjectIdFromInt(addEvent.ServiceID)
+
+ rel := openfga.RelationInput{
+ UserType: openfga.TypeSupportGroup,
+ UserId: supportGroupId,
+ ObjectType: openfga.TypeService,
+ ObjectId: serviceId,
+ Relation: openfga.RelSupportGroup,
+ }
+
+ var event event.Event = addEvent
+ sg.OnAddServiceToSupportGroup(db, event, handlerContext.Authz)
+
+ remaining, err := handlerContext.Authz.ListRelations(rel)
+ Expect(err).To(BeNil(), "no error should be thrown")
+ Expect(remaining).NotTo(BeEmpty(), "relation should exist after addition")
+ })
+ })
+
It("removes service from supportGroup", func() {
db.On("RemoveServiceFromSupportGroup", supportGroup.Id, service.Id).Return(nil)
db.On("GetSupportGroups", filter, order).Return([]entity.SupportGroupResult{supportGroup}, nil)
@@ -306,17 +462,47 @@ var _ = Describe("When modifying relationship of Service and SupportGroup", Labe
Expect(err).To(BeNil(), "no error should be thrown")
Expect(supportGroup).NotTo(BeNil(), "supportGroup should be returned")
})
+
+ Context("when handling a RemoveServiceFromSupportGroupEvent", func() {
+ It("should remove the service-supportGroup relation tuple in openfga", func() {
+ sgFake := test.NewFakeSupportGroupResult()
+ serviceFake := test.NewFakeServiceEntity()
+ removeEvent := &sg.RemoveServiceFromSupportGroupEvent{
+ SupportGroupID: sgFake.Id,
+ ServiceID: serviceFake.Id,
+ }
+ supportGroupId := openfga.UserIdFromInt(removeEvent.SupportGroupID)
+ serviceId := openfga.ObjectIdFromInt(removeEvent.ServiceID)
+
+ rel := openfga.RelationInput{
+ UserType: openfga.TypeSupportGroup,
+ UserId: supportGroupId,
+ ObjectType: openfga.TypeService,
+ ObjectId: serviceId,
+ Relation: openfga.TypeSupportGroup,
+ }
+
+ handlerContext.Authz.AddRelationBulk([]openfga.RelationInput{rel})
+
+ var event event.Event = removeEvent
+ sg.OnRemoveServiceFromSupportGroup(db, event, handlerContext.Authz)
+
+ remaining, err := handlerContext.Authz.ListRelations(rel)
+ Expect(err).To(BeNil(), "no error should be thrown")
+ Expect(remaining).To(BeEmpty(), "relation should not exist after removal")
+ })
+ })
})
var _ = Describe("When modifying relationship of User and SupportGroup", Label("app", "UserSupportGroupRelationship"), func() {
var (
+ er event.EventRegistry
db *mocks.MockDatabase
supportGroupHandler sg.SupportGroupHandler
user entity.User
supportGroup entity.SupportGroupResult
filter *entity.SupportGroupFilter
order []entity.Order
- handlerContext common.HandlerContext
)
BeforeEach(func() {
@@ -333,11 +519,9 @@ var _ = Describe("When modifying relationship of User and SupportGroup", Label("
},
Id: []*int64{&supportGroup.Id},
}
- handlerContext = common.HandlerContext{
- DB: db,
- EventReg: er,
- Authz: authz,
- }
+ er = event.NewEventRegistry(db, handlerContext.Authz)
+ handlerContext.DB = db
+ handlerContext.EventReg = er
})
It("adds user to supportGroup", func() {
@@ -349,6 +533,34 @@ var _ = Describe("When modifying relationship of User and SupportGroup", Label("
Expect(supportGroup).NotTo(BeNil(), "supportGroup should be returned")
})
+ Context("when handling an AddUserToSupportGroupEvent", func() {
+ It("should add the user-supportGroup relation tuple in openfga", func() {
+ sgFake := test.NewFakeSupportGroupResult()
+ userFake := test.NewFakeUserEntity()
+ addEvent := &sg.AddUserToSupportGroupEvent{
+ SupportGroupID: sgFake.Id,
+ UserID: userFake.Id,
+ }
+ supportGroupId := openfga.ObjectIdFromInt(addEvent.SupportGroupID)
+ userId := openfga.UserIdFromInt(addEvent.UserID)
+
+ rel := openfga.RelationInput{
+ UserType: openfga.TypeUser,
+ UserId: userId,
+ ObjectType: openfga.TypeSupportGroup,
+ ObjectId: supportGroupId,
+ Relation: openfga.RelMember,
+ }
+
+ var event event.Event = addEvent
+ sg.OnAddUserToSupportGroup(db, event, handlerContext.Authz)
+
+ remaining, err := handlerContext.Authz.ListRelations(rel)
+ Expect(err).To(BeNil(), "no error should be thrown")
+ Expect(remaining).NotTo(BeEmpty(), "relation should exist after addition")
+ })
+ })
+
It("removes user from supportGroup", func() {
db.On("RemoveUserFromSupportGroup", supportGroup.Id, user.Id).Return(nil)
db.On("GetSupportGroups", filter, order).Return([]entity.SupportGroupResult{supportGroup}, nil)
@@ -357,16 +569,46 @@ var _ = Describe("When modifying relationship of User and SupportGroup", Label("
Expect(err).To(BeNil(), "no error should be thrown")
Expect(supportGroup).NotTo(BeNil(), "supportGroup should be returned")
})
+
+ Context("when handling a RemoveUserFromSupportGroupEvent", func() {
+ It("should remove the user-supportGroup relation tuple in openfga", func() {
+ sgFake := test.NewFakeSupportGroupResult()
+ userFake := test.NewFakeUserEntity()
+ removeEvent := &sg.RemoveUserFromSupportGroupEvent{
+ SupportGroupID: sgFake.Id,
+ UserID: userFake.Id,
+ }
+ supportGroupId := openfga.ObjectIdFromInt(removeEvent.SupportGroupID)
+ userId := openfga.UserIdFromInt(removeEvent.UserID)
+
+ rel := openfga.RelationInput{
+ UserType: openfga.TypeUser,
+ UserId: userId,
+ ObjectType: openfga.TypeSupportGroup,
+ ObjectId: supportGroupId,
+ Relation: openfga.RelMember,
+ }
+ // Bulk add instead of single add
+ handlerContext.Authz.AddRelationBulk([]openfga.RelationInput{rel})
+
+ var event event.Event = removeEvent
+ sg.OnRemoveUserFromSupportGroup(db, event, handlerContext.Authz)
+
+ remaining, err := handlerContext.Authz.ListRelations(rel)
+ Expect(err).To(BeNil(), "no error should be thrown")
+ Expect(remaining).To(BeEmpty(), "relation should not exist after removal")
+ })
+ })
})
var _ = Describe("When listing supportGroupCcrns", Label("app", "ListSupportGroupCcrns"), func() {
var (
+ er event.EventRegistry
db *mocks.MockDatabase
supportGroupHandler sg.SupportGroupHandler
filter *entity.SupportGroupFilter
options *entity.ListOptions
ccrn string
- handlerContext common.HandlerContext
)
BeforeEach(func() {
@@ -374,11 +616,9 @@ var _ = Describe("When listing supportGroupCcrns", Label("app", "ListSupportGrou
options = entity.NewListOptions()
filter = getSupportGroupFilter()
ccrn = "src"
- handlerContext = common.HandlerContext{
- DB: db,
- EventReg: er,
- Authz: authz,
- }
+ er = event.NewEventRegistry(db, handlerContext.Authz)
+ handlerContext.DB = db
+ handlerContext.EventReg = er
})
When("no filters are used", func() {
diff --git a/internal/app/user/user_handler_events.go b/internal/app/user/user_handler_events.go
index 3f6c4ee2..c3bbd3f0 100644
--- a/internal/app/user/user_handler_events.go
+++ b/internal/app/user/user_handler_events.go
@@ -5,7 +5,11 @@ package user
import (
"github.com/cloudoperators/heureka/internal/app/event"
+ "github.com/cloudoperators/heureka/internal/database"
"github.com/cloudoperators/heureka/internal/entity"
+ appErrors "github.com/cloudoperators/heureka/internal/errors"
+ "github.com/cloudoperators/heureka/internal/openfga"
+ "github.com/sirupsen/logrus"
)
const (
@@ -82,3 +86,71 @@ type ListUserNamesAndIdsEvent struct {
func (e *ListUserNamesAndIdsEvent) Name() event.EventName {
return ListUserNamesAndIdsEventName
}
+
+// OnServiceDeleteAuthz is a handler for the DeleteServiceEvent
+func OnUserDeleteAuthz(db database.Database, e event.Event, authz openfga.Authorization) {
+ op := appErrors.Op("OnUserDeleteAuthz")
+
+ deleteInput := []openfga.RelationInput{}
+
+ l := logrus.WithFields(logrus.Fields{
+ "event": "OnUserDeleteAuthz",
+ "payload": e,
+ })
+
+ if deleteEvent, ok := e.(*DeleteUserEvent); ok {
+ // Delete all tuples where object is the user
+ deleteInput = append(deleteInput, openfga.RelationInput{
+ ObjectType: openfga.TypeUser,
+ ObjectId: openfga.ObjectIdFromInt(deleteEvent.UserID),
+ })
+
+ // Delete all tuples where user is the user
+ // includes: service, component, component verison, component instance, issue match, support group, role
+ deleteInput = append(deleteInput, openfga.RelationInput{
+ UserType: openfga.TypeUser,
+ UserId: openfga.UserIdFromInt(deleteEvent.UserID),
+ ObjectType: openfga.TypeService,
+ })
+ deleteInput = append(deleteInput, openfga.RelationInput{
+ UserType: openfga.TypeUser,
+ UserId: openfga.UserIdFromInt(deleteEvent.UserID),
+ ObjectType: openfga.TypeComponent,
+ })
+ deleteInput = append(deleteInput, openfga.RelationInput{
+ UserType: openfga.TypeUser,
+ UserId: openfga.UserIdFromInt(deleteEvent.UserID),
+ ObjectType: openfga.TypeComponentVersion,
+ })
+ deleteInput = append(deleteInput, openfga.RelationInput{
+ UserType: openfga.TypeUser,
+ UserId: openfga.UserIdFromInt(deleteEvent.UserID),
+ ObjectType: openfga.TypeComponentInstance,
+ })
+ deleteInput = append(deleteInput, openfga.RelationInput{
+ UserType: openfga.TypeUser,
+ UserId: openfga.UserIdFromInt(deleteEvent.UserID),
+ ObjectType: openfga.TypeIssueMatch,
+ })
+ deleteInput = append(deleteInput, openfga.RelationInput{
+ UserType: openfga.TypeUser,
+ UserId: openfga.UserIdFromInt(deleteEvent.UserID),
+ ObjectType: openfga.TypeSupportGroup,
+ })
+ deleteInput = append(deleteInput, openfga.RelationInput{
+ UserType: openfga.TypeUser,
+ UserId: openfga.UserIdFromInt(deleteEvent.UserID),
+ ObjectType: openfga.TypeRole,
+ })
+
+ err := authz.RemoveRelationBulk(deleteInput)
+ if err != nil {
+ wrappedErr := appErrors.InternalError(string(op), "User", "", err)
+ l.Error(wrappedErr)
+ }
+ } else {
+ err := NewUserHandlerError("OnUserDeleteAuthz: triggered with wrong event type")
+ wrappedErr := appErrors.InternalError(string(op), "User", "", err)
+ l.Error(wrappedErr)
+ }
+}
diff --git a/internal/app/user/user_handler_test.go b/internal/app/user/user_handler_test.go
index 0cd26f7b..0843b257 100644
--- a/internal/app/user/user_handler_test.go
+++ b/internal/app/user/user_handler_test.go
@@ -10,10 +10,12 @@ import (
"github.com/cloudoperators/heureka/internal/app/common"
"github.com/cloudoperators/heureka/internal/app/event"
u "github.com/cloudoperators/heureka/internal/app/user"
+ "github.com/cloudoperators/heureka/internal/cache"
"github.com/cloudoperators/heureka/internal/entity"
"github.com/cloudoperators/heureka/internal/entity/test"
"github.com/cloudoperators/heureka/internal/mocks"
"github.com/cloudoperators/heureka/internal/openfga"
+ "github.com/cloudoperators/heureka/internal/util"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/samber/lo"
@@ -27,10 +29,22 @@ func TestUserHandler(t *testing.T) {
var er event.EventRegistry
var authz openfga.Authorization
+var handlerContext common.HandlerContext
+var cfg *util.Config
+var enableLogs bool
var _ = BeforeSuite(func() {
+ cfg = common.GetTestConfig()
+ enableLogs := false
db := mocks.NewMockDatabase(GinkgoT())
- er = event.NewEventRegistry(db)
+ authz = openfga.NewAuthorizationHandler(cfg, enableLogs)
+ er = event.NewEventRegistry(db, authz)
+ handlerContext = common.HandlerContext{
+ DB: db,
+ EventReg: er,
+ Cache: cache.NewNoCache(),
+ Authz: authz,
+ }
})
func getUserFilter() *entity.UserFilter {
@@ -124,8 +138,7 @@ var _ = Describe("When creating User", Label("app", "CreateUser"), func() {
db = mocks.NewMockDatabase(GinkgoT())
user = test.NewFakeUserEntity()
first := 10
- var after int64
- after = 0
+ after := int64(0)
filter = &entity.UserFilter{
Paginated: entity.Paginated{
First: &first,
@@ -168,8 +181,7 @@ var _ = Describe("When updating User", Label("app", "UpdateUser"), func() {
db = mocks.NewMockDatabase(GinkgoT())
user = test.NewFakeUserEntity()
first := 10
- var after int64
- after = 0
+ after := int64(0)
filter = &entity.UserFilter{
Paginated: entity.Paginated{
First: &first,
@@ -212,8 +224,7 @@ var _ = Describe("When deleting User", Label("app", "DeleteUser"), func() {
db = mocks.NewMockDatabase(GinkgoT())
id = 1
first := 10
- var after int64
- after = 0
+ after := int64(0)
filter = &entity.UserFilter{
Paginated: entity.Paginated{
First: &first,
@@ -240,7 +251,129 @@ var _ = Describe("When deleting User", Label("app", "DeleteUser"), func() {
Expect(err).To(BeNil(), "no error should be thrown")
Expect(users.Elements).To(BeEmpty(), "no error should be thrown")
})
+
+ Context("when handling a DeleteUserEvent", func() {
+ Context("when new user is deleted", func() {
+ It("should delete tuples related to that user in openfga", func() {
+ // Test OnUserDeleteAuthz against all possible relations
+ authz := openfga.NewAuthorizationHandler(cfg, enableLogs)
+ userFake := test.NewFakeUserEntity()
+ deleteEvent := &u.DeleteUserEvent{
+ UserID: userFake.Id,
+ }
+ userId := openfga.UserIdFromInt(deleteEvent.UserID)
+
+ relations := []openfga.RelationInput{
+ { // user - role
+ UserType: openfga.TypeUser,
+ UserId: userId,
+ ObjectId: openfga.IDRole,
+ ObjectType: openfga.TypeRole,
+ Relation: openfga.RelAdmin,
+ },
+ { // user - service
+ UserType: openfga.TypeUser,
+ UserId: userId,
+ ObjectId: openfga.IDService,
+ ObjectType: openfga.TypeService,
+ Relation: openfga.RelMember,
+ },
+ { // user - component_instance
+ UserType: openfga.TypeUser,
+ UserId: userId,
+ ObjectId: openfga.IDComponentInstance,
+ ObjectType: openfga.TypeComponentInstance,
+ Relation: openfga.RelCanView,
+ },
+ { // user - support_group
+ UserType: openfga.TypeUser,
+ UserId: userId,
+ ObjectId: openfga.IDSupportGroup,
+ ObjectType: openfga.TypeSupportGroup,
+ Relation: openfga.RelMember,
+ },
+ { // user - issue_match
+ UserType: openfga.TypeUser,
+ UserId: userId,
+ ObjectId: openfga.IDIssueMatch,
+ ObjectType: openfga.TypeIssueMatch,
+ Relation: openfga.RelCanView,
+ },
+ { // user - component_version
+ UserType: openfga.TypeUser,
+ UserId: userId,
+ ObjectId: openfga.IDComponentVersion,
+ ObjectType: openfga.TypeComponentVersion,
+ Relation: openfga.RelCanView,
+ },
+ { // user - component
+ UserType: openfga.TypeUser,
+ UserId: userId,
+ ObjectId: openfga.IDComponent,
+ ObjectType: openfga.TypeComponent,
+ Relation: openfga.RelCanView,
+ },
+ }
+
+ handlerContext.Authz.AddRelationBulk(relations)
+
+ // get the number of relations before deletion
+ relCountBefore := 0
+ for _, r := range relations {
+ relations, err := handlerContext.Authz.ListRelations(r)
+ if err != nil {
+ Expect(err).To(BeNil(), "no error should be thrown")
+ }
+ relCountBefore += len(relations)
+ }
+ Expect(relCountBefore).To(Equal(len(relations)), "all relations should exist before deletion")
+
+ // check that relations were created
+ for _, r := range relations {
+ ok, err := handlerContext.Authz.CheckPermission(openfga.PermissionInput{
+ UserType: r.UserType,
+ UserId: r.UserId,
+ ObjectType: r.ObjectType,
+ ObjectId: r.ObjectId,
+ Relation: r.Relation,
+ })
+ Expect(err).To(BeNil(), "no error should be thrown")
+ Expect(ok).To(BeTrue(), "permission should be granted")
+ }
+
+ var event event.Event = deleteEvent
+ // Simulate event
+ u.OnUserDeleteAuthz(db, event, authz)
+
+ // get the number of relations after deletion
+ relCountAfter := 0
+ for _, r := range relations {
+ relations, err := handlerContext.Authz.ListRelations(r)
+ if err != nil {
+ Expect(err).To(BeNil(), "no error should be thrown")
+ }
+ relCountAfter += len(relations)
+ }
+ Expect(relCountAfter < relCountBefore).To(BeTrue(), "less relations after deletion")
+ Expect(relCountAfter).To(BeEquivalentTo(0), "no relations should exist after deletion")
+
+ // verify that relations were deleted
+ for _, r := range relations {
+ ok, err := handlerContext.Authz.CheckPermission(openfga.PermissionInput{
+ UserType: r.UserType,
+ UserId: r.UserId,
+ ObjectType: r.ObjectType,
+ ObjectId: r.ObjectId,
+ Relation: r.Relation,
+ })
+ Expect(err).To(BeNil(), "no error should be thrown")
+ Expect(ok).To(BeFalse(), "permission should NOT be granted")
+ }
+ })
+ })
+ })
})
+
var _ = Describe("When listing User", Label("app", "ListUserNames"), func() {
var (
db *mocks.MockDatabase
diff --git a/internal/openfga/authz.go b/internal/openfga/authz.go
index 8692abfa..fbf2d141 100644
--- a/internal/openfga/authz.go
+++ b/internal/openfga/authz.go
@@ -6,6 +6,7 @@ package openfga
import (
"context"
"encoding/json"
+ "errors"
"os"
"strings"
@@ -40,7 +41,7 @@ func NewAuthz(l *logrus.Logger, cfg *util.Config) Authorization {
}
// Check if the store already exists, otherwise create it
- storeId, err := CheckStore(fgaClient, cfg.AuthzOpenFgaStoreName)
+ storeId, err := checkStore(fgaClient, cfg.AuthzOpenFgaStoreName)
if err != nil {
l.Error("Could not list OpenFGA stores: ", err)
return nil
@@ -58,7 +59,7 @@ func NewAuthz(l *logrus.Logger, cfg *util.Config) Authorization {
fgaClient.SetStoreId(storeId)
// Check if the model already exists, otherwise create it
- modelId, err := CheckModel(fgaClient, storeId)
+ modelId, err := checkModel(fgaClient, storeId)
if err != nil {
l.Error("Could not list OpenFGA models: ", err)
return nil
@@ -109,8 +110,8 @@ func getAuthModelRequestFromFile(filePath string) (*client.ClientWriteAuthorizat
return &modelRequest, nil
}
-// CheckStore checks if a store with the given name exists in OpenFGA.
-func CheckStore(fgaClient *client.OpenFgaClient, storeName string) (string, error) {
+// checkStore checks if a store with the given name exists in OpenFGA.
+func checkStore(fgaClient *client.OpenFgaClient, storeName string) (string, error) {
storesResponse, err := fgaClient.ListStores(context.Background()).Execute()
if err != nil {
return "", err
@@ -123,8 +124,8 @@ func CheckStore(fgaClient *client.OpenFgaClient, storeName string) (string, erro
return "", nil
}
-// CheckModel checks if an authorization model exists in OpenFGA for the given store.
-func CheckModel(fgaClient *client.OpenFgaClient, storeId string) (string, error) {
+// checkModel checks if an authorization model exists in OpenFGA for the given store.
+func checkModel(fgaClient *client.OpenFgaClient, storeId string) (string, error) {
modelsResponse, err := fgaClient.ReadAuthorizationModels(context.Background()).Options(
client.ClientReadAuthorizationModelsOptions{StoreId: &storeId},
).Execute()
@@ -137,8 +138,8 @@ func CheckModel(fgaClient *client.OpenFgaClient, storeId string) (string, error)
return "", nil
}
-// CheckTuple checks if a specific tuple exists in OpenFGA.
-func (a *Authz) CheckTuple(r RelationInput) (bool, error) {
+// checkTuple checks if a specific tuple exists in OpenFGA.
+func (a *Authz) checkTuple(r RelationInput) (bool, error) {
userString := string(r.UserType) + ":" + string(r.UserId)
relationString := string(r.Relation)
objectString := string(r.ObjectType) + ":" + string(r.ObjectId)
@@ -150,14 +151,14 @@ func (a *Authz) CheckTuple(r RelationInput) (bool, error) {
}
resp, err := a.client.Read(context.Background()).Body(req).Execute()
if err != nil {
- a.logger.Errorf("OpenFGA Read (CheckTuple) error: %v", err)
+ a.logger.Errorf("OpenFGA Read (checkTuple) error: %v", err)
return false, err
}
return len(resp.Tuples) > 0, nil
}
-// CheckPermission checks if userId has permission on resourceId.
+// CheckPermission checks if userId has permission on objectId.
func (a *Authz) CheckPermission(p PermissionInput) (bool, error) {
req := client.ClientCheckRequest{
User: string(p.UserType) + ":" + string(p.UserId),
@@ -172,59 +173,261 @@ func (a *Authz) CheckPermission(p PermissionInput) (bool, error) {
return resp.GetAllowed(), nil
}
-// AddRelation adds a relationship between userId and resourceId.
+// AddRelation adds a specified relationship between userId and objectId.
func (a *Authz) AddRelation(r RelationInput) error {
- if ok, err := a.CheckTuple(r); err != nil {
+ l := a.logRel("HandleAddAuthzRelation", r)
+
+ // Check to avoid duplicate writes
+ ok, err := a.checkTuple(r)
+ if err != nil {
+ l.WithField("event-step", "OpenFGA Read").
+ WithError(err).
+ Error("Failed to read relation before add")
return err
- } else if !ok {
- tuple := client.ClientWriteRequest{
- Writes: []client.ClientTupleKey{
- {
- User: string(r.UserType) + ":" + string(r.UserId),
- Relation: string(r.Relation),
- Object: string(r.ObjectType) + ":" + string(r.ObjectId),
- },
+ }
+ if ok {
+ l.WithField("event-step", "OpenFGA Read").
+ Info("Relation already exists; skipping add")
+ return nil
+ }
+
+ // Write relation
+ tuple := client.ClientWriteRequest{
+ Writes: []client.ClientTupleKey{
+ {
+ User: string(r.UserType) + ":" + string(r.UserId),
+ Relation: string(r.Relation),
+ Object: string(r.ObjectType) + ":" + string(r.ObjectId),
},
- }
- resp, err := a.client.Write(context.Background()).Body(tuple).Execute()
- if err != nil {
- a.logger.Errorf("OpenFGA Write (AddRelation) error: %v", err)
- } else {
- a.logger.Infof("OpenFGA Write (AddRelation): %v | Added relation %s for user %s on resource %s", resp, r.Relation, r.UserId, r.ObjectId)
- }
+ },
+ }
+ _, err = a.client.Write(context.Background()).Body(tuple).Execute()
+ if err != nil {
+ l.WithField("event-step", "OpenFGA AddRelation").
+ WithError(err).
+ Errorf("Error while adding relation tuple: (%s, %s, %s, %s)", r.UserId, r.ObjectId, r.ObjectType, r.Relation)
return err
- } else {
- a.logger.Infof("Relation %s for user %s on resource %s already exists", r.Relation, r.UserId, r.ObjectId)
}
+
+ l.WithField("event-step", "OpenFGA AddRelation").
+ Infof("Added relation tuple: (%s, %s, %s, %s)", r.UserId, r.ObjectId, r.ObjectType, r.Relation)
return nil
}
-// RemoveRelation removes a relationship between userId and resourceId.
+// AddRelationBulk adds multiple specified relationships between userId(s) and objectId(s).
+func (a *Authz) AddRelationBulk(relations []RelationInput) error {
+ l := a.logger.WithFields(logrus.Fields{
+ "event": "HandleAddAuthzRelationBulk",
+ "relationCount": len(relations),
+ })
+
+ options := client.ClientWriteOptions{
+ Conflict: client.ClientWriteConflictOptions{
+ OnDuplicateWrites: client.CLIENT_WRITE_REQUEST_ON_DUPLICATE_WRITES_IGNORE,
+ },
+ }
+
+ tupleStrings := make([]client.ClientTupleKey, 0, len(relations))
+ for _, rel := range relations {
+ tupleStrings = append(tupleStrings, client.ClientTupleKey{
+ User: string(rel.UserType) + ":" + string(rel.UserId),
+ Relation: string(rel.Relation),
+ Object: string(rel.ObjectType) + ":" + string(rel.ObjectId),
+ })
+ }
+
+ tuple := client.ClientWriteRequest{
+ Writes: tupleStrings,
+ }
+
+ _, err := a.client.Write(context.Background()).Body(tuple).Options(options).Execute()
+ if err != nil {
+ l.WithField("event-step", "OpenFGA AddRelationsBulk").
+ WithError(err).
+ Error("Failed to add relations")
+ return err
+ }
+
+ l.WithField("event-step", "OpenFGA AddRelationsBulk").
+ WithField("added", len(tupleStrings)).
+ Info("Added relations")
+ return nil
+}
+
+// RemoveRelation removes a relationship between userId and objectId.
func (a *Authz) RemoveRelation(r RelationInput) error {
- if ok, err := a.CheckTuple(r); err != nil {
+ l := a.logRel("HandleRemoveAuthzRelation", r)
+
+ // Check existence before delete
+ ok, err := a.checkTuple(r)
+ if err != nil {
+ l.WithField("event-step", "OpenFGA Read").
+ WithError(err).
+ Error("Failed to read relation for deletion")
return err
- } else if ok {
- tuple := client.ClientWriteRequest{
- Deletes: []client.ClientTupleKeyWithoutCondition{
- {
- User: string(r.UserType) + ":" + string(r.UserId),
- Relation: string(r.Relation),
- Object: string(r.ObjectType) + ":" + string(r.ObjectId),
- },
+ }
+ if !ok {
+ l.WithField("event-step", "OpenFGA Read").
+ Info("No matching relation to delete")
+ return nil
+ }
+
+ // Delete the relation
+ writeReq := client.ClientWriteRequest{
+ Deletes: []client.ClientTupleKeyWithoutCondition{
+ {
+ User: string(r.UserType) + ":" + string(r.UserId),
+ Relation: string(r.Relation),
+ Object: string(r.ObjectType) + ":" + string(r.ObjectId),
},
- }
- _, err := a.client.Write(context.Background()).Body(tuple).Execute()
+ },
+ }
+ options := client.ClientWriteOptions{
+ Conflict: client.ClientWriteConflictOptions{
+ OnMissingDeletes: client.CLIENT_WRITE_REQUEST_ON_MISSING_DELETES_IGNORE,
+ },
+ }
+ _, err = a.client.Write(context.Background()).Body(writeReq).Options(options).Execute()
+ if err != nil {
+ l.WithField("event-step", "OpenFGA DeleteRelations").
+ WithError(err).
+ Errorf("Error while deleting relation tuple: (%s, %s, %s, %s)", r.UserId, r.ObjectId, r.ObjectType, r.Relation)
+ return err
+ }
+
+ l.WithField("event-step", "OpenFGA DeleteRelations").
+ WithField("deleted", 1).
+ Infof("Deleted relation tuple: (%s, %s, %s, %s)", r.UserId, r.ObjectId, r.ObjectType, r.Relation)
+ return nil
+}
+
+// RemoveRelationBulk removes all relations that match the given RelationInput as filters.
+func (a *Authz) RemoveRelationBulk(r []RelationInput) error {
+ l := a.logger.WithFields(logrus.Fields{
+ "event": "HandleRemoveAuthzRelationBulk",
+ "filterCount": len(r),
+ })
+
+ // Collect all matching tuples for given filters
+ tuples := []client.ClientTupleKeyWithoutCondition{}
+ for _, rel := range r {
+ found, err := a.ListRelations(rel)
if err != nil {
- a.logger.Errorf("OpenFGA Write (RemoveRelation) error: %v", err)
+ l.WithField("event-step", "OpenFGA ListRelations").
+ WithError(err).
+ Error("Failed to read relations for deletion")
+ return err
}
+ tuples = append(tuples, found...)
+ }
+
+ if len(tuples) == 0 {
+ l.WithField("event-step", "OpenFGA ListRelations").
+ Info("No matching relations to delete")
+ return nil
+ }
+
+ writeReq := client.ClientWriteRequest{
+ Deletes: tuples,
+ }
+ options := client.ClientWriteOptions{
+ Conflict: client.ClientWriteConflictOptions{
+ OnMissingDeletes: client.CLIENT_WRITE_REQUEST_ON_MISSING_DELETES_IGNORE,
+ },
+ }
+ _, err := a.client.Write(context.Background()).Body(writeReq).Options(options).Execute()
+ if err != nil {
+ l.WithField("event-step", "OpenFGA DeleteRelations").
+ WithField("attemptedDeletes", len(tuples)).
+ WithError(err).
+ Error("Failed to delete relations")
+ return err
+ }
+
+ l.WithField("event-step", "OpenFGA DeleteRelations").
+ WithField("deleted", len(tuples)).
+ Info("Deleted relations")
+ return nil
+}
+
+// UpdateRelation updates relations by removing relations that match the filter for the old relation and adding the new relation.
+func (a *Authz) UpdateRelation(add RelationInput, rem RelationInput) error {
+ l := a.logRel("HandleUpdateAuthzRelation", rem)
+
+ if err := a.RemoveRelationBulk([]RelationInput{rem}); err != nil {
+ l.WithField("event-step", "OpenFGA RemoveRelationBulk").
+ WithError(err).
+ Errorf("Error while removing relation tuple: (%s, %s, %s, %s)", rem.UserId, rem.ObjectId, rem.ObjectType, rem.Relation)
return err
- } else {
- a.logger.Infof("Relation %s for user %s on resource %s doesn't exist", r.Relation, r.UserId, r.ObjectId)
}
+ l.WithField("event-step", "OpenFGA RemoveRelationBulk").
+ Infof("Removed relation tuple: (%s, %s, %s, %s)", rem.UserId, rem.ObjectId, rem.ObjectType, rem.Relation)
+
+ if err := a.AddRelation(add); err != nil {
+ // switch log context to the add relation
+ a.logRel("HandleUpdateAuthzRelation", add).
+ WithField("event-step", "OpenFGA AddRelation").
+ WithError(err).
+ Errorf("Error while adding relation tuple: (%s, %s, %s, %s)", add.UserId, add.ObjectId, add.ObjectType, add.Relation)
+ return err
+ }
+ a.logRel("HandleUpdateAuthzRelation", add).
+ WithField("event-step", "OpenFGA AddRelation").
+ Infof("Added relation tuple: (%s, %s, %s, %s)", add.UserId, add.ObjectId, add.ObjectType, add.Relation)
+
return nil
}
-// ListAccessibleResources returns a list of resource Ids that the user can access.
+// ListRelations lists all relations that match any given RelationInput as filter(s)
+func (a *Authz) ListRelations(filter RelationInput) ([]client.ClientTupleKeyWithoutCondition, error) {
+ // openfga POST read relation tuple requirements to be checked for:
+ // tuple_key (the filter object itself) is optional, if not provided, all tuples are returned
+ // object is mandatory if a tuple_key is provided, but objectId is not necessary, just a type can be specified
+ // user is mandatory only if object is specified in type only (if object type and id are both specified, user is optional)
+ // if user is specified, it must have both type and id, not just a type or id alone
+
+ // convert relationinput filters to a client read request
+ var userStr, relationStr, objectStr string
+
+ if filter.UserType != "" && filter.UserId != "" {
+ userStr = string(filter.UserType) + ":" + string(filter.UserId)
+ }
+ if filter.Relation != "" {
+ relationStr = string(filter.Relation)
+ }
+ if filter.ObjectType != "" {
+ objectStr = string(filter.ObjectType) + ":"
+ if filter.ObjectId != "" {
+ objectStr += string(filter.ObjectId)
+ }
+ } else {
+ return nil, errors.New("objectType must be specified in the filter")
+ }
+
+ req := client.ClientReadRequest{
+ User: &userStr,
+ Relation: &relationStr,
+ Object: &objectStr,
+ }
+ resp, err := a.client.Read(context.Background()).Body(req).Execute()
+ if err != nil {
+ a.logger.Errorf("OpenFGA Read (ListRelations) error: %v", err)
+ return nil, err
+ }
+
+ // convert response tuples to []client.ClientTupleKeyWithoutCondition
+ var tuples []client.ClientTupleKeyWithoutCondition
+ for _, tuple := range resp.Tuples {
+ tuples = append(tuples, client.ClientTupleKeyWithoutCondition{
+ User: tuple.Key.User,
+ Relation: tuple.Key.Relation,
+ Object: tuple.Key.Object,
+ })
+ }
+ return tuples, nil
+}
+
+// ListAccessibleResources returns a list of objectIds of a certain objectType that the user can access.
func (a *Authz) ListAccessibleResources(p PermissionInput) ([]AccessibleResource, error) {
body := client.ClientListObjectsRequest{
User: string(p.UserType) + ":" + string(p.UserId),
diff --git a/internal/openfga/authz_test.go b/internal/openfga/authz_test.go
index 380bf8f9..c36f66ef 100644
--- a/internal/openfga/authz_test.go
+++ b/internal/openfga/authz_test.go
@@ -176,6 +176,81 @@ var _ = Describe("Authz", func() {
})
})
+ Describe("ListResources", func() {
+ It("should return nothing for a non-existing relation input", func() {
+ input := openfga.RelationInput{
+ UserType: userType,
+ UserId: "non_existing_user",
+ Relation: ownerRel,
+ ObjectType: documentType,
+ ObjectId: "non_existing_document",
+ }
+ relations, err := authz.ListRelations(input)
+ Expect(err).To(BeNil())
+ Expect(relations).To(BeEmpty())
+ })
+
+ It("should return existing relation", func() {
+ newRelation := openfga.RelationInput{
+ UserType: userType,
+ UserId: "new_user",
+ Relation: ownerRel,
+ ObjectType: documentType,
+ ObjectId: "new_document",
+ }
+ r = newRelation
+ err := authz.AddRelation(r)
+ Expect(err).To(BeNil())
+
+ relations, err := authz.ListRelations(r)
+ Expect(err).To(BeNil())
+ Expect(len(relations)).To(Equal(1))
+ Expect(relations[0].User).To(Equal("user:new_user"))
+ Expect(relations[0].Relation).To(Equal(ownerRel))
+ Expect(relations[0].Object).To(Equal("document:new_document"))
+ })
+
+ It("should return multiple existing relations", func() {
+ newRelation1 := openfga.RelationInput{
+ UserType: userType,
+ UserId: "new_user_1",
+ Relation: ownerRel,
+ ObjectType: documentType,
+ ObjectId: "new_document1",
+ }
+ err := authz.AddRelation(newRelation1)
+ Expect(err).To(BeNil())
+ newRelation2 := openfga.RelationInput{
+ UserType: userType,
+ UserId: "new_user_1",
+ Relation: ownerRel,
+ ObjectType: documentType,
+ ObjectId: "new_document2",
+ }
+ err = authz.AddRelation(newRelation2)
+ Expect(err).To(BeNil())
+ newRelation3 := openfga.RelationInput{
+ UserType: userType,
+ UserId: "new_user_1",
+ Relation: ownerRel,
+ ObjectType: documentType,
+ ObjectId: "new_document3",
+ }
+ err = authz.AddRelation(newRelation3)
+ Expect(err).To(BeNil())
+
+ listRelInput := openfga.RelationInput{
+ UserType: userType,
+ UserId: "new_user_1",
+ Relation: ownerRel,
+ ObjectType: documentType,
+ }
+ relations, err := authz.ListRelations(listRelInput)
+ Expect(err).To(BeNil())
+ Expect(len(relations)).To(Equal(3))
+ })
+ })
+
Describe("ListAccessibleResources", func() {
It("should return an empty slice and no error", func() {
p.ObjectId = "read"
diff --git a/internal/openfga/helpers.go b/internal/openfga/helpers.go
new file mode 100644
index 00000000..87905ae9
--- /dev/null
+++ b/internal/openfga/helpers.go
@@ -0,0 +1,49 @@
+package openfga
+
+import (
+ "strconv"
+
+ "github.com/sirupsen/logrus"
+)
+
+// ObjectIdFromInt converts a numeric ID to an OpenFGA ObjectId.
+func ObjectIdFromInt(id int64) ObjectId {
+ return ObjectId(strconv.FormatInt(id, 10))
+}
+
+// UserIdFromInt converts an int ID to an OpenFGA UserId.
+func UserIdFromInt(id int64) UserId {
+ return UserId(strconv.FormatInt(id, 10))
+}
+
+// matchesFilter checks if the given userParts and objectParts match the filters specified in RelationInput.
+func matchesFilter(userParts, objectParts []string, r RelationInput, relation string) bool {
+ if r.UserType != "" && (len(userParts) < 1 || userParts[0] != string(r.UserType)) {
+ return false
+ }
+ if r.UserId != "" && (len(userParts) < 2 || userParts[1] != string(r.UserId)) {
+ return false
+ }
+ if r.Relation != "" && relation != string(r.Relation) {
+ return false
+ }
+ if r.ObjectType != "" && (len(objectParts) < 1 || objectParts[0] != string(r.ObjectType)) {
+ return false
+ }
+ if r.ObjectId != "" && (len(objectParts) < 2 || objectParts[1] != string(r.ObjectId)) {
+ return false
+ }
+ return true
+}
+
+// helper: build a consistent log entry for a relation input
+func (a *Authz) logRel(event string, r RelationInput) *logrus.Entry {
+ return a.logger.WithFields(logrus.Fields{
+ "event": event,
+ "userType": r.UserType,
+ "user": r.UserId,
+ "relation": r.Relation,
+ "objectType": r.ObjectType,
+ "objectId": r.ObjectId,
+ })
+}
diff --git a/internal/openfga/interface.go b/internal/openfga/interface.go
index 72c9f21d..eeac6266 100644
--- a/internal/openfga/interface.go
+++ b/internal/openfga/interface.go
@@ -7,6 +7,7 @@ import (
"io/ioutil"
"github.com/cloudoperators/heureka/internal/util"
+ "github.com/openfga/go-sdk/client"
"github.com/sirupsen/logrus"
)
@@ -16,12 +17,49 @@ type ObjectType string
type RelationType string
type ObjectId string
+// IDs (shared across userId/objectId tuple definitions)
+const (
+ IDUser = "userID"
+ IDRole = "roleID"
+ IDSupportGroup = "supportGroupID"
+ IDService = "serviceID"
+ IDComponent = "componentID"
+ IDComponentVersion = "componentVersionID"
+ IDComponentInstance = "componentInstanceID"
+ IDIssueMatch = "issueMatchID"
+)
+
+// Types (shared across userType/objectType tuple definitions)
+const (
+ TypeUser = "user"
+ TypeRole = "role"
+ TypeSupportGroup = "support_group"
+ TypeService = "service"
+ TypeComponent = "component"
+ TypeComponentVersion = "component_version"
+ TypeComponentInstance = "component_instance"
+ TypeIssueMatch = "issue_match"
+)
+
+// Relations (shared across relations tuple definitions)
+const (
+ RelCanView = "can_view"
+ RelRole = "role"
+ RelSupportGroup = "support_group"
+ RelRelatedService = "related_service"
+ RelOwner = "owner"
+ RelAdmin = "admin"
+ RelMember = "member"
+ RelComponentInstance = "component_instance"
+ RelComponentVersion = "component_version"
+)
+
type PermissionInput struct {
UserType UserType
UserId UserId
Relation RelationType
ObjectType ObjectType
- ObjectId string
+ ObjectId ObjectId
}
type RelationInput struct {
@@ -29,7 +67,7 @@ type RelationInput struct {
UserId UserId
Relation RelationType
ObjectType ObjectType
- ObjectId string
+ ObjectId ObjectId
}
type AccessibleResource struct {
@@ -38,12 +76,20 @@ type AccessibleResource struct {
}
type Authorization interface {
- // check if userId has permission on resourceId
+ // Check if userId has permission on resourceId
CheckPermission(p PermissionInput) (bool, error)
- // add relationship between userId and resourceId
+ // Add relationship between userId and resourceId
AddRelation(r RelationInput) error
- // remove relationship between userId and resourceId
+ // Add multiple relationships between userId and resourceId
+ AddRelationBulk(r []RelationInput) error
+ // Remove a single relationship between userId and resourceId
RemoveRelation(r RelationInput) error
+ // Remove all relations that match any given RelationInput as filters
+ RemoveRelationBulk(r []RelationInput) error
+ // Update relations based on filters provided
+ UpdateRelation(r RelationInput, u RelationInput) error
+ // List Relations based on multiple filters
+ ListRelations(filters RelationInput) ([]client.ClientTupleKeyWithoutCondition, error)
// ListAccessibleResources returns a list of resource Ids that the user can access.
ListAccessibleResources(p PermissionInput) ([]AccessibleResource, error)
}
diff --git a/internal/openfga/model/model.fga b/internal/openfga/model/model.fga
index 28778b6c..c1ff1511 100644
--- a/internal/openfga/model/model.fga
+++ b/internal/openfga/model/model.fga
@@ -10,7 +10,7 @@ type user
type role
relations
define admin: [user]
- define component_scanner: [user]
+ define component_scanner: [user]
define issue_scanner: [user]
type support_group
diff --git a/internal/openfga/noauthz.go b/internal/openfga/noauthz.go
index a95a27e7..86cd5e9f 100644
--- a/internal/openfga/noauthz.go
+++ b/internal/openfga/noauthz.go
@@ -5,6 +5,7 @@ package openfga
import (
"github.com/cloudoperators/heureka/internal/util"
+ "github.com/openfga/go-sdk/client"
)
type NoAuthz struct {
@@ -27,11 +28,31 @@ func (a *NoAuthz) AddRelation(r RelationInput) error {
return nil
}
+// AddRelationBulk adds multiple relationships between userId and resourceId.
+func (a *NoAuthz) AddRelationBulk(r []RelationInput) error {
+ return nil
+}
+
// RemoveRelation removes a relationship between userId and resourceId.
func (a *NoAuthz) RemoveRelation(r RelationInput) error {
return nil
}
+// DeleteObjectRelations deletes all relations for a given object.
+func (a *NoAuthz) RemoveRelationBulk(input []RelationInput) error {
+ return nil
+}
+
+// UpdateRelation updates a relationship between userId and resourceId.
+func (a *NoAuthz) UpdateRelation(r RelationInput, u RelationInput) error {
+ return nil
+}
+
+// ListRelations lists all relations for a given input.
+func (a *NoAuthz) ListRelations(input RelationInput) ([]client.ClientTupleKeyWithoutCondition, error) {
+ return []client.ClientTupleKeyWithoutCondition{}, nil
+}
+
// ListAccessibleResources returns a list of resource Ids that the user can access.
func (a *NoAuthz) ListAccessibleResources(p PermissionInput) ([]AccessibleResource, error) {
resources := []AccessibleResource{}
diff --git a/internal/util/config.go b/internal/util/config.go
index 7a9ac885..facb2f87 100644
--- a/internal/util/config.go
+++ b/internal/util/config.go
@@ -33,16 +33,18 @@ type Config struct {
//Environment string `envconfig:"ENVIRONMENT" required:"true" json:"environment"`
//// https://pkg.go.dev/github.com/robfig/cron#hdr-Predefined_schedules
//DiscoverySchedule string `envconfig:"DISOVERY_SCHEDULE" default:"0 0 0 * * *" json:"discoverySchedule"`
- SeedMode bool `envconfig:"SEED_MODE" required:"false" default:"false" json:"seedMode"`
- AuthTokenSecret string `envconfig:"AUTH_TOKEN_SECRET" required:"false" json:"-"`
- AuthOidcClientId string `envconfig:"AUTH_OIDC_CLIENT_ID" required:"false" json:"-"`
- AuthOidcUrl string `envconfig:"AUTH_OIDC_URL" required:"false" json:"-"`
- AuthzOpenFgaApiUrl string `envconfig:"AUTHZ_FGA_API_URL" required:"false" json:"-"`
- AuthzOpenFgaApiToken string `envconfig:"AUTHZ_FGA_API_TOKEN" required:"false" json:"-"`
- AuthzOpenFgaStoreName string `envconfig:"AUTHZ_FGA_STORE_NAME" required:"false" json:"-"`
- AuthzModelFilePath string `envconfig:"AUTHZ_MODEL_FILE_PATH" required:"false" json:"-"`
- DefaultIssuePriority int64 `envconfig:"DEFAULT_ISSUE_PRIORITY" default:"100" json:"defaultIssuePriority"`
- DefaultRepositoryName string `envconfig:"DEFAULT_REPOSITORY_NAME" default:"nvd" json:"defaultRepositoryName"`
+ SeedMode bool `envconfig:"SEED_MODE" required:"false" default:"false" json:"seedMode"`
+ AuthTokenSecret string `envconfig:"AUTH_TOKEN_SECRET" required:"false" json:"-"`
+ AuthOidcClientId string `envconfig:"AUTH_OIDC_CLIENT_ID" required:"false" json:"-"`
+ AuthOidcUrl string `envconfig:"AUTH_OIDC_URL" required:"false" json:"-"`
+ AuthzOpenFgaApiUrl string `envconfig:"AUTHZ_FGA_API_URL" required:"false" json:"-"`
+ AuthzOpenFgaApiToken string `envconfig:"AUTHZ_FGA_API_TOKEN" required:"false" json:"-"`
+ AuthzOpenFgaStoreName string `envconfig:"AUTHZ_FGA_STORE_NAME" required:"false" json:"-"`
+ AuthzModelFilePath string `envconfig:"AUTHZ_MODEL_FILE_PATH" required:"false" json:"-"`
+ DefaultIssuePriority int64 `envconfig:"DEFAULT_ISSUE_PRIORITY" default:"100" json:"defaultIssuePriority"`
+ DefaultRepositoryName string `envconfig:"DEFAULT_REPOSITORY_NAME" default:"nvd" json:"defaultRepositoryName"`
+ // CurrentUser is a placeholder variable to be implemented for future user context functionality
+ CurrentUser string `envconfig:"CURRENT_USER" required:"false" default:"heureka-admin" json:"currentUser"`
CacheEnable bool `envconfig:"CACHE_ENABLE" default:"false" json:"-"`
CacheValkeyUrl string `envconfig:"CACHE_VALKEY_URL" default:"" json:"-"`
CacheValkeyPassword string `envconfig:"CACHE_VALKEY_PASSWORD" default:"" json:"-"`