From 614c37f014409056eb9baa84c8f78178baaa9231 Mon Sep 17 00:00:00 2001 From: Mauricio Zanetti Salomao Date: Wed, 7 Jan 2026 16:48:24 -0300 Subject: [PATCH 1/5] (feat): Add member visibility setting for basic profile access and update related structures Jira Ticket: https://linuxfoundation.atlassian.net/browse/LFXV2-920 Assisted by [Claude Code](https://claude.ai/code) Signed-off-by: Mauricio Zanetti Salomao --- .../lfx-v2-committee-service/templates/ruleset.yaml | 2 +- internal/domain/model/committee_base.go | 11 +++++++++++ internal/domain/model/committee_message.go | 2 ++ internal/service/committee_writer.go | 4 ++++ pkg/constants/access_control.go | 2 ++ 5 files changed, 20 insertions(+), 1 deletion(-) diff --git a/charts/lfx-v2-committee-service/templates/ruleset.yaml b/charts/lfx-v2-committee-service/templates/ruleset.yaml index 8a8a9e4..9f9d6fd 100644 --- a/charts/lfx-v2-committee-service/templates/ruleset.yaml +++ b/charts/lfx-v2-committee-service/templates/ruleset.yaml @@ -245,7 +245,7 @@ spec: - authorizer: openfga_check config: values: - relation: viewer + relation: basic_profile_viewer object: "committee:{{ "{{- .Request.URL.Captures.uid -}}" }}" {{- else }} - authorizer: allow_all diff --git a/internal/domain/model/committee_base.go b/internal/domain/model/committee_base.go index de5e504..a345638 100644 --- a/internal/domain/model/committee_base.go +++ b/internal/domain/model/committee_base.go @@ -18,6 +18,8 @@ import ( const ( categoryGovernmentAdvisoryCouncil = "Government Advisory Council" + + memberVisibilityBasicProfileSetting = "basic_profile" ) // Committee represents the core committee business entity @@ -155,3 +157,12 @@ func (c *Committee) Tags() []string { func (c *Committee) IsGovernmentAdvisoryCouncil() bool { return c.Category == categoryGovernmentAdvisoryCouncil } + +// IsMemberVisibilityBasicProfile returns true if the committee's member visibility setting is "basic_profile" +func (c *Committee) IsMemberVisibilityBasicProfile() bool { + if c.CommitteeSettings == nil { + return false + } + + return c.CommitteeSettings.MemberVisibility == memberVisibilityBasicProfileSetting +} diff --git a/internal/domain/model/committee_message.go b/internal/domain/model/committee_message.go index 328116e..25027d7 100644 --- a/internal/domain/model/committee_message.go +++ b/internal/domain/model/committee_message.go @@ -109,6 +109,8 @@ type CommitteeAccessMessage struct { // e.g. "project" and it's value is the project UID. // e.g. "parent" and it's value is the parent UID. References map[string]string `json:"references"` + // self is used to store the self relation of the object, e.g. for committee members to access their own basic profile info. + Self []string `json:"self"` } // CommitteeMemberUpdateEventData represents the data structure for committee member update events diff --git a/internal/service/committee_writer.go b/internal/service/committee_writer.go index 78b0001..b8f37f2 100644 --- a/internal/service/committee_writer.go +++ b/internal/service/committee_writer.go @@ -238,6 +238,10 @@ func (uc *committeeWriterOrchestrator) buildAccessControlMessage(ctx context.Con message.Relations[constants.RelationAuditor] = committee.Auditors } + if committee.CommitteeSettings != nil && committee.IsMemberVisibilityBasicProfile() { + message.Self = append(message.Self, constants.RelationSelfForMemberBasicProfileAccess) + } + slog.DebugContext(ctx, "building access control message", "message", message, ) diff --git a/pkg/constants/access_control.go b/pkg/constants/access_control.go index 141172a..582c7bc 100644 --- a/pkg/constants/access_control.go +++ b/pkg/constants/access_control.go @@ -12,4 +12,6 @@ const ( RelationWriter = "writer" // RelationAuditor is the relation name for the auditor of an object. RelationAuditor = "auditor" + // RelationSelfForMemberBasicProfileAccess is the relation name for committee members to access basic profile info. + RelationSelfForMemberBasicProfileAccess = "self_for_member_basic_profile_access" ) From 1fbc743487c4cf395954a940bc46ceb3a10d5991 Mon Sep 17 00:00:00 2001 From: Mauricio Zanetti Salomao Date: Wed, 7 Jan 2026 16:49:45 -0300 Subject: [PATCH 2/5] chore: bump chart version from 0.2.19 to 0.2.20 Jira Ticket: https://linuxfoundation.atlassian.net/browse/LFXV2-920 Assisted by [Claude Code](https://claude.ai/code) Signed-off-by: Mauricio Zanetti Salomao --- charts/lfx-v2-committee-service/Chart.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/lfx-v2-committee-service/Chart.yaml b/charts/lfx-v2-committee-service/Chart.yaml index 7245904..9ee6c31 100644 --- a/charts/lfx-v2-committee-service/Chart.yaml +++ b/charts/lfx-v2-committee-service/Chart.yaml @@ -5,5 +5,5 @@ apiVersion: v2 name: lfx-v2-committee-service description: LFX Platform V2 Committee Service chart type: application -version: 0.2.19 +version: 0.2.20 appVersion: "latest" From 90655a7dcbf55494c37d279dca302ccbfa55644e Mon Sep 17 00:00:00 2001 From: Mauricio Zanetti Salomao Date: Thu, 8 Jan 2026 09:26:39 -0300 Subject: [PATCH 3/5] fix: correct member visibility check in IsMemberVisibilityBasicProfile method Jira Ticket: https://linuxfoundation.atlassian.net/browse/LFXV2-920 Assisted by [Claude Code](https://claude.ai/code) Signed-off-by: Mauricio Zanetti Salomao --- internal/domain/model/committee_base.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/domain/model/committee_base.go b/internal/domain/model/committee_base.go index a345638..3e64244 100644 --- a/internal/domain/model/committee_base.go +++ b/internal/domain/model/committee_base.go @@ -164,5 +164,5 @@ func (c *Committee) IsMemberVisibilityBasicProfile() bool { return false } - return c.CommitteeSettings.MemberVisibility == memberVisibilityBasicProfileSetting + return c.MemberVisibility == memberVisibilityBasicProfileSetting } From c650253be217fa7a4d373b54008c86cd81b61d20 Mon Sep 17 00:00:00 2001 From: Mauricio Zanetti Salomao Date: Thu, 8 Jan 2026 11:13:29 -0300 Subject: [PATCH 4/5] feat: implement member visibility checks and update access control message structure Jira Ticket: https://linuxfoundation.atlassian.net/browse/LFXV2-920 Reviewed with [GitHub Copilot](https://github.com/features/copilot) Signed-off-by: Mauricio Zanetti Salomao --- internal/domain/model/committee_base_test.go | 63 ++++++++++++++++++++ internal/domain/model/committee_message.go | 3 +- internal/service/committee_writer.go | 1 + internal/service/committee_writer_test.go | 59 ++++++++++++++++++ 4 files changed, 125 insertions(+), 1 deletion(-) diff --git a/internal/domain/model/committee_base_test.go b/internal/domain/model/committee_base_test.go index 43732db..cea837a 100644 --- a/internal/domain/model/committee_base_test.go +++ b/internal/domain/model/committee_base_test.go @@ -590,3 +590,66 @@ func BenchmarkCommitteeTags_Parallel(b *testing.B) { } }) } + +func TestCommitteeIsMemberVisibilityBasicProfile(t *testing.T) { + tests := []struct { + name string + committee Committee + expected bool + }{ + { + name: "returns true when member visibility is basic_profile", + committee: Committee{ + CommitteeBase: CommitteeBase{}, + CommitteeSettings: &CommitteeSettings{ + MemberVisibility: "basic_profile", + }, + }, + expected: true, + }, + { + name: "returns false when member visibility is full_profile", + committee: Committee{ + CommitteeBase: CommitteeBase{}, + CommitteeSettings: &CommitteeSettings{ + MemberVisibility: "full_profile", + }, + }, + expected: false, + }, + { + name: "returns false when member visibility is empty string", + committee: Committee{ + CommitteeBase: CommitteeBase{}, + CommitteeSettings: &CommitteeSettings{ + MemberVisibility: "", + }, + }, + expected: false, + }, + { + name: "returns false when member visibility is other value", + committee: Committee{ + CommitteeBase: CommitteeBase{}, + CommitteeSettings: &CommitteeSettings{ + MemberVisibility: "private", + }, + }, + expected: false, + }, + { + name: "returns false when CommitteeSettings is nil", + committee: Committee{ + CommitteeBase: CommitteeBase{}, + CommitteeSettings: nil, + }, + expected: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.expected, tc.committee.IsMemberVisibilityBasicProfile()) + }) + } +} diff --git a/internal/domain/model/committee_message.go b/internal/domain/model/committee_message.go index 25027d7..6282085 100644 --- a/internal/domain/model/committee_message.go +++ b/internal/domain/model/committee_message.go @@ -109,7 +109,8 @@ type CommitteeAccessMessage struct { // e.g. "project" and it's value is the project UID. // e.g. "parent" and it's value is the parent UID. References map[string]string `json:"references"` - // self is used to store the self relation of the object, e.g. for committee members to access their own basic profile info. + // Self stores OpenFGA self-relation tuples that enable conditional member-to-member visibility. + // When populated, members can view other members' basic profiles based on committee visibility settings. Self []string `json:"self"` } diff --git a/internal/service/committee_writer.go b/internal/service/committee_writer.go index b8f37f2..724a5d6 100644 --- a/internal/service/committee_writer.go +++ b/internal/service/committee_writer.go @@ -228,6 +228,7 @@ func (uc *committeeWriterOrchestrator) buildAccessControlMessage(ctx context.Con // project is required in the flow constants.RelationProject: committee.ProjectUID, }, + Self: []string{}, } if committee.CommitteeSettings != nil && len(committee.Writers) > 0 { diff --git a/internal/service/committee_writer_test.go b/internal/service/committee_writer_test.go index dd4f70f..d3068a7 100644 --- a/internal/service/committee_writer_test.go +++ b/internal/service/committee_writer_test.go @@ -516,6 +516,7 @@ func TestCommitteeWriterOrchestrator_buildAccessControlMessage(t *testing.T) { References: map[string]string{ "project": "project-1", }, + Self: []string{}, }, }, { @@ -542,6 +543,7 @@ func TestCommitteeWriterOrchestrator_buildAccessControlMessage(t *testing.T) { References: map[string]string{ "project": "project-2", }, + Self: []string{}, }, }, { @@ -563,6 +565,63 @@ func TestCommitteeWriterOrchestrator_buildAccessControlMessage(t *testing.T) { References: map[string]string{ "project": "project-3", }, + Self: []string{}, + }, + }, + { + name: "committee with basic_profile member visibility", + committee: &model.Committee{ + CommitteeBase: model.CommitteeBase{ + UID: "committee-4", + ProjectUID: "project-4", + Public: false, + ParentUID: nil, + }, + CommitteeSettings: &model.CommitteeSettings{ + MemberVisibility: "basic_profile", + Writers: []string{"writer@example.com"}, + Auditors: []string{"auditor@example.com"}, + }, + }, + expected: &model.CommitteeAccessMessage{ + UID: "committee-4", + ObjectType: "committee", + Public: false, + Relations: map[string][]string{ + "writer": {"writer@example.com"}, + "auditor": {"auditor@example.com"}, + }, + References: map[string]string{ + "project": "project-4", + }, + Self: []string{"self_for_member_basic_profile_access"}, + }, + }, + { + name: "committee with non-basic_profile member visibility", + committee: &model.Committee{ + CommitteeBase: model.CommitteeBase{ + UID: "committee-5", + ProjectUID: "project-5", + Public: true, + ParentUID: nil, + }, + CommitteeSettings: &model.CommitteeSettings{ + MemberVisibility: "full_profile", + Writers: []string{"writer@example.com"}, + }, + }, + expected: &model.CommitteeAccessMessage{ + UID: "committee-5", + ObjectType: "committee", + Public: true, + Relations: map[string][]string{ + "writer": {"writer@example.com"}, + }, + References: map[string]string{ + "project": "project-5", + }, + Self: []string{}, }, }, } From 893404c74351e26e7f810a3cff00b6f4b41e406a Mon Sep 17 00:00:00 2001 From: Mauricio Zanetti Salomao Date: Thu, 8 Jan 2026 11:51:53 -0300 Subject: [PATCH 5/5] refactor: adjust formatting in TestCommitteeIsMemberVisibilityBasicProfile Jira Ticket: https://linuxfoundation.atlassian.net/browse/LFXV2-920 Assisted by [Claude Code](https://claude.ai/code) Signed-off-by: Mauricio Zanetti Salomao --- internal/domain/model/committee_base_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/domain/model/committee_base_test.go b/internal/domain/model/committee_base_test.go index cea837a..20afd09 100644 --- a/internal/domain/model/committee_base_test.go +++ b/internal/domain/model/committee_base_test.go @@ -593,9 +593,9 @@ func BenchmarkCommitteeTags_Parallel(b *testing.B) { func TestCommitteeIsMemberVisibilityBasicProfile(t *testing.T) { tests := []struct { - name string + name string committee Committee - expected bool + expected bool }{ { name: "returns true when member visibility is basic_profile",