Skip to content

Commit ce9a781

Browse files
fix(users): publish FGA update_access after username scrub (LFXV2-2645)
Refresh OpenFGA tuples from scrubbed settings via buildFGAUpdateAccessMessage, matching normal settings writes while still omitting role-change notification emails. Co-authored-by: Cursor <cursoragent@cursor.com> Signed-off-by: Andres Tobon <andrest2455@gmail.com>
1 parent 884d406 commit ce9a781

2 files changed

Lines changed: 38 additions & 9 deletions

File tree

internal/service/project_subscriber.go

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"time"
1414

1515
emailapi "github.com/linuxfoundation/lfx-v2-email-service/pkg/api"
16+
fgaconstants "github.com/linuxfoundation/lfx-v2-fga-sync/pkg/constants"
1617
indexerConstants "github.com/linuxfoundation/lfx-v2-indexer-service/pkg/constants"
1718
indexerTypes "github.com/linuxfoundation/lfx-v2-indexer-service/pkg/types"
1819
inviteapi "github.com/linuxfoundation/lfx-v2-invite-service/pkg/api"
@@ -657,15 +658,7 @@ func (s *ProjectsService) scrubUsernameFromProjectSettings(ctx context.Context,
657658
if updateErr == nil {
658659
slog.InfoContext(ctx, "project_subscriber: cleared username from project settings",
659660
"project_uid", projectUID, "username", username)
660-
indexMsg := indexerTypes.IndexerMessageEnvelope{
661-
Action: indexerConstants.ActionUpdated,
662-
Data: *settings,
663-
IndexingConfig: settings.IndexingConfig(projectUID),
664-
}
665-
if indexErr := s.MessageBuilder.SendIndexerMessage(ctx, constants.IndexProjectSettingsSubject, indexMsg, false); indexErr != nil {
666-
slog.WarnContext(ctx, "project_subscriber: failed to reindex project settings after username scrub",
667-
constants.ErrKey, indexErr, "project_uid", projectUID)
668-
}
661+
s.publishProjectSettingsScrubSideEffects(ctx, projectUID, settings)
669662
return
670663
}
671664
if !errors.Is(updateErr, domain.ErrRevisionMismatch) || attempt == maxRetries-1 {
@@ -678,6 +671,33 @@ func (s *ProjectsService) scrubUsernameFromProjectSettings(ctx context.Context,
678671
}
679672
}
680673

674+
// publishProjectSettingsScrubSideEffects reindexes scrubbed settings and refreshes OpenFGA
675+
// access tuples. Best-effort: failures are logged but do not fail the scrub handler.
676+
// ProjectSettingsUpdatedSubject is intentionally omitted to avoid role-change emails.
677+
func (s *ProjectsService) publishProjectSettingsScrubSideEffects(ctx context.Context, projectUID string, settings *models.ProjectSettings) {
678+
indexMsg := indexerTypes.IndexerMessageEnvelope{
679+
Action: indexerConstants.ActionUpdated,
680+
Data: *settings,
681+
IndexingConfig: settings.IndexingConfig(projectUID),
682+
}
683+
if indexErr := s.MessageBuilder.SendIndexerMessage(ctx, constants.IndexProjectSettingsSubject, indexMsg, false); indexErr != nil {
684+
slog.WarnContext(ctx, "project_subscriber: failed to reindex project settings after username scrub",
685+
constants.ErrKey, indexErr, "project_uid", projectUID)
686+
}
687+
688+
projectBase, err := s.ProjectRepository.GetProjectBase(ctx, projectUID)
689+
if err != nil {
690+
slog.WarnContext(ctx, "project_subscriber: failed to load project for FGA refresh after username scrub",
691+
constants.ErrKey, err, "project_uid", projectUID)
692+
return
693+
}
694+
fgaMsg := buildFGAUpdateAccessMessage(projectBase, settings)
695+
if accessErr := s.MessageBuilder.SendAccessMessage(ctx, fgaconstants.GenericUpdateAccessSubject, fgaMsg, false); accessErr != nil {
696+
slog.WarnContext(ctx, "project_subscriber: failed to publish FGA update after username scrub",
697+
constants.ErrKey, accessErr, "project_uid", projectUID)
698+
}
699+
}
700+
681701
// buildProjectURL constructs the deep-link URL for a project's overview page.
682702
func buildProjectURL(baseURL, slug string) string {
683703
base := strings.TrimRight(baseURL, "/") + "/project/overview"

internal/service/project_subscriber_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"time"
1313

1414
emailapi "github.com/linuxfoundation/lfx-v2-email-service/pkg/api"
15+
fgaconstants "github.com/linuxfoundation/lfx-v2-fga-sync/pkg/constants"
1516
indexerTypes "github.com/linuxfoundation/lfx-v2-indexer-service/pkg/types"
1617
inviteapi "github.com/linuxfoundation/lfx-v2-invite-service/pkg/api"
1718
"github.com/stretchr/testify/assert"
@@ -1185,9 +1186,11 @@ func TestHandleUserDeleted(t *testing.T) {
11851186
r.On("UpdateProjectSettings", mock.Anything, mock.MatchedBy(func(s *models.ProjectSettings) bool {
11861187
return len(s.Writers) == 1 && s.Writers[0].Username == "" && s.Writers[0].Email == "deleted@example.com"
11871188
}), uint64(1)).Return(nil)
1189+
r.On("GetProjectBase", mock.Anything, projectUID).Return(&models.ProjectBase{UID: projectUID}, nil)
11881190
},
11891191
setupMsg: func(m *domain.MockMessageBuilder) {
11901192
m.On("SendIndexerMessage", mock.Anything, constants.IndexProjectSettingsSubject, mock.Anything, false).Return(nil)
1193+
m.On("SendAccessMessage", mock.Anything, fgaconstants.GenericUpdateAccessSubject, mock.Anything, false).Return(nil)
11911194
},
11921195
},
11931196
{
@@ -1221,9 +1224,11 @@ func TestHandleUserDeleted(t *testing.T) {
12211224
r.On("UpdateProjectSettings", mock.Anything, mock.MatchedBy(func(s *models.ProjectSettings) bool {
12221225
return len(s.Auditors) == 1 && s.Auditors[0].Username == ""
12231226
}), uint64(2)).Return(nil).Once()
1227+
r.On("GetProjectBase", mock.Anything, projectUID).Return(&models.ProjectBase{UID: projectUID}, nil)
12241228
},
12251229
setupMsg: func(m *domain.MockMessageBuilder) {
12261230
m.On("SendIndexerMessage", mock.Anything, constants.IndexProjectSettingsSubject, mock.Anything, false).Return(nil)
1231+
m.On("SendAccessMessage", mock.Anything, fgaconstants.GenericUpdateAccessSubject, mock.Anything, false).Return(nil)
12271232
},
12281233
},
12291234
{
@@ -1248,9 +1253,11 @@ func TestHandleUserDeleted(t *testing.T) {
12481253
return len(s.MeetingCoordinators) == 1 && s.MeetingCoordinators[0].Username == "" &&
12491254
s.ExecutiveDirector != nil && s.ExecutiveDirector.Username == ""
12501255
}), uint64(1)).Return(nil)
1256+
r.On("GetProjectBase", mock.Anything, projectUID).Return(&models.ProjectBase{UID: projectUID}, nil)
12511257
},
12521258
setupMsg: func(m *domain.MockMessageBuilder) {
12531259
m.On("SendIndexerMessage", mock.Anything, constants.IndexProjectSettingsSubject, mock.Anything, false).Return(nil)
1260+
m.On("SendAccessMessage", mock.Anything, fgaconstants.GenericUpdateAccessSubject, mock.Anything, false).Return(nil)
12541261
},
12551262
},
12561263
{
@@ -1268,9 +1275,11 @@ func TestHandleUserDeleted(t *testing.T) {
12681275
r.On("ListAllProjectsSettings", mock.Anything).Return([]*models.ProjectSettings{match, other}, nil)
12691276
r.On("GetProjectSettingsWithRevision", mock.Anything, projectUID).Return(match, uint64(1), nil)
12701277
r.On("UpdateProjectSettings", mock.Anything, mock.Anything, uint64(1)).Return(nil)
1278+
r.On("GetProjectBase", mock.Anything, projectUID).Return(&models.ProjectBase{UID: projectUID}, nil)
12711279
},
12721280
setupMsg: func(m *domain.MockMessageBuilder) {
12731281
m.On("SendIndexerMessage", mock.Anything, constants.IndexProjectSettingsSubject, mock.Anything, false).Return(nil)
1282+
m.On("SendAccessMessage", mock.Anything, fgaconstants.GenericUpdateAccessSubject, mock.Anything, false).Return(nil)
12741283
},
12751284
},
12761285
}

0 commit comments

Comments
 (0)