Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion charts/lfx-v2-committee-service/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
2 changes: 1 addition & 1 deletion charts/lfx-v2-committee-service/templates/ruleset.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions internal/domain/model/committee_base.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import (

const (
categoryGovernmentAdvisoryCouncil = "Government Advisory Council"

memberVisibilityBasicProfileSetting = "basic_profile"
)

// Committee represents the core committee business entity
Expand Down Expand Up @@ -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.MemberVisibility == memberVisibilityBasicProfileSetting
}
63 changes: 63 additions & 0 deletions internal/domain/model/committee_base_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
})
}
}
3 changes: 3 additions & 0 deletions internal/domain/model/committee_message.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,9 @@ 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 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"`
}

// CommitteeMemberUpdateEventData represents the data structure for committee member update events
Expand Down
5 changes: 5 additions & 0 deletions internal/service/committee_writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -238,6 +239,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,
)
Expand Down
59 changes: 59 additions & 0 deletions internal/service/committee_writer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,7 @@ func TestCommitteeWriterOrchestrator_buildAccessControlMessage(t *testing.T) {
References: map[string]string{
"project": "project-1",
},
Self: []string{},
},
},
{
Expand All @@ -542,6 +543,7 @@ func TestCommitteeWriterOrchestrator_buildAccessControlMessage(t *testing.T) {
References: map[string]string{
"project": "project-2",
},
Self: []string{},
},
},
{
Expand All @@ -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{"[email protected]"},
Auditors: []string{"[email protected]"},
},
},
expected: &model.CommitteeAccessMessage{
UID: "committee-4",
ObjectType: "committee",
Public: false,
Relations: map[string][]string{
"writer": {"[email protected]"},
"auditor": {"[email protected]"},
},
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{"[email protected]"},
},
},
expected: &model.CommitteeAccessMessage{
UID: "committee-5",
ObjectType: "committee",
Public: true,
Relations: map[string][]string{
"writer": {"[email protected]"},
},
References: map[string]string{
"project": "project-5",
},
Self: []string{},
},
},
}
Expand Down
2 changes: 2 additions & 0 deletions pkg/constants/access_control.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)