@@ -19,6 +19,16 @@ import (
1919 "github.com/linuxfoundation/lfx-v2-mailing-list-service/pkg/utils"
2020)
2121
22+ // groupsioMailingListMemberStub represents the minimal data needed for member access control
23+ type groupsioMailingListMemberStub struct {
24+ // UID is the mailing list member ID.
25+ UID string `json:"uid"`
26+ // Username is the username (i.e. LFID) of the member. This is the identity of the user object in FGA.
27+ Username string `json:"username"`
28+ // MailingListUID is the mailing list ID for the mailing list the member belongs to.
29+ MailingListUID string `json:"mailing_list_uid"`
30+ }
31+
2232// ensureMemberIdempotent checks if member already exists by Groups.io member ID or email
2333// Returns existing entity if found, nil if not found, error on failure
2434// Pattern mirrors ensureMailingListIdempotent
@@ -415,8 +425,8 @@ func (o *grpsIOWriterOrchestrator) DeleteGrpsIOMember(ctx context.Context, uid s
415425 )
416426
417427 // Publish delete messages (indexer and access control)
418- if o .publisher != nil {
419- if err := o .publishMemberDeleteMessages (ctx , uid ); err != nil {
428+ if o .publisher != nil && member != nil {
429+ if err := o .publishMemberDeleteMessages (ctx , uid , * member ); err != nil {
420430 slog .ErrorContext (ctx , "failed to publish member delete messages" , "error" , err )
421431 // Don't fail the operation on message failure, delete succeeded
422432 }
@@ -461,16 +471,24 @@ func (o *grpsIOWriterOrchestrator) publishMemberMessages(ctx context.Context, me
461471 return fmt .Errorf ("failed to build %s indexer message: %w" , action , err )
462472 }
463473
464- // TODO: LFXV2-459 - Review and implement member access control logic for OpenFGA integration
465- // Access control message building and publishing will be implemented after research is complete
466-
467- // Publish messages concurrently (only indexer for now)
474+ // Prepare messages to publish
468475 messages := []func () error {
469476 func () error {
470477 return o .publisher .Indexer (ctx , constants .IndexGroupsIOMemberSubject , indexerMessage )
471478 },
472479 }
473480
481+ // Only publish access control message if member has a username (required for FGA identity)
482+ if member .Username != "" {
483+ accessMessage := o .buildMemberAccessMessage (member )
484+ messages = append (messages , func () error {
485+ return o .publisher .Access (ctx , constants .PutMemberGroupsIOMailingListSubject , accessMessage )
486+ })
487+ } else {
488+ slog .DebugContext (ctx , "skipping access control message - member has no username" ,
489+ "member_uid" , member .UID )
490+ }
491+
474492 // Execute all messages concurrently
475493 errPublishingMessage := concurrent .NewWorkerPool (len (messages )).Run (ctx , messages ... )
476494 if errPublishingMessage != nil {
@@ -489,15 +507,13 @@ func (o *grpsIOWriterOrchestrator) publishMemberMessages(ctx context.Context, me
489507 return nil
490508}
491509
492- // publishMemberDeleteMessages publishes member delete messages concurrently (for future use)
493- // nolint:unused // Reserved for future member deletion functionality
494- func (o * grpsIOWriterOrchestrator ) publishMemberDeleteMessages (ctx context.Context , uid string ) error {
510+ // publishMemberDeleteMessages publishes member delete messages concurrently
511+ func (o * grpsIOWriterOrchestrator ) publishMemberDeleteMessages (ctx context.Context , uid string , member model.GrpsIOMember ) error {
495512 if o .publisher == nil {
496513 slog .WarnContext (ctx , "publisher not available, skipping member delete message publishing" )
497514 return nil
498515 }
499516
500- // For delete messages, we just need the UID
501517 indexerMessage := & model.IndexerMessage {
502518 Action : model .ActionDeleted ,
503519 Tags : []string {},
@@ -509,16 +525,22 @@ func (o *grpsIOWriterOrchestrator) publishMemberDeleteMessages(ctx context.Conte
509525 return fmt .Errorf ("failed to build member delete indexer message: %w" , err )
510526 }
511527
512- // Publish delete messages concurrently
528+ // Prepare messages to publish
513529 messages := []func () error {
514530 func () error {
515531 return o .publisher .Indexer (ctx , constants .IndexGroupsIOMemberSubject , builtMessage )
516532 },
517- // TODO: LFXV2-459 Implement proper member removal from mailing list relations
518- // Currently commented out to avoid deleting entire mailing list from OpenFGA
519- // func() error {
520- // return o.publisher.Access(ctx, constants.DeleteAllAccessGroupsIOMemberSubject, uid)
521- // },
533+ }
534+
535+ // Only publish access control message if member has a username (required for FGA identity)
536+ if member .Username != "" {
537+ accessMessage := o .buildMemberAccessMessage (& member )
538+ messages = append (messages , func () error {
539+ return o .publisher .Access (ctx , constants .RemoveMemberGroupsIOMailingListSubject , accessMessage )
540+ })
541+ } else {
542+ slog .DebugContext (ctx , "skipping access control delete message - member has no username" ,
543+ "member_uid" , uid )
522544 }
523545
524546 // Execute all messages concurrently
@@ -546,6 +568,15 @@ func (o *grpsIOWriterOrchestrator) buildMemberIndexerMessage(ctx context.Context
546568 return indexerMessage .Build (ctx , member )
547569}
548570
571+ // buildMemberAccessMessage creates the access control message stub for OpenFGA integration
572+ func (o * grpsIOWriterOrchestrator ) buildMemberAccessMessage (member * model.GrpsIOMember ) * groupsioMailingListMemberStub {
573+ return & groupsioMailingListMemberStub {
574+ UID : member .UID ,
575+ Username : member .Username ,
576+ MailingListUID : member .MailingListUID ,
577+ }
578+ }
579+
549580// mergeMemberData merges existing member data with updated fields, preserving immutable fields
550581func (o * grpsIOWriterOrchestrator ) mergeMemberData (ctx context.Context , existing * model.GrpsIOMember , updated * model.GrpsIOMember ) {
551582 // Preserve immutable fields
0 commit comments