Skip to content

[LFXV2-1476] feat(fga): add granular committee member access — roster_viewer and email_viewer#79

Open
andrest50 wants to merge 12 commits into
mainfrom
feat/LFXV2-1476-committee-fga-granular-scopes
Open

[LFXV2-1476] feat(fga): add granular committee member access — roster_viewer and email_viewer#79
andrest50 wants to merge 12 commits into
mainfrom
feat/LFXV2-1476-committee-fga-granular-scopes

Conversation

@andrest50

@andrest50 andrest50 commented Apr 16, 2026

Copy link
Copy Markdown
Contributor

What changed

This PR splits committee member data access into two distinct scopes, aligning the API with the FGA model changes in lfx-v2-helm:

  • roster_viewer — can see member names, roles, voting status, and org info
  • email_viewer — can see email addresses (gated separately)

Previously GET /committees/{uid}/members/{member_uid} returned everything including email, and was checked against a single viewer relation. Now email lives behind its own endpoint and its own FGA relation.


Response type split

GET /committees/{uid}/members/{member_uid} → checked against roster_viewer

Returns profile and role data — no email:

{
  "uid": "...",
  "committee_uid": "...",
  "committee_name": "...",
  "committee_category": "...",
  "username": "...",
  "first_name": "...",
  "last_name": "...",
  "job_title": "...",
  "linkedin_profile": "...",
  "appointed_by": "...",
  "status": "...",
  "role": { "name": "...", "start_date": "...", "end_date": "..." },
  "voting": { "status": "...", "start_date": "...", "end_date": "..." },
  "organization": { "id": "...", "name": "...", "website": "..." },
  "created_at": "...",
  "updated_at": "..."
}

GET /committees/{uid}/members/{member_uid}/contact → checked against email_viewer (new)

Returns only the identifiers needed to look up contact info:

{
  "uid": "...",
  "committee_uid": "...",
  "email": "user@example.com"
}

Per-committee member visibility (member_visibility)

A new member_visibility setting on CommitteeSettings controls whether committee members can see each other, without affecting what staff/auditors can see:

Value Effect
hidden (default) Members cannot see each other — neither roster_viewer nor email_viewer is set
basic_profile Members can see each other's names and roles — sets committee_for_member_roster_access to the committee itself
full_profile Members can see each other's names, roles, and emails — sets both committee_for_member_roster_access and committee_for_member_email_access to the committee itself

These are self-referential FGA references (analogous to vote_for_participant_result_access): when set, the FGA model grants all members of the committee the corresponding viewer relation on each other.


OpenSearch indexing

A new index subject lfx.index.committee_member_sensitive has been added alongside the existing lfx.index.committee_member. This allows the indexer to store email in a separate document type gated by the email_viewer relation, keeping sensitive fields out of the general member index.

On member deletion, a delete event is now published to both index subjects so that the email document is removed from the sensitive index and does not remain accessible.


Ruleset changes

# Before
- id: "rule:lfx:lfx-v2-committee-service:committee_members:get"
  # ...
  relation: viewer

# After — two rules, two relations
- id: "rule:lfx:lfx-v2-committee-service:committee_members:get"
  # ...
  relation: roster_viewer

- id: "rule:lfx:lfx-v2-committee-service:committee_members:get_contact"
  match:
    routes:
      - path: /committees/:uid/members/:member_uid/contact
  # ...
  relation: email_viewer

Ticket

LFXV2-1476

🤖 Generated with Claude Code

…ittee access control

- Add RelationRosterViewer, RelationEmailViewer,
  RelationCommitteeForMemberRosterAccess, MemberVisibilityBasicProfile,
  and MemberVisibilityHidden constants to pkg/constants/access_control.go
- committee_writer.go: set roster_viewer: ["*"] on public committees;
  set committee_for_member_roster_access self-pointer when
  member_visibility = "basic_profile"
- committee_member_writer.go: switch access_check_relation from "viewer"
  to constants.RelationRosterViewer; remove email from fulltext
- Update docs/fga-contract.md and docs/indexer-contract.md to reflect
  the new relations and the email-exclusion rationale

Generated with [Claude Code](https://claude.ai/code)

Signed-off-by: Andres Tobon <andrest2455@gmail.com>
…ranular roster access

- Split committee member response: CommitteeMemberFullWithReadonlyAttributes no longer
  includes email; email is now returned only via the new contact endpoint
- Add CommitteeMemberBasicBaseAttributes() Goa design function (all non-email fields)
- Add CommitteeMemberContactWithReadonlyAttributes Goa type (uid, committee_uid, email)
- Add get-committee-member-contact endpoint: GET /committees/{uid}/members/{member_uid}/contact
- Add GetCommitteeMemberContact service handler (reuses GetMember orchestrator)
- Add convertMemberDomainToContactResponse conversion (uid + committee_uid + email only)
- Update ruleset: committee_members:get now checks roster_viewer relation
- Add ruleset rule: committee_members:get_contact checks email_viewer relation
- Regenerate all gen/ files via make apigen
- Update tests to remove Email field from CommitteeMemberFullWithReadonlyAttributes assertions

Generated with [Claude Code](https://claude.ai/code)

Signed-off-by: Andres Tobon <andrest2455@gmail.com>
Copilot AI review requested due to automatic review settings April 16, 2026 02:15
@andrest50 andrest50 requested a review from a team as a code owner April 16, 2026 02:15
@coderabbitai

coderabbitai Bot commented Apr 16, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Splits member email into a new sensitive struct, adds GET /committees/{uid}/members/{member_uid}/contact, replaces viewer with roster_viewer for roster access and introduces email_viewer for email-gated access; updates FGA/indexer contracts, constants, writer/indexer logic, API types, services, and tests.

Changes

Cohort / File(s) Summary
Authorization & Chart
charts/lfx-v2-committee-service/templates/ruleset.yaml
Replaced viewer with roster_viewer for committee_members:get; added committee_members:get_contact rule using email_viewer on committee:{{ .Request.URL.Captures.uid }} and explicit object binding.
API Design & Types
cmd/committee-api/design/committee.go, cmd/committee-api/design/type.go
Added get-committee-member-contact endpoint and CommitteeMemberContactWithReadonlyAttributes; introduced CommitteeMemberBasicBaseAttributes() and added full_profile to member_visibility.
Service Implementation
cmd/committee-api/service/committee_service.go, cmd/committee-api/service/committee_service_response.go
Added GetCommitteeMemberContact and convertMemberDomainToContactResponse; moved email into CommitteeMemberSensitive and removed email from full-member responses and constructors.
Domain Model
internal/domain/model/committee_member.go
Added CommitteeMemberSensitive{Email} and removed Email from CommitteeMemberBase; validation still requires email but sourced from sensitive struct; tags no longer include email.
Indexer & Writer
internal/service/committee_member_writer.go, internal/service/committee_writer.go
Changed roster index relation to roster_viewer, removed email from roster fulltext, added sensitive email-only index publish (IndexCommitteeMemberSensitiveSubject) gated by email_viewer, and adjusted access-control message references based on member visibility.
Constants & Subjects
pkg/constants/access_control.go, pkg/constants/subjects.go
Added RelationRosterViewer, RelationEmailViewer, RelationCommitteeForMemberRosterAccess, RelationCommitteeForMemberEmailAccess, visibility constants (hidden, basic_profile, full_profile), and IndexCommitteeMemberSensitiveSubject.
Tests — Service & Responses
cmd/committee-api/service/committee_service_response_test.go, cmd/committee-api/service/committee_service_test.go
Updated fixtures to use CommitteeMemberSensitive.Email; removed email from full responses; added tests for GetCommitteeMemberContact, error propagation, and a mock GetMember recorder.
Tests — Domain, Mocks & Writer
internal/domain/model/committee_member_test.go, internal/infrastructure/mock/committee.go, internal/service/committee_member_writer_test.go, internal/service/committee_reader_test.go, internal/service/committee_writer_test.go
Adjusted test data/expectations to place email in CommitteeMemberSensitive; updated tag/index expectations and writer access-control test cases to include roster_viewer and visibility-driven references.
Docs
docs/fga-contract.md, docs/indexer-contract.md
Documented roster_viewer, committee_for_member_roster_access, and email visibility rules; removed email from indexer fulltext and explained rationale.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant API as Committee API
    participant FGA as OpenFGA
    participant DB as Storage/Indexer

    Client->>API: GET /committees/{uid}/members/{member_uid}/contact (Bearer)
    API->>FGA: Check relation "email_viewer" on object committee:{uid}
    alt FGA allowed
        FGA-->>API: allow
        API->>DB: Read member (includes CommitteeMemberSensitive.Email)
        DB-->>API: Member with sensitive email
        API-->>Client: 200 OK (uid, committee_uid, email)
    else FGA denied
        FGA-->>API: deny
        API-->>Client: 403 Forbidden
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.42% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding granular committee member access with two distinct FGA relations (roster_viewer and email_viewer), which is the primary objective of the changeset.
Description check ✅ Passed The PR description clearly explains the changes: splitting member data access into roster_viewer and email_viewer relations, adding a new contact endpoint, implementing member visibility settings, and updating indexing strategy.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/LFXV2-1476-committee-fga-granular-scopes

Comment @coderabbitai help to get the list of available commands and usage tips.

…s for roster_viewer

Public committees now include roster_viewer: ["*"] in the FGA message.
Update the two test cases with Public: true to include this relation.

Generated with [Claude Code](https://claude.ai/code)

Signed-off-by: Andres Tobon <andrest2455@gmail.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR aligns the committee-service API and indexing behavior with updated FGA relations by splitting committee member access into (1) roster visibility (non-sensitive profile/role info) and (2) email visibility via a new contact endpoint.

Changes:

  • Removes email from the primary committee member “full” response and introduces GET /committees/{uid}/members/{member_uid}/contact for email retrieval.
  • Adds new FGA relations/constants (roster_viewer, email_viewer, and self-referential committee_for_member_roster_access) and updates ruleset/docs accordingly.
  • Updates committee-member indexing configuration to use roster_viewer and removes email from the fulltext field.

Reviewed changes

Copilot reviewed 34 out of 36 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
pkg/constants/subjects.go Adds a new index subject constant for sensitive committee member indexing.
pkg/constants/access_control.go Introduces new FGA relation constants and member_visibility constants.
internal/service/committee_writer.go Updates committee access-control message construction to set roster_viewer for public committees and member-visibility references.
internal/service/committee_reader_test.go Updates tests to move email into CommitteeMemberSensitive.
internal/service/committee_member_writer_test.go Updates writer tests for the new sensitive email struct placement.
internal/service/committee_member_writer.go Switches indexing access relation to roster_viewer and removes email from fulltext.
internal/infrastructure/mock/committee.go Updates mock data to place email under CommitteeMemberSensitive.
internal/domain/model/committee_member_test.go Adjusts validation/tags/index-key tests for sensitive email separation.
internal/domain/model/committee_member.go Introduces CommitteeMemberSensitive and removes email from the base struct; removes email tags.
gen/http/openapi3.yaml Updates OpenAPI v3 docs: removes email from roster response and adds contact endpoint/schema.
gen/http/openapi.yaml Updates Swagger v2 YAML docs similarly (new contact endpoint + schema).
gen/http/openapi.json Updates generated Swagger JSON (new endpoint and removal of email from roster response schema).
gen/http/committee_service/server/types.go Updates server transport types to remove email from roster responses and add contact response types.
gen/http/committee_service/server/server.go Mounts/initializes the new get-committee-member-contact handler.
gen/http/committee_service/server/paths.go Adds path helper for the contact endpoint.
gen/http/committee_service/server/encode_decode.go Adds encoder/decoder + error encoding for the contact endpoint.
gen/http/committee_service/client/types.go Updates client transport types for removed email fields and new contact response types.
gen/http/committee_service/client/paths.go Adds client path helper for the contact endpoint.
gen/http/committee_service/client/encode_decode.go Adds client request builder/encoder/decoder for the contact endpoint.
gen/http/committee_service/client/client.go Adds client endpoint method for get-committee-member-contact.
gen/http/committee_service/client/cli.go Adds CLI payload builder for the contact endpoint.
gen/http/cli/committee/cli.go Wires CLI parsing/usage for get-committee-member-contact.
gen/committee_service/service.go Adds service interface + payload/result types for get-committee-member-contact; removes email from roster result type.
gen/committee_service/endpoints.go Adds endpoint wiring for get-committee-member-contact.
gen/committee_service/client.go Adds strongly-typed service client method for get-committee-member-contact.
docs/indexer-contract.md Updates indexer contract docs for roster_viewer and removes email from fulltext description.
docs/fga-contract.md Documents the new roster_viewer relation and self-referential member visibility reference.
cmd/committee-api/service/committee_service_test.go Updates service tests to reflect email moving into the sensitive struct.
cmd/committee-api/service/committee_service_response_test.go Updates response conversion tests for sensitive email separation.
cmd/committee-api/service/committee_service_response.go Updates payload/domain conversions and adds contact-response conversion helper.
cmd/committee-api/service/committee_service.go Adds GetCommitteeMemberContact endpoint implementation and updates member creation flows for sensitive email.
cmd/committee-api/design/type.go Adds design types/attribute grouping for roster vs contact shapes.
cmd/committee-api/design/committee.go Adds the new get-committee-member-contact endpoint to the Goa design.
charts/lfx-v2-committee-service/templates/ruleset.yaml Updates ruleset: GET member uses roster_viewer; adds GET contact rule using email_viewer.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/service/committee_member_writer.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
internal/domain/model/committee_member_test.go (1)

410-482: Use the expected hashes or remove them.

This table now carries expected values and “SHA-256 of …” comments, but the test never compares result against them. As written, it only proves “deterministic 64-char hex”, not that the key is actually derived from committee_uid|email. Either assert the expected hash or compute it inline so this contract stays covered.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/domain/model/committee_member_test.go` around lines 410 - 482, The
table includes expected SHA-256 strings but the test never asserts them; update
the subtest for each case to assert that result == expected (or remove the
unused expected fields). Specifically, in the loop where you call
CommitteeMember.BuildIndexKey(ctx), add an assertion comparing the returned
result to the test case's expected value (tt.expected) or, if you prefer, remove
the expected field from the test cases; if you keep expected, ensure the strings
are the actual SHA-256 hex of CommitteeMemberBase.CommitteeUID + "|" +
CommitteeMemberSensitive.Email so the check verifies the intended contract.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cmd/committee-api/service/committee_service.go`:
- Around line 295-301: The new GetCommitteeMemberContact handler currently lacks
unit tests to assert that GetMember (committeeReaderOrchestrator.GetMember)
returns a CommitteeMember with CommitteeMemberSensitive.Email and that the
handler returns that email via convertMemberDomainToContactResponse; add tests
for the GetCommitteeMemberContact endpoint that mock
committeeReaderOrchestrator.GetMember to return a CommitteeMember containing
CommitteeMemberSensitive.Email, verify the handler calls GetMember with ctx,
p.UID and p.MemberUID, and assert the HTTP/response payload contains the
expected email and proper error handling when GetMember returns an error.

In `@internal/service/committee_member_writer.go`:
- Around line 790-803: The roster-level indexing branch currently sets
indexerMessage.IndexingConfig and still publishes the full data.Member and
data.Member.Tags(), which can leak emails; update the roster_viewer branch (the
code that builds indexerMessage.IndexingConfig and assigns memberData) to
publish a sanitized member payload (or filtered Tags) that removes email-related
fields and tags before sending to the roster index, and ensure any email value
is only emitted to the separate sensitive index
(lfx.index.committee_member_sensitive) with AccessCheckRelation/email_viewer;
specifically modify the logic around indexerMessage.IndexingConfig, memberData,
and data.Member.Tags() so roster viewers never receive email-bearing fields
while keeping the sensitive email-only index with access_check_relation =
"email_viewer".

---

Nitpick comments:
In `@internal/domain/model/committee_member_test.go`:
- Around line 410-482: The table includes expected SHA-256 strings but the test
never asserts them; update the subtest for each case to assert that result ==
expected (or remove the unused expected fields). Specifically, in the loop where
you call CommitteeMember.BuildIndexKey(ctx), add an assertion comparing the
returned result to the test case's expected value (tt.expected) or, if you
prefer, remove the expected field from the test cases; if you keep expected,
ensure the strings are the actual SHA-256 hex of
CommitteeMemberBase.CommitteeUID + "|" + CommitteeMemberSensitive.Email so the
check verifies the intended contract.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ca4b964c-fc6a-408d-a00f-0d2571201784

📥 Commits

Reviewing files that changed from the base of the PR and between 0ee8b27 and 69227e2.

⛔ Files ignored due to path filters (17)
  • gen/committee_service/client.go is excluded by !**/gen/**
  • gen/committee_service/endpoints.go is excluded by !**/gen/**
  • gen/committee_service/service.go is excluded by !**/gen/**
  • gen/http/cli/committee/cli.go is excluded by !**/gen/**
  • gen/http/committee_service/client/cli.go is excluded by !**/gen/**
  • gen/http/committee_service/client/client.go is excluded by !**/gen/**
  • gen/http/committee_service/client/encode_decode.go is excluded by !**/gen/**
  • gen/http/committee_service/client/paths.go is excluded by !**/gen/**
  • gen/http/committee_service/client/types.go is excluded by !**/gen/**
  • gen/http/committee_service/server/encode_decode.go is excluded by !**/gen/**
  • gen/http/committee_service/server/paths.go is excluded by !**/gen/**
  • gen/http/committee_service/server/server.go is excluded by !**/gen/**
  • gen/http/committee_service/server/types.go is excluded by !**/gen/**
  • gen/http/openapi.json is excluded by !**/gen/**
  • gen/http/openapi.yaml is excluded by !**/gen/**
  • gen/http/openapi3.json is excluded by !**/gen/**
  • gen/http/openapi3.yaml is excluded by !**/gen/**
📒 Files selected for processing (18)
  • charts/lfx-v2-committee-service/templates/ruleset.yaml
  • cmd/committee-api/design/committee.go
  • cmd/committee-api/design/type.go
  • cmd/committee-api/service/committee_service.go
  • cmd/committee-api/service/committee_service_response.go
  • cmd/committee-api/service/committee_service_response_test.go
  • cmd/committee-api/service/committee_service_test.go
  • docs/fga-contract.md
  • docs/indexer-contract.md
  • internal/domain/model/committee_member.go
  • internal/domain/model/committee_member_test.go
  • internal/infrastructure/mock/committee.go
  • internal/service/committee_member_writer.go
  • internal/service/committee_member_writer_test.go
  • internal/service/committee_reader_test.go
  • internal/service/committee_writer.go
  • pkg/constants/access_control.go
  • pkg/constants/subjects.go

Comment thread cmd/committee-api/service/committee_service.go
Comment thread internal/service/committee_member_writer.go
…tact endpoint tests

- Prevent email leak in roster index: use CommitteeMemberBase (no email)
  for the lfx.index.committee_member payload; publish a second sensitive
  indexer message to lfx.index.committee_member_sensitive gated by
  email_viewer
- Fix TestCommitteeMember_BuildIndexKey: replace placeholder expected
  strings with real SHA-256 hashes and add result assertions
- Add TestGetCommitteeMemberContact: covers happy-path email return,
  not-found propagation, and unexpected-error propagation; verifies
  GetMember is called with correct committeeUID and memberUID

Generated with [Claude Code](https://claude.com/claude-code)

Signed-off-by: Andres Tobon <andrest2455@gmail.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/service/committee_member_writer.go (1)

821-889: ⚠️ Potential issue | 🟠 Major

Emit a delete to the sensitive index too.

lfx.index.committee_member_sensitive is a separate subject, but this path only builds/publishes it for create/update. On member delete, the roster document is removed while the last email document can remain indexed under email_viewer.

Suggested fix
-	var sensitiveIndexerMessageBuild *model.CommitteeIndexerMessage
-	if action == model.ActionCreated || action == model.ActionUpdated {
+	var sensitiveIndexerMessageBuild *model.CommitteeIndexerMessage
+	if action == model.ActionCreated || action == model.ActionUpdated || action == model.ActionDeleted {
 		sensitiveIndexerMessage := model.CommitteeIndexerMessage{
 			Action: action,
 			IndexingConfig: &indexerTypes.IndexingConfig{
 				ObjectID:            data.Member.UID,
 				AccessCheckObject:   fmt.Sprintf("committee:%s", data.Member.CommitteeUID),
 				AccessCheckRelation: constants.RelationEmailViewer,
 				ParentRefs:          []string{fmt.Sprintf("committee:%s", data.Member.CommitteeUID)},
 			},
 		}
-		sensitivePayload := struct {
-			UID          string `json:"uid"`
-			CommitteeUID string `json:"committee_uid"`
-			Email        string `json:"email"`
-		}{
-			UID:          data.Member.UID,
-			CommitteeUID: data.Member.CommitteeUID,
-			Email:        data.Member.Email,
-		}
+		var sensitivePayload any = data.Member.UID
+		if action != model.ActionDeleted {
+			sensitivePayload = struct {
+				UID          string `json:"uid"`
+				CommitteeUID string `json:"committee_uid"`
+				Email        string `json:"email"`
+			}{
+				UID:          data.Member.UID,
+				CommitteeUID: data.Member.CommitteeUID,
+				Email:        data.Member.Email,
+			}
+		}
 		built, errBuildSensitive := sensitiveIndexerMessage.Build(ctx, sensitivePayload)
 		if errBuildSensitive != nil {
 			slog.ErrorContext(ctx, "failed to build sensitive member indexer message",
 				"error", errBuildSensitive,
 				"action", action,
 			)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/service/committee_member_writer.go` around lines 821 - 889, The
sensitive indexer message is only built for model.ActionCreated and
model.ActionUpdated, so deletes never emit to the sensitive subject; expand the
build condition to include model.ActionDeleted (i.e., change the if that creates
sensitiveIndexerMessage/sensitivePayload to run for ActionCreated ||
ActionUpdated || ActionDeleted) and ensure the sensitivePayload still includes
UID and CommitteeUID (and Email if available, or empty string) so that
sensitiveIndexerMessageBuild is non-nil for deletes and will be published by the
existing publisher goroutine. Use the same symbols shown
(sensitiveIndexerMessage, sensitivePayload, sensitiveIndexerMessageBuild,
action, model.ActionDeleted) to locate and update the code.
🧹 Nitpick comments (2)
cmd/committee-api/service/committee_service_test.go (1)

1603-1611: Assert concrete GOA error types in failure-path contact tests.

The cases named “propagates not-found/unexpected” currently only check require.Error (Line 1657). Add require.ErrorAs to lock in *committeeservice.NotFoundError and *committeeservice.InternalServerError mapping, otherwise a wrong error translation could still pass.

💡 Tighten failure assertions
 func TestGetCommitteeMemberContact(t *testing.T) {
 	tests := []struct {
 		name           string
 		payload        *committeeservice.GetCommitteeMemberContactPayload
 		setupMock      func(*mockCommitteeReaderOrchestrator)
 		expectError    bool
+		validateError  func(*testing.T, error)
 		validateResult func(*testing.T, *committeeservice.CommitteeMemberContactWithReadonlyAttributes)
 		validateCalls  func(*testing.T, []getMemberCall)
 	}{
 ...
 		{
 			name: "propagates not-found error from GetMember",
 ...
 			expectError: true,
+			validateError: func(t *testing.T, err error) {
+				var nfErr *committeeservice.NotFoundError
+				require.ErrorAs(t, err, &nfErr)
+			},
 			validateResult: func(t *testing.T, res *committeeservice.CommitteeMemberContactWithReadonlyAttributes) {
 				assert.Nil(t, res)
 			},
 ...
 		{
 			name: "propagates unexpected error from GetMember",
 ...
 			expectError: true,
+			validateError: func(t *testing.T, err error) {
+				var internalErr *committeeservice.InternalServerError
+				require.ErrorAs(t, err, &internalErr)
+			},
 			validateResult: func(t *testing.T, res *committeeservice.CommitteeMemberContactWithReadonlyAttributes) {
 				assert.Nil(t, res)
 			},
 ...
 			if tt.expectError {
 				require.Error(t, err)
+				if tt.validateError != nil {
+					tt.validateError(t, err)
+				}
 			} else {
 				require.NoError(t, err)
 			}

Also applies to: 1622-1630, 1657-1661

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/committee-api/service/committee_service_test.go` around lines 1603 -
1611, The failing-path tests for GetCommitteeMemberContact (cases like
"propagates not-found error from GetMember" that set
mockCommitteeReaderOrchestrator.getMemberErr) only call require.Error; update
those cases to also assert the concrete Goa error type using require.ErrorAs
(e.g. require.ErrorAs(t, err, new(*committeeservice.NotFoundError)) for
not-found cases and require.ErrorAs(t, err,
new(*committeeservice.InternalServerError)) for unexpected/internal cases) so
the test locks in the exact translation of errors when invoking the handler for
GetCommitteeMemberContactPayload.
internal/domain/model/committee_member_test.go (1)

415-425: Add one normalization case for BuildIndexKey().

BuildIndexKey() trims and lowercases both CommitteeUID and Email before hashing (internal/domain/model/committee_member.go:80-88), but every case here is already normalized. Adding a " Committee-123 " / " Test@Example.com " case that expects the same digest as the basic member would lock in the dedupe contract and catch casing/whitespace regressions.

Suggested test case
 	{
 		name: "basic member",
 		member: &CommitteeMember{
 			CommitteeMemberBase: CommitteeMemberBase{
 				CommitteeUID: "committee-123",
 			},
 			CommitteeMemberSensitive: CommitteeMemberSensitive{Email: "test@example.com"},
 		},
 		// SHA-256 of "committee-123|test@example.com"
 		expected: "93548eeb4f04488dfe77d98d56f0642fff5e1c9637314866d07e9f289cc4343a",
 	},
+	{
+		name: "normalizes committee uid and email",
+		member: &CommitteeMember{
+			CommitteeMemberBase: CommitteeMemberBase{
+				CommitteeUID: " Committee-123 ",
+			},
+			CommitteeMemberSensitive: CommitteeMemberSensitive{Email: " Test@Example.com "},
+		},
+		// Normalizes to "committee-123|test@example.com"
+		expected: "93548eeb4f04488dfe77d98d56f0642fff5e1c9637314866d07e9f289cc4343a",
+	},

Also applies to: 427-447, 449-467

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/domain/model/committee_member_test.go` around lines 415 - 425, Add a
normalization test case for BuildIndexKey(): create a new CommitteeMember
instance with CommitteeMemberBase.CommitteeUID set to " Committee-123 " and
CommitteeMemberSensitive.Email set to " Test@Example.com " and assert that
BuildIndexKey() returns the same digest as the existing basic member
("93548eeb4f04488dfe77d98d56f0642fff5e1c9637314866d07e9f289cc4343a"); this
verifies the trimming/lowercasing logic in BuildIndexKey() and should be added
alongside the other cases that reference CommitteeMember, CommitteeMemberBase,
and CommitteeMemberSensitive so it fails if normalization regresses.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@internal/service/committee_member_writer.go`:
- Around line 821-889: The sensitive indexer message is only built for
model.ActionCreated and model.ActionUpdated, so deletes never emit to the
sensitive subject; expand the build condition to include model.ActionDeleted
(i.e., change the if that creates sensitiveIndexerMessage/sensitivePayload to
run for ActionCreated || ActionUpdated || ActionDeleted) and ensure the
sensitivePayload still includes UID and CommitteeUID (and Email if available, or
empty string) so that sensitiveIndexerMessageBuild is non-nil for deletes and
will be published by the existing publisher goroutine. Use the same symbols
shown (sensitiveIndexerMessage, sensitivePayload, sensitiveIndexerMessageBuild,
action, model.ActionDeleted) to locate and update the code.

---

Nitpick comments:
In `@cmd/committee-api/service/committee_service_test.go`:
- Around line 1603-1611: The failing-path tests for GetCommitteeMemberContact
(cases like "propagates not-found error from GetMember" that set
mockCommitteeReaderOrchestrator.getMemberErr) only call require.Error; update
those cases to also assert the concrete Goa error type using require.ErrorAs
(e.g. require.ErrorAs(t, err, new(*committeeservice.NotFoundError)) for
not-found cases and require.ErrorAs(t, err,
new(*committeeservice.InternalServerError)) for unexpected/internal cases) so
the test locks in the exact translation of errors when invoking the handler for
GetCommitteeMemberContactPayload.

In `@internal/domain/model/committee_member_test.go`:
- Around line 415-425: Add a normalization test case for BuildIndexKey(): create
a new CommitteeMember instance with CommitteeMemberBase.CommitteeUID set to "
Committee-123 " and CommitteeMemberSensitive.Email set to " Test@Example.com "
and assert that BuildIndexKey() returns the same digest as the existing basic
member ("93548eeb4f04488dfe77d98d56f0642fff5e1c9637314866d07e9f289cc4343a");
this verifies the trimming/lowercasing logic in BuildIndexKey() and should be
added alongside the other cases that reference CommitteeMember,
CommitteeMemberBase, and CommitteeMemberSensitive so it fails if normalization
regresses.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ce450a0f-4e5f-485b-b608-ebc38bd94e9d

📥 Commits

Reviewing files that changed from the base of the PR and between cbde4b2 and b7c4069.

📒 Files selected for processing (3)
  • cmd/committee-api/service/committee_service_test.go
  • internal/domain/model/committee_member_test.go
  • internal/service/committee_member_writer.go

… access

Splits member_visibility into three tiers:
- hidden: no member-to-member visibility (default)
- basic_profile: members can view each other's names & roles (roster only)
- full_profile: members can view each other's names, roles, and email addresses

Adds RelationCommitteeForMemberEmailAccess constant and wires it into
buildAccessControlMessage so full_profile sets both the roster and email
self-referential FGA relations. Adds test coverage for both new cases.

Generated with [Claude Code](https://claude.com/claude-code)

Signed-off-by: Andres Tobon <andrest2455@gmail.com>
Copilot AI review requested due to automatic review settings April 16, 2026 18:16
…cessControlMessage

Adds a test case verifying that member_visibility=hidden leaves both
committee_for_member_roster_access and committee_for_member_email_access
unset, so members cannot see each other at all.

Generated with [Claude Code](https://claude.ai/code)

Signed-off-by: Andres Tobon <andrest2455@gmail.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
internal/service/committee_writer_test.go (1)

641-695: Consider adding an explicit hidden visibility test case.

You already cover default/no-visibility paths, but an explicit MemberVisibilityHidden case would make this contract clearer and more regression-proof.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/service/committee_writer_test.go` around lines 641 - 695, Add an
explicit test case in committee_writer_test.go that covers
MemberVisibilityHidden: create a model.Committee with
CommitteeSettings.MemberVisibility set to constants.MemberVisibilityHidden
(e.g., UID "committee-hidden", ProjectUID "project-hidden") and assert the
produced fgatypes.GenericFGAMessage (GenericAccessData) contains only the
project reference (no "committee_for_member_roster_access" or
"committee_for_member_email_access") and still includes ExcludeRelations:
[]string{"member"}; mirror the structure of the existing "basic_profile" /
"full_profile" cases and name it something like "hidden_profile sets no member
access" so the behavior is explicit and regression-tested.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cmd/committee-api/design/type.go`:
- Around line 747-750: Fix the spelling typo in the description string for the
member_visibility attribute: change "Dertermines the visibility level of members
profiles to other members of the same committee" to "Determines the visibility
level of members' profiles to other members of the same committee". Update the
description in the MemberVisibilityAttribute function (the dsl.Attribute call
for "member_visibility") and while editing also add the missing apostrophe in
"members' profiles" for correct possessive form.

---

Nitpick comments:
In `@internal/service/committee_writer_test.go`:
- Around line 641-695: Add an explicit test case in committee_writer_test.go
that covers MemberVisibilityHidden: create a model.Committee with
CommitteeSettings.MemberVisibility set to constants.MemberVisibilityHidden
(e.g., UID "committee-hidden", ProjectUID "project-hidden") and assert the
produced fgatypes.GenericFGAMessage (GenericAccessData) contains only the
project reference (no "committee_for_member_roster_access" or
"committee_for_member_email_access") and still includes ExcludeRelations:
[]string{"member"}; mirror the structure of the existing "basic_profile" /
"full_profile" cases and name it something like "hidden_profile sets no member
access" so the behavior is explicit and regression-tested.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4c5feaaa-436e-47f5-93e3-7e36365336d4

📥 Commits

Reviewing files that changed from the base of the PR and between b7c4069 and 7f0cfa0.

⛔ Files ignored due to path filters (7)
  • gen/http/committee_service/client/cli.go is excluded by !**/gen/**
  • gen/http/committee_service/client/types.go is excluded by !**/gen/**
  • gen/http/committee_service/server/types.go is excluded by !**/gen/**
  • gen/http/openapi.json is excluded by !**/gen/**
  • gen/http/openapi.yaml is excluded by !**/gen/**
  • gen/http/openapi3.json is excluded by !**/gen/**
  • gen/http/openapi3.yaml is excluded by !**/gen/**
📒 Files selected for processing (4)
  • cmd/committee-api/design/type.go
  • internal/service/committee_writer.go
  • internal/service/committee_writer_test.go
  • pkg/constants/access_control.go
✅ Files skipped from review due to trivial changes (1)
  • pkg/constants/access_control.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/service/committee_writer.go

Comment thread cmd/committee-api/design/type.go

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 34 out of 36 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/service/committee_member_writer.go
Comment thread docs/fga-contract.md Outdated
Comment thread cmd/committee-api/design/type.go Outdated
- Fix typo in member_visibility attribute description (Dertermines → Determines)
- Update fga-contract.md to document committee_for_member_email_access separately
  from committee_for_member_roster_access with correct conditions per visibility tier
- Publish delete event to sensitive index subject on member deletion so email
  documents are not left accessible to email_viewer after removal

Generated with [Claude Code](https://claude.ai/code)

Signed-off-by: Andres Tobon <andrest2455@gmail.com>
…lds to sensitive index

- Remove email from committee_member roster index (CommitteeMemberBase has no email field)
- Add missing committee_member_sensitive section documenting the email-only index
- Add fulltext, name_and_aliases, sort_name, tags, and parent_refs to sensitive IndexingConfig
- Set history_check_object/relation (committee:{uid} / auditor) on sensitive index

Generated with [Claude Code](https://claude.ai/claude-code)

Signed-off-by: Andres Tobon <andrest2455@gmail.com>
Copilot AI review requested due to automatic review settings April 16, 2026 18:40

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 34 out of 36 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/service/committee_member_writer.go
Comment thread cmd/committee-api/service/committee_service_test.go
Add explicit case for MemberVisibilityHidden and "" in buildAccessControlMessage
so the "no self-referential references" behavior is documented intent rather than
implicit switch fall-through. Pre-feature records stored as "" are treated
identically to "hidden", preventing any silent behavior change if a default
case is added in the future.

Generated with [Claude Code](https://claude.ai/claude-code)

Signed-off-by: Andres Tobon <andrest2455@gmail.com>
…s and add Version to test payloads

- committee_member_writer.go: extract sensitiveMemberTags variable and set
  both CommitteeIndexerMessage.Tags and IndexingConfig.Tags from the same slice,
  ensuring the top-level Tags field is not left empty on the sensitive index message
- committee_service_test.go: add Version: "1" to all three
  GetCommitteeMemberContactPayload test instances in TestGetCommitteeMemberContact

Generated with [Claude Code](https://claude.ai/code)

Signed-off-by: Andres Tobon <andrest2455@gmail.com>
Copilot AI review requested due to automatic review settings April 16, 2026 19:03

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 34 out of 36 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread cmd/committee-api/design/type.go
Comment on lines 425 to 439
@@ -437,26 +438,52 @@ func CommitteeMemberBaseAttributes() {
OrganizationInfoAttributes()
}

Copilot AI Apr 16, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CommitteeMemberBaseAttributes() is still used for update payloads and includes EmailAttribute() (and the update method requires email). Since the roster-view member endpoints no longer return email, clients that can update a member but don’t have email_viewer may have no way to obtain the current email to satisfy PUT “complete replacement” semantics. Consider making email optional for updates (use stored email when omitted), switching to PATCH semantics for non-email updates, or explicitly ensuring the update permission implies email_viewer so callers can fetch /contact first.

Copilot uses AI. Check for mistakes.
Re-publishes all committee members into the new two-index structure
(roster_viewer + email_viewer) introduced by the committee_member index split.

Generated with [Claude Code](https://claude.ai/code)

Signed-off-by: Andres Tobon <andrest2455@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants