-
Notifications
You must be signed in to change notification settings - Fork 1
[LFXV2-1505] Fix past meeting artifact FGA access to use per-artifact viewer relations #146
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
3f9a12c
143f018
a901880
b7930c3
8f12040
13fe33a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -236,7 +236,11 @@ func (p *NATSPublisher) PublishInviteResponseEvent(ctx context.Context, action s | |
|
|
||
| // PublishPastMeetingEvent publishes a past meeting event to indexer and FGA-sync services | ||
| func (p *NATSPublisher) PublishPastMeetingEvent(ctx context.Context, action string, meeting *models.PastMeetingEventData) error { | ||
| p.logger.InfoContext(ctx, "publishing past meeting event", "action", action, "past_meeting_id", meeting.ID) | ||
| if meeting.MeetingAndOccurrenceID == "" { | ||
| return domain.NewValidationError("meeting_and_occurrence_id is required for publishing messages about the past meeting") | ||
| } | ||
|
|
||
| p.logger.InfoContext(ctx, "publishing past meeting event", "action", action, "past_meeting_id", meeting.MeetingAndOccurrenceID) | ||
|
andrest50 marked this conversation as resolved.
|
||
|
|
||
| tags := meeting.Tags() | ||
| publicFalse := false | ||
|
|
@@ -246,11 +250,11 @@ func (p *NATSPublisher) PublishPastMeetingEvent(ctx context.Context, action stri | |
| Data: meeting, | ||
| Tags: tags, | ||
| IndexingConfig: &indexerTypes.IndexingConfig{ | ||
| ObjectID: meeting.ID, | ||
| ObjectID: meeting.MeetingAndOccurrenceID, | ||
| Public: &publicFalse, | ||
| AccessCheckObject: indexerConstants.ObjectTypeV1PastMeeting + ":" + meeting.ID, | ||
| AccessCheckObject: indexerConstants.ObjectTypeV1PastMeeting + ":" + meeting.MeetingAndOccurrenceID, | ||
| AccessCheckRelation: "viewer", | ||
| HistoryCheckObject: indexerConstants.ObjectTypeV1PastMeeting + ":" + meeting.ID, | ||
| HistoryCheckObject: indexerConstants.ObjectTypeV1PastMeeting + ":" + meeting.MeetingAndOccurrenceID, | ||
| HistoryCheckRelation: "auditor", | ||
|
andrest50 marked this conversation as resolved.
|
||
| ParentRefs: meeting.ParentRefs(), | ||
| Tags: tags, | ||
|
|
@@ -265,7 +269,11 @@ func (p *NATSPublisher) PublishPastMeetingEvent(ctx context.Context, action stri | |
| } | ||
|
|
||
| // Publish past meeting access control via generic FGA handler. | ||
| // Per-artifact conditional relations (recording_viewer, transcript_viewer, ai_summary_viewer) | ||
| // are written here — not in the artifact publishers — so FGA is updated whenever the past | ||
| // meeting record changes, not only when an artifact is re-published. | ||
|
andrest50 marked this conversation as resolved.
|
||
| pastMeetingRefs := map[string][]string{} | ||
| pastMeetingRelations := map[string][]string{} | ||
| if meeting.MeetingID != "" { | ||
| pastMeetingRefs["meeting"] = []string{"v1_meeting:" + meeting.MeetingID} | ||
| } | ||
|
|
@@ -282,14 +290,54 @@ func (p *NATSPublisher) PublishPastMeetingEvent(ctx context.Context, action stri | |
| pastMeetingRefs["committee"] = committeeUIDs | ||
| } | ||
|
|
||
| // Per-artifact access: self-referential references enable role-based access | ||
| // via the existing host/attendee/invitee tuples on the same v1_past_meeting object. | ||
|
andrest50 marked this conversation as resolved.
|
||
| selfRef := "v1_past_meeting:" + meeting.MeetingAndOccurrenceID | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's a good point. And actually when we send the indexer message using meeting.ID, that is the same value as the meeting.MeetingAndOccurrenceID, so I switched to using meeting.MeetingAndOccurrenceID in all places in the code and moved the validation to the top of the function. There aren't any cases where this validation has failed currently since all meetings have an ID, but it is still a good safety check. |
||
|
|
||
| switch meeting.RecordingAccess { | ||
| case "public": | ||
| pastMeetingRelations["recording_viewer"] = []string{"*"} | ||
| case "meeting_participants": | ||
| pastMeetingRefs["past_meeting_for_host_recording_view"] = []string{selfRef} | ||
| pastMeetingRefs["past_meeting_for_attendee_recording_view"] = []string{selfRef} | ||
| pastMeetingRefs["past_meeting_for_participant_recording_view"] = []string{selfRef} | ||
| default: // meeting_hosts or unset | ||
| pastMeetingRefs["past_meeting_for_host_recording_view"] = []string{selfRef} | ||
| } | ||
|
|
||
| switch meeting.TranscriptAccess { | ||
| case "public": | ||
| pastMeetingRelations["transcript_viewer"] = []string{"*"} | ||
| case "meeting_participants": | ||
| pastMeetingRefs["past_meeting_for_host_transcript_view"] = []string{selfRef} | ||
| pastMeetingRefs["past_meeting_for_attendee_transcript_view"] = []string{selfRef} | ||
| pastMeetingRefs["past_meeting_for_participant_transcript_view"] = []string{selfRef} | ||
| default: // meeting_hosts or unset | ||
| pastMeetingRefs["past_meeting_for_host_transcript_view"] = []string{selfRef} | ||
| } | ||
|
|
||
| switch meeting.AISummaryAccess { | ||
| case "public": | ||
| pastMeetingRelations["ai_summary_viewer"] = []string{"*"} | ||
| case "meeting_participants": | ||
| pastMeetingRefs["past_meeting_for_host_summary_view"] = []string{selfRef} | ||
| pastMeetingRefs["past_meeting_for_attendee_summary_view"] = []string{selfRef} | ||
| pastMeetingRefs["past_meeting_for_participant_summary_view"] = []string{selfRef} | ||
| default: // meeting_hosts or unset | ||
| pastMeetingRefs["past_meeting_for_host_summary_view"] = []string{selfRef} | ||
| } | ||
|
|
||
| pastMeetingAccessMsg := fgatypes.GenericFGAMessage{ | ||
| ObjectType: "v1_past_meeting", | ||
| Operation: "update_access", | ||
| Data: fgatypes.GenericAccessData{ | ||
| UID: meeting.ID, | ||
| UID: meeting.MeetingAndOccurrenceID, | ||
| Public: false, | ||
| Relations: map[string][]string{}, | ||
| Relations: pastMeetingRelations, | ||
| References: pastMeetingRefs, | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| // host/invitee/attendee are managed by PublishPastMeetingParticipantEvent | ||
| // and must not be overwritten here. | ||
| ExcludeRelations: []string{"host", "invitee", "attendee"}, | ||
| }, | ||
|
coderabbitai[bot] marked this conversation as resolved.
andrest50 marked this conversation as resolved.
|
||
| } | ||
|
|
||
|
|
@@ -388,7 +436,7 @@ func (p *NATSPublisher) PublishPastMeetingRecordingEvent(ctx context.Context, ac | |
| ObjectID: recording.ID, | ||
| Public: &isPublic, | ||
| AccessCheckObject: indexerConstants.ObjectTypeV1PastMeeting + ":" + recording.MeetingAndOccurrenceID, | ||
| AccessCheckRelation: "viewer", | ||
| AccessCheckRelation: "recording_viewer", | ||
| HistoryCheckObject: indexerConstants.ObjectTypeV1PastMeeting + ":" + recording.MeetingAndOccurrenceID, | ||
| HistoryCheckRelation: "auditor", | ||
| ParentRefs: recording.ParentRefs(), | ||
|
|
@@ -403,39 +451,9 @@ func (p *NATSPublisher) PublishPastMeetingRecordingEvent(ctx context.Context, ac | |
| return fmt.Errorf("failed to publish recording to indexer: %w", err) | ||
| } | ||
|
|
||
| // Publish recording access control via generic FGA handler. | ||
| // references builds object-to-object tuples; values use "v1_past_meeting:<id>" so fga-sync | ||
| // writes the correct type (define past_meeting: [v1_past_meeting]). | ||
| pastMeetingRef := "v1_past_meeting:" + recording.MeetingAndOccurrenceID | ||
| recordingRefs := map[string][]string{ | ||
| "past_meeting": {pastMeetingRef}, | ||
| } | ||
| switch recording.RecordingAccess { | ||
| case "public": | ||
| // isPublic=true handles viewer access via user:* | ||
| case "meeting_participants": | ||
| recordingRefs["past_meeting_for_host_view"] = []string{pastMeetingRef} | ||
| recordingRefs["past_meeting_for_attendee_view"] = []string{pastMeetingRef} | ||
| recordingRefs["past_meeting_for_participant_view"] = []string{pastMeetingRef} | ||
| default: // meeting_hosts or unset | ||
| recordingRefs["past_meeting_for_host_view"] = []string{pastMeetingRef} | ||
| } | ||
|
|
||
| recordingAccessMsg := fgatypes.GenericFGAMessage{ | ||
| ObjectType: "v1_past_meeting_recording", | ||
| Operation: "update_access", | ||
| Data: fgatypes.GenericAccessData{ | ||
| UID: recording.ID, | ||
| Public: isPublic, | ||
| Relations: map[string][]string{}, | ||
| References: recordingRefs, | ||
| }, | ||
| } | ||
|
|
||
| if err := p.publish(ctx, fgaconstants.GenericUpdateAccessSubject, recordingAccessMsg); err != nil { | ||
| return fmt.Errorf("failed to publish recording access control: %w", err) | ||
| } | ||
|
|
||
| // FGA access for recordings is managed in PublishPastMeetingEvent, not here, | ||
| // because recording_access lives on the past meeting record. This ensures FGA | ||
| // stays in sync when the access setting changes without a new recording event. | ||
| return nil | ||
| } | ||
|
|
||
|
|
@@ -454,7 +472,7 @@ func (p *NATSPublisher) PublishPastMeetingTranscriptEvent(ctx context.Context, a | |
| ObjectID: transcript.ID, | ||
| Public: &isPublic, | ||
| AccessCheckObject: indexerConstants.ObjectTypeV1PastMeeting + ":" + transcript.MeetingAndOccurrenceID, | ||
| AccessCheckRelation: "viewer", | ||
| AccessCheckRelation: "transcript_viewer", | ||
| HistoryCheckObject: indexerConstants.ObjectTypeV1PastMeeting + ":" + transcript.MeetingAndOccurrenceID, | ||
| HistoryCheckRelation: "auditor", | ||
| ParentRefs: transcript.ParentRefs(), | ||
|
|
@@ -469,37 +487,8 @@ func (p *NATSPublisher) PublishPastMeetingTranscriptEvent(ctx context.Context, a | |
| return fmt.Errorf("failed to publish transcript to indexer: %w", err) | ||
| } | ||
|
|
||
| // Publish transcript access control via generic FGA handler. | ||
| pastMeetingRef := "v1_past_meeting:" + transcript.MeetingAndOccurrenceID | ||
| transcriptRefs := map[string][]string{ | ||
| "past_meeting": {pastMeetingRef}, | ||
| } | ||
| switch transcript.TranscriptAccess { | ||
| case "public": | ||
| // isPublic=true handles viewer access via user:* | ||
| case "meeting_participants": | ||
| transcriptRefs["past_meeting_for_host_view"] = []string{pastMeetingRef} | ||
| transcriptRefs["past_meeting_for_attendee_view"] = []string{pastMeetingRef} | ||
| transcriptRefs["past_meeting_for_participant_view"] = []string{pastMeetingRef} | ||
| default: // meeting_hosts or unset | ||
| transcriptRefs["past_meeting_for_host_view"] = []string{pastMeetingRef} | ||
| } | ||
|
|
||
| transcriptAccessMsg := fgatypes.GenericFGAMessage{ | ||
| ObjectType: "v1_past_meeting_transcript", | ||
| Operation: "update_access", | ||
| Data: fgatypes.GenericAccessData{ | ||
| UID: transcript.ID, | ||
| Public: isPublic, | ||
| Relations: map[string][]string{}, | ||
| References: transcriptRefs, | ||
| }, | ||
| } | ||
|
|
||
| if err := p.publish(ctx, fgaconstants.GenericUpdateAccessSubject, transcriptAccessMsg); err != nil { | ||
| return fmt.Errorf("failed to publish transcript access control: %w", err) | ||
| } | ||
|
|
||
| // FGA access for transcripts is managed in PublishPastMeetingEvent, not here, | ||
| // because transcript_access lives on the past meeting record. | ||
| return nil | ||
| } | ||
|
|
||
|
|
@@ -519,7 +508,7 @@ func (p *NATSPublisher) PublishPastMeetingSummaryEvent(ctx context.Context, acti | |
| ObjectID: summary.ID, | ||
| Public: &isPublic, | ||
| AccessCheckObject: indexerConstants.ObjectTypeV1PastMeeting + ":" + summary.MeetingAndOccurrenceID, | ||
| AccessCheckRelation: "viewer", | ||
| AccessCheckRelation: "ai_summary_viewer", | ||
| HistoryCheckObject: indexerConstants.ObjectTypeV1PastMeeting + ":" + summary.MeetingAndOccurrenceID, | ||
| HistoryCheckRelation: "auditor", | ||
| ParentRefs: summary.ParentRefs(), | ||
|
|
@@ -534,37 +523,8 @@ func (p *NATSPublisher) PublishPastMeetingSummaryEvent(ctx context.Context, acti | |
| return fmt.Errorf("failed to publish summary to indexer: %w", err) | ||
| } | ||
|
|
||
| // Publish summary access control via generic FGA handler. | ||
| pastMeetingRef := "v1_past_meeting:" + summary.MeetingAndOccurrenceID | ||
| summaryRefs := map[string][]string{ | ||
| "past_meeting": {pastMeetingRef}, | ||
| } | ||
| switch summaryAccess { | ||
| case "public": | ||
| // isPublic=true handles viewer access via user:* | ||
| case "meeting_participants": | ||
| summaryRefs["past_meeting_for_host_view"] = []string{pastMeetingRef} | ||
| summaryRefs["past_meeting_for_attendee_view"] = []string{pastMeetingRef} | ||
| summaryRefs["past_meeting_for_participant_view"] = []string{pastMeetingRef} | ||
| default: // meeting_hosts or unset | ||
| summaryRefs["past_meeting_for_host_view"] = []string{pastMeetingRef} | ||
| } | ||
|
|
||
| summaryAccessMsg := fgatypes.GenericFGAMessage{ | ||
| ObjectType: "v1_past_meeting_summary", | ||
| Operation: "update_access", | ||
| Data: fgatypes.GenericAccessData{ | ||
| UID: summary.ID, | ||
| Public: isPublic, | ||
| Relations: map[string][]string{}, | ||
| References: summaryRefs, | ||
| }, | ||
| } | ||
|
|
||
| if err := p.publish(ctx, fgaconstants.GenericUpdateAccessSubject, summaryAccessMsg); err != nil { | ||
| return fmt.Errorf("failed to publish summary access control: %w", err) | ||
| } | ||
|
|
||
| // FGA access for summaries is managed in PublishPastMeetingEvent, not here, | ||
| // because ai_summary_access lives on the past meeting record. | ||
| return nil | ||
| } | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.