diff --git a/CLAUDE.md b/CLAUDE.md index d4c1ee5..e65de4c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -160,16 +160,56 @@ Complete API endpoint documentation and NATS message handlers are now documented "lfx.projects-api.get_slug" // Get project slug by UID "lfx.projects-api.get_logo" // Get project logo URL by UID "lfx.projects-api.slug_to_uid" // Convert slug to UID +"lfx.projects-api.get_parent_uid" // Get parent project UID // Outbound events (published by this service) -"lfx.index.project" // Project created/updated for indexing -"lfx.index.project_settings" // Settings updated for indexing -"lfx.update_access.project" // Project access control updates -"lfx.update_access.project_settings" // Project settings access control updates -"lfx.delete_all_access.project" // Project access control deletion -"lfx.delete_all_access.project_settings" // Project settings access control deletion +"lfx.index.project" // Project created/updated/deleted for indexing +"lfx.index.project_settings" // Settings created/updated for indexing +"lfx.projects-api.project_settings.updated" // Settings changed (before/after) +"lfx.fga-sync.update_access" // Generic FGA access control updates +"lfx.fga-sync.delete_access" // Generic FGA access control deletion ``` +### FGA Sync Message Format + +The service uses the generic FGA sync handlers for access control. All messages use the `GenericFGAMessage` envelope: + +```go +// Update access control (full sync) +GenericFGAMessage{ + ObjectType: "project", + Operation: "update_access", + Data: UpdateAccessData{ + UID: "project-uid", + Public: true, + Relations: map[string][]string{ + "writer": []string{"username1", "username2"}, + "auditor": []string{"username3"}, + "meeting_coordinator": []string{"username4"}, + }, + References: map[string][]string{ + "parent": []string{"project:parent-uid"}, + }, + }, +} + +// Delete all access control +GenericFGAMessage{ + ObjectType: "project", + Operation: "delete_access", + Data: DeleteAccessData{ + UID: "project-uid", + }, +} +``` + +**Key Points:** + +- Relations map user roles to usernames (e.g., `"writer": ["user1", "user2"]`) +- References map object relationships with formatted UIDs (e.g., `"parent": ["project:parent-uid"]`) +- Update operations are full sync - any relations not included will be removed +- Delete operations remove all access control tuples for the resource + ## Testing Patterns ### Unit Tests diff --git a/README.md b/README.md index ab981f0..0663eb4 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,10 @@ This service handles the following NATS subjects for inter-service communication This service publishes the following NATS events: +#### Project Data Events + +- `lfx.index.project`: Published when a project is created, updated, or deleted. Contains the project base data and tags for indexing. +- `lfx.index.project_settings`: Published when project settings are created or updated. Contains the project settings data and tags for indexing. - `lfx.projects-api.project_settings.updated`: Published when project settings are updated. Contains both the old and new settings to allow downstream services to react to changes. Message format: ```json @@ -45,6 +49,43 @@ This service publishes the following NATS events: } ``` +#### Access Control Events + +This service uses the generic FGA sync handlers for managing fine-grained access control. All access control messages use the `GenericFGAMessage` envelope format: + +- `lfx.fga-sync.update_access`: Published when project access permissions are updated. This is a full sync operation - any relations not included will be removed. Message format: + + ```json + { + "object_type": "project", + "operation": "update_access", + "data": { + "uid": "project-uid", + "public": true, + "relations": { + "writer": ["username1", "username2"], + "auditor": ["username3"], + "meeting_coordinator": ["username4"] + }, + "references": { + "parent": ["project:parent-uid"] + } + } + } + ``` + +- `lfx.fga-sync.delete_access`: Published when a project is deleted. Removes all access control tuples for the project. Message format: + + ```json + { + "object_type": "project", + "operation": "delete_access", + "data": { + "uid": "project-uid" + } + } + ``` + ### Project Tags The LFX v2 Project Service generates a set of tags for projects and project settings that are sent to the indexer-service. These tags enable searchability and discoverability of projects through OpenSearch. diff --git a/cmd/project-api/service_endpoint_project_test.go b/cmd/project-api/service_endpoint_project_test.go index fd023d1..8e99275 100644 --- a/cmd/project-api/service_endpoint_project_test.go +++ b/cmd/project-api/service_endpoint_project_test.go @@ -154,7 +154,7 @@ func TestCreateProject(t *testing.T) { mockRepo.On("CreateProject", mock.Anything, mock.AnythingOfType("*models.ProjectBase"), mock.AnythingOfType("*models.ProjectSettings")).Return(nil) // Mock message sending mockMsg.On("SendIndexerMessage", mock.Anything, mock.AnythingOfType("string"), mock.AnythingOfType("models.ProjectIndexerMessage"), mock.AnythingOfType("bool")).Return(nil) - mockMsg.On("SendAccessMessage", mock.Anything, mock.AnythingOfType("string"), mock.AnythingOfType("models.ProjectAccessMessage"), mock.AnythingOfType("bool")).Return(nil) + mockMsg.On("SendAccessMessage", mock.Anything, mock.AnythingOfType("string"), mock.AnythingOfType("models.GenericFGAMessage"), mock.AnythingOfType("bool")).Return(nil) mockMsg.On("SendIndexerMessage", mock.Anything, mock.AnythingOfType("string"), mock.AnythingOfType("models.ProjectSettingsIndexerMessage"), mock.AnythingOfType("bool")).Return(nil) }, expectedError: false, diff --git a/internal/domain/models/message.go b/internal/domain/models/message.go index 12d8aec..a3d2728 100644 --- a/internal/domain/models/message.go +++ b/internal/domain/models/message.go @@ -38,20 +38,28 @@ type IndexerMessageEnvelope struct { Tags []string `json:"tags"` } -// ProjectAccessData is the schema for the data in the message sent to the fga-sync service. -// These are the fields that the fga-sync service needs in order to update the OpenFGA permissions. -type ProjectAccessData struct { - UID string `json:"uid"` - Public bool `json:"public"` - ParentUID string `json:"parent_uid"` - Writers []string `json:"writers"` - Auditors []string `json:"auditors"` - MeetingCoordinators []string `json:"meeting_coordinators"` +// GenericFGAMessage is the envelope for all FGA sync operations. +// It uses the generic, resource-agnostic FGA Sync handlers. +type GenericFGAMessage struct { + ObjectType string `json:"object_type"` // Resource type (e.g., "project", "committee", "meeting") + Operation string `json:"operation"` // Operation name (e.g., "update_access", "delete_access") + Data any `json:"data"` // Operation-specific payload } -// ProjectAccessMessage is a type-safe NATS message for project access control operations. -type ProjectAccessMessage struct { - Data ProjectAccessData `json:"data"` +// UpdateAccessData is the data payload for update_access operations. +// This is a full sync operation - any relations not included will be removed. +type UpdateAccessData struct { + UID string `json:"uid"` // Unique identifier for the resource + Public bool `json:"public"` // If true, adds user:* as viewer + Relations map[string][]string `json:"relations,omitempty"` // Map of relation names to arrays of usernames + References map[string][]string `json:"references,omitempty"` // Map of relation names to arrays of object UIDs + ExcludeRelations []string `json:"exclude_relations,omitempty"` // Relations managed elsewhere +} + +// DeleteAccessData is the data payload for delete_access operations. +// Deletes all access control tuples for a resource. +type DeleteAccessData struct { + UID string `json:"uid"` // Unique identifier for the resource to delete } // ProjectSettingsUpdatedMessage is a NATS message published when project settings are updated. diff --git a/internal/domain/models/message_test.go b/internal/domain/models/message_test.go index 4f7935b..3028f9e 100644 --- a/internal/domain/models/message_test.go +++ b/internal/domain/models/message_test.go @@ -119,31 +119,63 @@ func TestProjectSettingsIndexerMessage(t *testing.T) { } } -func TestProjectAccessMessage(t *testing.T) { +func TestGenericFGAMessage(t *testing.T) { tests := []struct { name string - message ProjectAccessMessage - verify func(t *testing.T, msg ProjectAccessMessage) + message GenericFGAMessage + verify func(t *testing.T, msg GenericFGAMessage) }{ { - name: "project access message with all fields", - message: ProjectAccessMessage{ - Data: ProjectAccessData{ - UID: "access-123", - Public: true, - ParentUID: "parent-456", - Writers: []string{"user1", "user2"}, - Auditors: []string{"auditor1"}, - MeetingCoordinators: []string{"coordinator1"}, + name: "generic FGA message for update_access", + message: GenericFGAMessage{ + ObjectType: "project", + Operation: "update_access", + Data: UpdateAccessData{ + UID: "project-123", + Public: true, + Relations: map[string][]string{ + "writer": {"user1", "user2"}, + "auditor": {"auditor1"}, + "meeting_coordinator": {"coordinator1"}, + }, + References: map[string][]string{ + "parent": {"project:parent-456"}, + }, }, }, - verify: func(t *testing.T, msg ProjectAccessMessage) { - assert.Equal(t, "access-123", msg.Data.UID) - assert.True(t, msg.Data.Public) - assert.Equal(t, "parent-456", msg.Data.ParentUID) - assert.Len(t, msg.Data.Writers, 2) - assert.Len(t, msg.Data.Auditors, 1) - assert.Len(t, msg.Data.MeetingCoordinators, 1) + verify: func(t *testing.T, msg GenericFGAMessage) { + assert.Equal(t, "project", msg.ObjectType) + assert.Equal(t, "update_access", msg.Operation) + + data, ok := msg.Data.(UpdateAccessData) + assert.True(t, ok) + assert.Equal(t, "project-123", data.UID) + assert.True(t, data.Public) + assert.Len(t, data.Relations, 3) + assert.Len(t, data.Relations["writer"], 2) + assert.Len(t, data.Relations["auditor"], 1) + assert.Len(t, data.Relations["meeting_coordinator"], 1) + assert.Len(t, data.References, 1) + assert.Len(t, data.References["parent"], 1) + assert.Equal(t, "project:parent-456", data.References["parent"][0]) + }, + }, + { + name: "generic FGA message for delete_access", + message: GenericFGAMessage{ + ObjectType: "project", + Operation: "delete_access", + Data: DeleteAccessData{ + UID: "project-789", + }, + }, + verify: func(t *testing.T, msg GenericFGAMessage) { + assert.Equal(t, "project", msg.ObjectType) + assert.Equal(t, "delete_access", msg.Operation) + + data, ok := msg.Data.(DeleteAccessData) + assert.True(t, ok) + assert.Equal(t, "project-789", data.UID) }, }, } @@ -155,6 +187,67 @@ func TestProjectAccessMessage(t *testing.T) { } } +func TestUpdateAccessData(t *testing.T) { + tests := []struct { + name string + data UpdateAccessData + verify func(t *testing.T, data UpdateAccessData) + }{ + { + name: "update access data with all fields", + data: UpdateAccessData{ + UID: "project-123", + Public: true, + Relations: map[string][]string{ + "writer": {"user1", "user2"}, + "auditor": {"user3"}, + }, + References: map[string][]string{ + "parent": {"project:parent-456"}, + }, + ExcludeRelations: []string{"custom_relation"}, + }, + verify: func(t *testing.T, data UpdateAccessData) { + assert.Equal(t, "project-123", data.UID) + assert.True(t, data.Public) + assert.Len(t, data.Relations, 2) + assert.Len(t, data.References, 1) + assert.Len(t, data.ExcludeRelations, 1) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.verify(t, tt.data) + }) + } +} + +func TestDeleteAccessData(t *testing.T) { + tests := []struct { + name string + data DeleteAccessData + verify func(t *testing.T, data DeleteAccessData) + }{ + { + name: "delete access data", + data: DeleteAccessData{ + UID: "project-456", + }, + verify: func(t *testing.T, data DeleteAccessData) { + assert.Equal(t, "project-456", data.UID) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.verify(t, tt.data) + }) + } +} + func TestIndexerMessageEnvelope(t *testing.T) { tests := []struct { name string diff --git a/internal/infrastructure/nats/message.go b/internal/infrastructure/nats/message.go index 818dc50..3331375 100644 --- a/internal/infrastructure/nats/message.go +++ b/internal/infrastructure/nats/message.go @@ -151,20 +151,16 @@ func (m *MessageBuilder) SendIndexerMessage(ctx context.Context, subject string, } } -// SendAccessMessage sends access control messages to NATS. +// SendAccessMessage sends access control messages to NATS using the generic FGA sync format. func (m *MessageBuilder) SendAccessMessage(ctx context.Context, subject string, message interface{}, sync bool) error { switch msg := message.(type) { - case models.ProjectAccessMessage: - dataBytes, err := json.Marshal(msg.Data) + case models.GenericFGAMessage: + messageBytes, err := json.Marshal(msg) if err != nil { - slog.ErrorContext(ctx, "error marshalling access message data into JSON", constants.ErrKey, err) + slog.ErrorContext(ctx, "error marshalling FGA message into JSON", constants.ErrKey, err) return err } - return m.sendMessage(ctx, subject, dataBytes, sync) - - case string: - // For delete operations, the message is just the UID string - return m.sendMessage(ctx, subject, []byte(msg), sync) + return m.sendMessage(ctx, subject, messageBytes, sync) default: slog.ErrorContext(ctx, "unsupported access message type", "type", fmt.Sprintf("%T", message)) diff --git a/internal/infrastructure/nats/message_test.go b/internal/infrastructure/nats/message_test.go index 766d51d..81dfe2e 100644 --- a/internal/infrastructure/nats/message_test.go +++ b/internal/infrastructure/nats/message_test.go @@ -238,51 +238,65 @@ func TestMessageBuilder_PublishAccessMessage(t *testing.T) { wantErr bool }{ { - name: "successful send access message", - subject: constants.UpdateAccessProjectSubject, - message: models.ProjectAccessMessage{ - Data: models.ProjectAccessData{ - UID: "test-uid", - Public: true, - ParentUID: "parent-uid", - Writers: []string{"user1"}, - Auditors: []string{"user2"}, + name: "successful send update access message", + subject: constants.FGASyncUpdateAccessSubject, + message: models.GenericFGAMessage{ + ObjectType: "project", + Operation: "update_access", + Data: models.UpdateAccessData{ + UID: "test-uid", + Public: true, + Relations: map[string][]string{ + "writer": {"user1"}, + "auditor": {"user2"}, + }, + References: map[string][]string{ + "parent": {"project:parent-uid"}, + }, }, }, setupMocks: func(mockConn *MockNATSConn) { - mockConn.On("Publish", constants.UpdateAccessProjectSubject, mock.AnythingOfType("[]uint8")).Return(nil) + mockConn.On("Publish", constants.FGASyncUpdateAccessSubject, mock.AnythingOfType("[]uint8")).Return(nil) }, setupCtx: backgroundCtx, wantErr: false, }, { - name: "unsupported message type", - subject: constants.UpdateAccessProjectSubject, - message: 123, // Invalid type - int is not supported + name: "successful send delete access message", + subject: constants.FGASyncDeleteAccessSubject, + message: models.GenericFGAMessage{ + ObjectType: "project", + Operation: "delete_access", + Data: models.DeleteAccessData{ + UID: "test-uid-to-delete", + }, + }, setupMocks: func(mockConn *MockNATSConn) { - // No publish expected + mockConn.On("Publish", constants.FGASyncDeleteAccessSubject, mock.AnythingOfType("[]uint8")).Return(nil) }, setupCtx: backgroundCtx, - wantErr: true, + wantErr: false, }, { - name: "successful send delete access message", - subject: constants.DeleteAllAccessSubject, - message: "test-uid-to-delete", + name: "unsupported message type", + subject: constants.FGASyncUpdateAccessSubject, + message: 123, // Invalid type - int is not supported setupMocks: func(mockConn *MockNATSConn) { - mockConn.On("Publish", constants.DeleteAllAccessSubject, []byte("test-uid-to-delete")).Return(nil) + // No publish expected }, setupCtx: backgroundCtx, - wantErr: false, + wantErr: true, }, { name: "nats publish error", - subject: constants.UpdateAccessProjectSubject, - message: models.ProjectAccessMessage{ - Data: models.ProjectAccessData{UID: "test"}, + subject: constants.FGASyncUpdateAccessSubject, + message: models.GenericFGAMessage{ + ObjectType: "project", + Operation: "update_access", + Data: models.UpdateAccessData{UID: "test"}, }, setupMocks: func(mockConn *MockNATSConn) { - mockConn.On("Publish", constants.UpdateAccessProjectSubject, mock.AnythingOfType("[]uint8")).Return(errors.New("nats error")) + mockConn.On("Publish", constants.FGASyncUpdateAccessSubject, mock.AnythingOfType("[]uint8")).Return(errors.New("nats error")) }, setupCtx: backgroundCtx, wantErr: true, @@ -322,41 +336,55 @@ func TestMessageBuilder_PublishAccessMessage_Sync(t *testing.T) { wantErr bool }{ { - name: "successful sync send access message", - subject: constants.UpdateAccessProjectSubject, - message: models.ProjectAccessMessage{ - Data: models.ProjectAccessData{ - UID: "test-uid", - Public: true, - ParentUID: "parent-uid", - Writers: []string{"user1"}, - Auditors: []string{"user2"}, + name: "successful sync send update access message", + subject: constants.FGASyncUpdateAccessSubject, + message: models.GenericFGAMessage{ + ObjectType: "project", + Operation: "update_access", + Data: models.UpdateAccessData{ + UID: "test-uid", + Public: true, + Relations: map[string][]string{ + "writer": {"user1"}, + "auditor": {"user2"}, + }, + References: map[string][]string{ + "parent": {"project:parent-uid"}, + }, }, }, setupMocks: func(mockConn *MockNATSConn) { - mockConn.On("Request", constants.UpdateAccessProjectSubject, mock.AnythingOfType("[]uint8"), defaultRequestTimeout).Return(&nats.Msg{Data: []byte("ack")}, nil) + mockConn.On("Request", constants.FGASyncUpdateAccessSubject, mock.AnythingOfType("[]uint8"), defaultRequestTimeout).Return(&nats.Msg{Data: []byte("OK")}, nil) }, setupCtx: backgroundCtx, wantErr: false, }, { name: "successful sync send delete access message", - subject: constants.DeleteAllAccessSubject, - message: "test-uid-to-delete", + subject: constants.FGASyncDeleteAccessSubject, + message: models.GenericFGAMessage{ + ObjectType: "project", + Operation: "delete_access", + Data: models.DeleteAccessData{ + UID: "test-uid-to-delete", + }, + }, setupMocks: func(mockConn *MockNATSConn) { - mockConn.On("Request", constants.DeleteAllAccessSubject, []byte("test-uid-to-delete"), defaultRequestTimeout).Return(&nats.Msg{Data: []byte("ack")}, nil) + mockConn.On("Request", constants.FGASyncDeleteAccessSubject, mock.AnythingOfType("[]uint8"), defaultRequestTimeout).Return(&nats.Msg{Data: []byte("OK")}, nil) }, setupCtx: backgroundCtx, wantErr: false, }, { name: "nats request error - sync mode", - subject: constants.UpdateAccessProjectSubject, - message: models.ProjectAccessMessage{ - Data: models.ProjectAccessData{UID: "test"}, + subject: constants.FGASyncUpdateAccessSubject, + message: models.GenericFGAMessage{ + ObjectType: "project", + Operation: "update_access", + Data: models.UpdateAccessData{UID: "test"}, }, setupMocks: func(mockConn *MockNATSConn) { - mockConn.On("Request", constants.UpdateAccessProjectSubject, mock.AnythingOfType("[]uint8"), defaultRequestTimeout).Return(nil, errors.New("nats request timeout")) + mockConn.On("Request", constants.FGASyncUpdateAccessSubject, mock.AnythingOfType("[]uint8"), defaultRequestTimeout).Return(nil, errors.New("nats request timeout")) }, setupCtx: backgroundCtx, wantErr: true, diff --git a/internal/service/converters.go b/internal/service/converters.go index 229578d..657b640 100644 --- a/internal/service/converters.go +++ b/internal/service/converters.go @@ -4,6 +4,7 @@ package service import ( + "fmt" "time" projsvc "github.com/linuxfoundation/lfx-v2-project-service/api/project/v1/gen/project_service" @@ -437,6 +438,39 @@ func extractUsernames(users []models.UserInfo) []string { return usernames } +// buildFGAUpdateAccessMessage builds a GenericFGAMessage for update_access operations. +// It constructs the relations map from project settings and references map from project base. +func buildFGAUpdateAccessMessage(projectDB *models.ProjectBase, projectSettingsDB *models.ProjectSettings) models.GenericFGAMessage { + // Build relations map for FGA sync + relations := make(map[string][]string) + if writers := extractUsernames(projectSettingsDB.Writers); len(writers) > 0 { + relations["writer"] = writers + } + if auditors := extractUsernames(projectSettingsDB.Auditors); len(auditors) > 0 { + relations["auditor"] = auditors + } + if coordinators := extractUsernames(projectSettingsDB.MeetingCoordinators); len(coordinators) > 0 { + relations["meeting_coordinator"] = coordinators + } + + // Build references map for parent relationship + references := make(map[string][]string) + if projectDB.ParentUID != "" { + references["parent"] = []string{fmt.Sprintf("project:%s", projectDB.ParentUID)} + } + + return models.GenericFGAMessage{ + ObjectType: "project", + Operation: "update_access", + Data: models.UpdateAccessData{ + UID: projectDB.UID, + Public: projectDB.Public, + Relations: relations, + References: references, + }, + } +} + // createTestUserInfo creates a UserInfo for testing purposes func createTestUserInfo(username, name, email, avatar string) models.UserInfo { return models.UserInfo{ diff --git a/internal/service/project_operations.go b/internal/service/project_operations.go index 82bd572..bc57b79 100644 --- a/internal/service/project_operations.go +++ b/internal/service/project_operations.go @@ -175,17 +175,8 @@ func (s *ProjectsService) CreateProject(ctx context.Context, payload *projsvc.Cr }) g.Go(func() error { - msg := models.ProjectAccessMessage{ - Data: models.ProjectAccessData{ - UID: projectDB.UID, - Public: projectDB.Public, - ParentUID: projectDB.ParentUID, - Writers: extractUsernames(projectSettingsDB.Writers), - Auditors: extractUsernames(projectSettingsDB.Auditors), - MeetingCoordinators: extractUsernames(projectSettingsDB.MeetingCoordinators), - }, - } - return s.MessageBuilder.SendAccessMessage(ctx, constants.UpdateAccessProjectSubject, msg, runSync) + msg := buildFGAUpdateAccessMessage(projectDB, projectSettingsDB) + return s.MessageBuilder.SendAccessMessage(ctx, constants.FGASyncUpdateAccessSubject, msg, runSync) }) if err := g.Wait(); err != nil { @@ -440,17 +431,8 @@ func (s *ProjectsService) UpdateProjectBase(ctx context.Context, payload *projsv }) g.Go(func() error { - msg := models.ProjectAccessMessage{ - Data: models.ProjectAccessData{ - UID: projectDB.UID, - Public: projectDB.Public, - ParentUID: projectDB.ParentUID, - Writers: extractUsernames(projectSettingsDB.Writers), - Auditors: extractUsernames(projectSettingsDB.Auditors), - MeetingCoordinators: extractUsernames(projectSettingsDB.MeetingCoordinators), - }, - } - return s.MessageBuilder.SendAccessMessage(ctx, constants.UpdateAccessProjectSubject, msg, runSync) + msg := buildFGAUpdateAccessMessage(projectDB, projectSettingsDB) + return s.MessageBuilder.SendAccessMessage(ctx, constants.FGASyncUpdateAccessSubject, msg, runSync) }) if err := g.Wait(); err != nil { @@ -580,17 +562,8 @@ func (s *ProjectsService) UpdateProjectSettings(ctx context.Context, payload *pr }) g.Go(func() error { - msg := models.ProjectAccessMessage{ - Data: models.ProjectAccessData{ - UID: projectDB.UID, - Public: projectDB.Public, - ParentUID: projectDB.ParentUID, - Writers: extractUsernames(projectSettingsDB.Writers), - Auditors: extractUsernames(projectSettingsDB.Auditors), - MeetingCoordinators: extractUsernames(projectSettingsDB.MeetingCoordinators), - }, - } - return s.MessageBuilder.SendAccessMessage(ctx, constants.UpdateAccessProjectSubject, msg, runSync) + msg := buildFGAUpdateAccessMessage(projectDB, projectSettingsDB) + return s.MessageBuilder.SendAccessMessage(ctx, constants.FGASyncUpdateAccessSubject, msg, runSync) }) g.Go(func() error { @@ -685,7 +658,14 @@ func (s *ProjectsService) DeleteProject(ctx context.Context, payload *projsvc.De }) g.Go(func() error { - return s.MessageBuilder.SendAccessMessage(ctx, constants.DeleteAllAccessSubject, *payload.UID, runSync) + msg := models.GenericFGAMessage{ + ObjectType: "project", + Operation: "delete_access", + Data: models.DeleteAccessData{ + UID: *payload.UID, + }, + } + return s.MessageBuilder.SendAccessMessage(ctx, constants.FGASyncDeleteAccessSubject, msg, runSync) }) if err := g.Wait(); err != nil { diff --git a/internal/service/project_operations_test.go b/internal/service/project_operations_test.go index a7a6d70..9fc9e4f 100644 --- a/internal/service/project_operations_test.go +++ b/internal/service/project_operations_test.go @@ -154,7 +154,7 @@ func TestProjectsService_CreateProject(t *testing.T) { mockRepo.On("ProjectSlugExists", mock.Anything, "test-project").Return(false, nil) mockRepo.On("CreateProject", mock.Anything, mock.AnythingOfType("*models.ProjectBase"), mock.AnythingOfType("*models.ProjectSettings")).Return(nil) mockBuilder.On("SendIndexerMessage", mock.Anything, mock.AnythingOfType("string"), mock.AnythingOfType("models.ProjectIndexerMessage"), mock.AnythingOfType("bool")).Return(nil) - mockBuilder.On("SendAccessMessage", mock.Anything, mock.AnythingOfType("string"), mock.AnythingOfType("models.ProjectAccessMessage"), mock.AnythingOfType("bool")).Return(nil) + mockBuilder.On("SendAccessMessage", mock.Anything, mock.AnythingOfType("string"), mock.AnythingOfType("models.GenericFGAMessage"), mock.AnythingOfType("bool")).Return(nil) mockBuilder.On("SendIndexerMessage", mock.Anything, mock.AnythingOfType("string"), mock.AnythingOfType("models.ProjectSettingsIndexerMessage"), mock.AnythingOfType("bool")).Return(nil) }, wantErr: false, diff --git a/pkg/constants/nats.go b/pkg/constants/nats.go index 074a5d4..70f2b18 100644 --- a/pkg/constants/nats.go +++ b/pkg/constants/nats.go @@ -22,21 +22,15 @@ const ( // The subject is of the form: lfx.index.project_settings IndexProjectSettingsSubject = "lfx.index.project_settings" - // UpdateAccessProjectSubject is the subject for the project access control updates. - // The subject is of the form: lfx.update_access.project - UpdateAccessProjectSubject = "lfx.update_access.project" + // FGASyncUpdateAccessSubject is the subject for FGA sync update_access operations. + // Uses the generic, resource-agnostic FGA Sync handler. + // The subject is of the form: lfx.fga-sync.update_access + FGASyncUpdateAccessSubject = "lfx.fga-sync.update_access" - // UpdateAccessProjectSettingsSubject is the subject for the project settings access control updates. - // The subject is of the form: lfx.update_access.project_settings - UpdateAccessProjectSettingsSubject = "lfx.update_access.project_settings" - - // DeleteAllAccessSubject is the subject for the project access control deletion. - // The subject is of the form: lfx.delete_all_access.project - DeleteAllAccessSubject = "lfx.delete_all_access.project" - - // DeleteAllAccessProjectSettingsSubject is the subject for the project settings access control deletion. - // The subject is of the form: lfx.delete_all_access.project_settings - DeleteAllAccessProjectSettingsSubject = "lfx.delete_all_access.project_settings" + // FGASyncDeleteAccessSubject is the subject for FGA sync delete_access operations. + // Uses the generic, resource-agnostic FGA Sync handler. + // The subject is of the form: lfx.fga-sync.delete_access + FGASyncDeleteAccessSubject = "lfx.fga-sync.delete_access" // ProjectSettingsUpdatedSubject is the subject for project settings change events. // This event is published when project settings are updated, containing both before and after states.