Skip to content

[LFXV2-932] Restrict Committee Member Email Visibility for Non-Privileged Users#53

Open
mauriciozanettisalomao wants to merge 11 commits intolinuxfoundation:mainfrom
mauriciozanettisalomao:feat/lfxv2-932-hide-committee-member-email-less-privileged-user
Open

[LFXV2-932] Restrict Committee Member Email Visibility for Non-Privileged Users#53
mauriciozanettisalomao wants to merge 11 commits intolinuxfoundation:mainfrom
mauriciozanettisalomao:feat/lfxv2-932-hide-committee-member-email-less-privileged-user

Conversation

@mauriciozanettisalomao
Copy link
Contributor

Overview

Jira ticket https://linuxfoundation.atlassian.net/browse/LFXV2-932

Building on the member visibility implementation from LFXV2-920, we need to implement attribute-level visibility control for user profiles. Specifically, we need the ability to hide certain profile attributes (like email) from being displayed based on policy logic.

Changes

  • Split CommitteeMember type into base attributes and sensitive attributes (email)
  • Added new response type CommitteeMemberBasicWithReadonlyAttributes that excludes email
  • Updated GET /committees/{uid}/members/{member_uid} endpoint to return basic info only
  • Added sensitive data subject constant for privileged access scenarios
  • Updated OpenAPI schema to reflect new type structure

Important Note

No dedicated endpoint was created to return sensitive data (email) for writers/auditors at this time. This will be evaluated in a follow-up to determine if a separate endpoint is necessary.

Implementation Notes

Data is kept in a single KV bucket rather than splitting into separate buckets (basic/sensitive). This approach provides:

  • Simpler code with easier maintenance
  • Atomic operations without coordination complexity
  • No hard requirement to scale buckets independently

The separation exists primarily at the model layer to enable different access control policies in the Indexer (OpenSearch), where sensitive fields can be restricted based on user permissions


Tests

1. Create Committee Member

Request:

POST /committees/f01dec3e-2611-482e-bffc-b4a6d9cd0afd/members?v=1 HTTP/1.1
Content-Type: application/json
Authorization: Bearer <REDACTED>
Host: localhost:8080

{
  "committee_uid": "a36a0b0e-fad3-4ec1-be0a-42b46045a1d8",
  "committee_name": "API Test Committee 2025-12-11 - UPDATED",
  "committee_category": "Technical Steering Committee",
  "username": "mauriciozanetti",
  "email": "user@example.com",
  "job_title": "Software Engineer",
  "role": {
    "name": "Chair",
    "start_date": "2025-01-01",
    "end_date": "2025-12-31"
  },
  "appointed_by": "Community",
  "status": "Active",
  "voting": {
    "status": "Voting Rep",
    "start_date": "2025-01-01",
    "end_date": "2025-12-31"
  },
  "created_at": "2025-12-11T13:44:13-03:00",
  "updated_at": "2025-12-11T13:44:13-03:00"
}

Response:

HTTP/1.1 201 Created
Content-Type: application/json
X-Request-Id: dfbb48b1-c4d9-49b1-9519-0c5c6a5b5586
Date: Fri, 02 Jan 2026 15:59:36 GMT

{
  "uid": "88e04ac7-fe74-4e88-92a0-f3ef021c1312",
  "committee_uid": "f01dec3e-2611-482e-bffc-b4a6d9cd0afd",
  "committee_name": "Test Show Members",
  "committee_category": "Other",
  "username": "mauriciozanetti",
  "job_title": "Software Engineer",
  "role": {
    "name": "Chair",
    "start_date": "2025-01-01",
    "end_date": "2025-12-31"
  },
  "appointed_by": "Community",
  "status": "Active",
  "voting": {
    "status": "Voting Rep",
    "start_date": "2025-01-01",
    "end_date": "2025-12-31"
  },
  "email": "user@example.com",
  "created_at": "2026-01-02T12:58:55-03:00",
  "updated_at": "2026-01-02T12:58:55-03:00"
}

Note: Create/Update operations return full member data (including email) for now.


2. Verify OpenSearch Indexing

Request:

POST /resources/_search?pretty&size=10000 HTTP/1.1
Content-Type: application/json
Host: opensearch-cluster-master.lfx.svc.cluster.local:9200

{
  "size": 25,
  "track_scores": true,
  "query": {
    "bool": {
      "must": [
        {
          "term": {
            "latest": true
          }
        },
        {
          "term": {
            "object_id": "88e04ac7-fe74-4e88-92a0-f3ef021c1312"
          }
        }
      ]
    }
  },
  "sort": [
    {
      "sort_name": {
        "order": "asc"
      }
    },
    {
      "_id": "asc"
    }
  ]
}

Response:

HTTP/1.1 200 OK
content-type: application/json; charset=UTF-8

{
  "took": 48,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 2,
      "relation": "eq"
    },
    "max_score": 5.2872577,
    "hits": [
      {
        "_index": "resources",
        "_id": "committee_member_sensitive:88e04ac7-fe74-4e88-92a0-f3ef021c1312",
        "_score": 5.2872577,
        "_source": {
          "object_ref": "committee_member_sensitive:88e04ac7-fe74-4e88-92a0-f3ef021c1312",
          "object_type": "committee_member_sensitive",
          "object_id": "88e04ac7-fe74-4e88-92a0-f3ef021c1312",
          "parent_refs": [
            "committee:f01dec3e-2611-482e-bffc-b4a6d9cd0afd"
          ],
          "sort_name": "user@example.com",
          "name_and_aliases": [
            "user@example.com"
          ],
          "tags": [
            "88e04ac7-fe74-4e88-92a0-f3ef021c1312",
            "committee_member_uid:88e04ac7-fe74-4e88-92a0-f3ef021c1312",
            "committee_uid:f01dec3e-2611-482e-bffc-b4a6d9cd0afd",
            "committee_category:Other",
            "username:mauriciozanetti",
            "email:user@example.com",
            "voting_status:Voting Rep"
          ],
          "access_check_object": "committee:f01dec3e-2611-482e-bffc-b4a6d9cd0afd",
          "access_check_relation": "auditor",
          "history_check_object": "committee:f01dec3e-2611-482e-bffc-b4a6d9cd0afd",
          "history_check_relation": "writer",
          "access_check_query": "committee:f01dec3e-2611-482e-bffc-b4a6d9cd0afd#auditor",
          "history_check_query": "committee:f01dec3e-2611-482e-bffc-b4a6d9cd0afd#writer",
          "latest": true,
          "created_at": "2026-01-02T15:59:36.381471327Z",
          "updated_at": "2026-01-02T15:59:36.381471327Z",
          "data": {
            "committee_uid": "f01dec3e-2611-482e-bffc-b4a6d9cd0afd",
            "email": "user@example.com",
            "uid": "88e04ac7-fe74-4e88-92a0-f3ef021c1312"
          },
          "fulltext": "user@example.com"
        }
      },
      {
        "_index": "resources",
        "_id": "committee_member:88e04ac7-fe74-4e88-92a0-f3ef021c1312",
        "_score": 5.2872577,
        "_source": {
          "object_ref": "committee_member:88e04ac7-fe74-4e88-92a0-f3ef021c1312",
          "object_type": "committee_member",
          "object_id": "88e04ac7-fe74-4e88-92a0-f3ef021c1312",
          "parent_refs": [
            "committee:f01dec3e-2611-482e-bffc-b4a6d9cd0afd"
          ],
          "name_and_aliases": [
            "Test Show Members",
            "mauriciozanetti"
          ],
          "tags": [
            "88e04ac7-fe74-4e88-92a0-f3ef021c1312",
            "committee_member_uid:88e04ac7-fe74-4e88-92a0-f3ef021c1312",
            "committee_uid:f01dec3e-2611-482e-bffc-b4a6d9cd0afd",
            "committee_category:Other",
            "username:mauriciozanetti",
            "email:user@example.com",
            "voting_status:Voting Rep"
          ],
          "access_check_object": "committee:f01dec3e-2611-482e-bffc-b4a6d9cd0afd",
          "access_check_relation": "viewer",
          "history_check_object": "committee:f01dec3e-2611-482e-bffc-b4a6d9cd0afd",
          "history_check_relation": "writer",
          "access_check_query": "committee:f01dec3e-2611-482e-bffc-b4a6d9cd0afd#viewer",
          "history_check_query": "committee:f01dec3e-2611-482e-bffc-b4a6d9cd0afd#writer",
          "latest": true,
          "created_at": "2026-01-02T15:59:36.857261686Z",
          "updated_at": "2026-01-02T15:59:36.857261686Z",
          "data": {
            "appointed_by": "Community",
            "committee_category": "Other",
            "committee_name": "Test Show Members",
            "committee_uid": "f01dec3e-2611-482e-bffc-b4a6d9cd0afd",
            "created_at": "2026-01-02T12:58:55.467299-03:00",
            "first_name": "",
            "job_title": "Software Engineer",
            "last_name": "",
            "organization": {
              "name": ""
            },
            "role": {
              "end_date": "2025-12-31",
              "name": "Chair",
              "start_date": "2025-01-01"
            },
            "status": "Active",
            "uid": "88e04ac7-fe74-4e88-92a0-f3ef021c1312",
            "updated_at": "2026-01-02T12:58:55.467299-03:00",
            "username": "mauriciozanetti",
            "voting": {
              "end_date": "2025-12-31",
              "start_date": "2025-01-01",
              "status": "Voting Rep"
            }
          },
          "fulltext": "Test Show Members mauriciozanetti"
        }
      }
    ]
  }
}

Key Observations:

Two separate OpenSearch documents are created:

  1. committee_member_sensitive document:

    • Contains: uid, committee_uid, email
    • access_check_relation: auditor - Requires auditor/writer permission
    • Only accessible to privileged users
  2. committee_member document:

    • Contains: All basic member information (username, name, role, voting, organization, etc.)
    • Does NOT contain email
    • access_check_relation: viewer - Accessible to any committee viewer
    • Publicly accessible to users with viewer permission

3. Delete Committee Member

Request:

DELETE /committees/f01dec3e-2611-482e-bffc-b4a6d9cd0afd/members/88e04ac7-fe74-4e88-92a0-f3ef021c1312?v=1 HTTP/1.1
If-Match: 202
Content-Type: application/json
Authorization: Bearer <REDACTED>
Host: localhost:8080

Response:

HTTP/1.1 204 No Content
X-Request-Id: 889942d5-21ac-4660-8cb5-a1d4019cd412
Date: Fri, 02 Jan 2026 16:08:35 GMT

4. Verify Deletion in OpenSearch

Request:

POST /resources/_search?pretty&size=10000 HTTP/1.1
Content-Type: application/json
Host: opensearch-cluster-master.lfx.svc.cluster.local:9200

{
  "size": 25,
  "track_scores": true,
  "query": {
    "bool": {
      "must": [
        {
          "term": {
            "latest": true
          }
        },
        {
          "term": {
            "object_id": "88e04ac7-fe74-4e88-92a0-f3ef021c1312"
          }
        }
      ]
    }
  },
  "sort": [
    {
      "sort_name": {
        "order": "asc"
      }
    },
    {
      "_id": "asc"
    }
  ]
}

Response:

HTTP/1.1 200 OK
content-type: application/json; charset=UTF-8

{
  "took": 6,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 2,
      "relation": "eq"
    },
    "max_score": 5.2872577,
    "hits": [
      {
        "_index": "resources",
        "_id": "committee_member:88e04ac7-fe74-4e88-92a0-f3ef021c1312",
        "_score": 5.2872577,
        "_source": {
          "object_ref": "committee_member:88e04ac7-fe74-4e88-92a0-f3ef021c1312",
          "object_type": "committee_member",
          "object_id": "88e04ac7-fe74-4e88-92a0-f3ef021c1312",
          "latest": true,
          "deleted_at": "2026-01-02T16:08:36.351858156Z"
        }
      },
      {
        "_index": "resources",
        "_id": "committee_member_sensitive:88e04ac7-fe74-4e88-92a0-f3ef021c1312",
        "_score": 5.2872577,
        "_source": {
          "object_ref": "committee_member_sensitive:88e04ac7-fe74-4e88-92a0-f3ef021c1312",
          "object_type": "committee_member_sensitive",
          "object_id": "88e04ac7-fe74-4e88-92a0-f3ef021c1312",
          "latest": true,
          "deleted_at": "2026-01-02T16:08:36.015298141Z"
        }
      }
    ]
  }
}

Key Observations:

  • Both committee_member and committee_member_sensitive documents are marked as deleted
  • Both have deleted_at timestamp set
  • Both maintain latest: true flag for audit purposes
  • Deletion is properly synchronized across both object types

Summary

This implementation successfully separates sensitive committee member data (email) from basic information at both the API and indexing layers. The separation enables:

  1. Fine-grained access control - Email addresses are only accessible to users with auditor/writer privileges
  2. Simple data management - Single KV bucket maintains atomicity and simplicity
  3. Proper audit trail - Deletion is tracked across both object types
  4. Backward compatibility - Create/Update operations still return full data for now

- Updated OpenAPI schema to replace `CommitteeMemberFullWithReadonlyAttributes` with `CommitteeMemberBasicWithReadonlyAttributes`, removing the email field from the basic representation.
- Introduced `CommitteeMemberSensitive` schema to encapsulate sensitive information, specifically the email address.
- Modified `CommitteeMember` struct to include `CommitteeMemberSensitive` for better separation of concerns.
- Updated mock repository to reflect changes in committee member structure, ensuring sensitive information is handled appropriately.

Jira Ticket: https://linuxfoundation.atlassian.net/browse/LFXV2-932

Assisted by [Claude Code](https://claude.ai/code)

Signed-off-by: Mauricio Zanetti Salomao <mauriciozanetti86@gmail.com>
Jira Ticket: https://linuxfoundation.atlassian.net/browse/LFXV2-932

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

Signed-off-by: Mauricio Zanetti Salomao <mauriciozanetti86@gmail.com>
…ndex subject

Jira Ticket: https://linuxfoundation.atlassian.net/browse/LFXV2-932

Assisted by [Claude Code](https://claude.ai/code)

Signed-off-by: Mauricio Zanetti Salomao <mauriciozanetti86@gmail.com>
…vices

Jira Ticket: https://linuxfoundation.atlassian.net/browse/LFXV2-932

Assisted by [Claude Code](https://claude.ai/code)

Signed-off-by: Mauricio Zanetti Salomao <mauriciozanetti86@gmail.com>
Jira Ticket: https://linuxfoundation.atlassian.net/browse/LFXV2-932

Assisted by [Claude Code](https://claude.ai/code)

Signed-off-by: Mauricio Zanetti Salomao <mauriciozanetti86@gmail.com>
…ncurrency and error handling

Jira Ticket: https://linuxfoundation.atlassian.net/browse/LFXV2-932

Assisted by [Claude Code](https://claude.ai/code)

Signed-off-by: Mauricio Zanetti Salomao <mauriciozanetti86@gmail.com>
…mitteeWriterOrchestrator

Jira Ticket: https://linuxfoundation.atlassian.net/browse/LFXV2-932

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

Signed-off-by: Mauricio Zanetti Salomao <mauriciozanetti86@gmail.com>
@mauriciozanettisalomao mauriciozanettisalomao requested a review from a team as a code owner January 2, 2026 17:43
Copilot AI review requested due to automatic review settings January 2, 2026 17:43
@coderabbitai
Copy link

coderabbitai bot commented Jan 2, 2026

Caution

Review failed

An error occurred during the review process. Please try again later.

Walkthrough

Email was moved from base member attributes into a new CommitteeMemberSensitive type; API get/create now return a basic member shape. A new GET /committees/{uid}/members/{member_uid}/contact endpoint was added. Member message publishing was refactored into helper builders and a consolidated concurrent publish flow. Tests and mocks updated.

Changes

Cohort / File(s) Summary
API Design - Endpoints & Routing
cmd/committee-api/design/committee.go
Replaced full member result with basic result for get-committee-member; added new get-committee-member-contact GET endpoint with payload/result, security, errors, route /committees/{uid}/members/{member_uid}/contact.
API Design - Types
cmd/committee-api/design/type.go
Added CommitteeMemberSensitive and CommitteeMemberSensitiveAttributes; removed Email from base attributes; added CommitteeMemberBasicWithReadonlyAttributes and CommitteeMemberContactWithReadonlyAttributes; wired sensitive attrs into create/update/full types.
Service Layer - Handlers & Mappers
cmd/committee-api/service/committee_service.go, cmd/committee-api/service/committee_service_response.go
Get/create now use convertMemberDomainBasicResponse; added GetCommitteeMemberContact handler and convertMemberDomainToContactResponse mapper; email mapping moved into CommitteeMemberSensitive in payload conversions; ETag support for contact endpoint.
Domain Model - Public Types
internal/domain/model/committee_member.go
Introduced CommitteeMemberSensitive (holds Email) and embedded it into CommitteeMember; removed Email from CommitteeMemberBase.
Message Publishing - Writer Refactor
internal/service/committee_member_writer.go
Extracted indexer/event/ACL message construction into helpers (memberMessageIndexer, memberMessageEvent, memberAccessControlMessage); consolidated publishing into a single worker-pool concurrent flow; updated error propagation.
Tests - Unit & Integration
cmd/committee-api/service/*_test.go, internal/service/*_test.go, internal/domain/*_test.go, internal/infrastructure/mock/committee.go
Updated fixtures and assertions to move Email into CommitteeMemberSensitive; added/updated tests for basic/contact mappers and for new message helper behaviors and publish flow; mocks updated.
Constants & Charts
pkg/constants/subjects.go, charts/lfx-v2-committee-service/Chart.yaml, charts/.../templates/ruleset.yaml
Added IndexCommitteeMemberSensitiveSubject = "lfx.index.committee_member_sensitive"; bumped Helm chart version to 0.2.20; added ruleset entry for new contact GET route with appropriate authenticators/authorizers.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Client
  participant API as Committee API
  participant Service as Committee Service
  participant Writer as Member Writer
  participant Helpers as Message Helpers
  participant Pool as Worker Pool
  participant Indexer as Indexer
  participant EventBus as Event Bus
  participant ACL as Access Control

  Client->>API: create/update/delete member
  API->>Service: handle request (validate, persist, convert)
  Service->>Writer: publishMemberMessages(ctx, action, data)
  Writer->>Helpers: build indexer funcs, event func, acl func
  Helpers-->>Writer: publish funcs (or error)
  Writer->>Pool: submit publish funcs
  Pool->>Indexer: execute indexer publish funcs
  Pool->>EventBus: execute event publish func
  Pool->>ACL: execute acl publish func
  alt any publish error
    Pool-->>Writer: error
    Writer-->>Service: return error
    Service-->>API: return error to Client
  else all succeed
    Pool-->>Writer: success
    Writer-->>Service: success (+ ETag where applicable)
    Service-->>API: success
    Service-->>Client: success
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.77% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: restricting email visibility for non-privileged users through attribute-level visibility control, which is the core objective of LFXV2-932.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, providing detailed overview, implementation changes, testing examples, and key observations about access control behavior.
Linked Issues check ✅ Passed The PR successfully implements attribute-level visibility control (LFXV2-932) with email hidden from non-privileged users and integrates with existing visibility controls (LFXV2-920). Model split, basic response type, endpoint update, and indexer separation are all in place.
Out of Scope Changes check ✅ Passed Changes are focused and scoped to implementing attribute-level visibility: model restructuring, type additions, response mapping, test updates, and indexer support. Chart version bump is a standard operational change. No extraneous modifications detected.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

🧹 Recent nitpick comments
cmd/committee-api/service/committee_service_response.go (1)

619-723: Basic response converter includes all expected fields.

The implementation correctly includes Role, Voting, and Organization mappings (lines 656-709), addressing the earlier feedback. The converter properly omits the Email field while including all base member attributes.

Note: There is significant code duplication between this function and convertMemberDomainToFullResponse (lines 488-591). Based on prior discussion, this refactoring was acknowledged but deferred. Consider extracting shared mapping logic into helper functions in a follow-up.

♻️ Suggested approach to reduce duplication

Extract helper functions for the repeated nested structure mappings:

func (s *committeeServicesrvc) mapRoleToResponse(role model.CommitteeMemberRole) *struct {
    Name      string
    StartDate *string
    EndDate   *string
} {
    if role.Name == "" {
        return nil
    }
    r := &struct {
        Name      string
        StartDate *string
        EndDate   *string
    }{Name: role.Name}
    if role.StartDate != "" {
        r.StartDate = &role.StartDate
    }
    if role.EndDate != "" {
        r.EndDate = &role.EndDate
    }
    return r
}

// Similar helpers for Voting and Organization...

Then both converters can use these shared helpers.


📜 Recent review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 13a04fe and da1e281.

⛔ 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 (5)
  • 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
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-01-02T18:06:31.356Z
Learnt from: mauriciozanettisalomao
Repo: linuxfoundation/lfx-v2-committee-service PR: 53
File: cmd/committee-api/service/committee_service.go:254-255
Timestamp: 2026-01-02T18:06:31.356Z
Learning: In the committee member API, no dedicated endpoint was created to return sensitive data (email) for writers/auditors in PR `#53`. The GET endpoint returns only basic info while Create/Update return full data. This gap will be evaluated in a follow-up to determine if a separate endpoint is necessary.

Applied to files:

  • cmd/committee-api/service/committee_service.go
🧬 Code graph analysis (3)
cmd/committee-api/service/committee_service_response.go (3)
cmd/committee-api/design/type.go (3)
  • CommitteeMemberSensitive (391-394)
  • CommitteeMemberContactWithReadonlyAttributes (451-459)
  • CommitteeMemberBasicWithReadonlyAttributes (424-434)
internal/domain/model/committee_member.go (2)
  • CommitteeMemberSensitive (49-51)
  • CommitteeMember (20-26)
gen/committee_service/service.go (2)
  • CommitteeMemberContactWithReadonlyAttributes (229-240)
  • CommitteeMemberBasicWithReadonlyAttributes (172-226)
cmd/committee-api/service/committee_service.go (1)
gen/committee_service/service.go (2)
  • GetCommitteeMemberContactPayload (494-503)
  • GetCommitteeMemberContactResult (507-511)
cmd/committee-api/design/committee.go (2)
cmd/committee-api/design/type.go (11)
  • CommitteeMemberBasicWithReadonlyAttributes (424-434)
  • BearerTokenAttribute (329-334)
  • VersionAttribute (307-312)
  • CommitteeUIDAttribute (110-116)
  • MemberUIDAttribute (538-543)
  • CommitteeMemberContactWithReadonlyAttributes (451-459)
  • ETagAttribute (315-319)
  • BadRequestError (740-745)
  • NotFoundError (748-753)
  • InternalServerError (764-769)
  • ServiceUnavailableError (772-777)
gen/committee_service/service.go (6)
  • CommitteeMemberBasicWithReadonlyAttributes (172-226)
  • CommitteeMemberContactWithReadonlyAttributes (229-240)
  • BadRequestError (691-694)
  • NotFoundError (706-709)
  • InternalServerError (701-704)
  • ServiceUnavailableError (711-714)
🔇 Additional comments (11)
cmd/committee-api/design/type.go (3)

390-413: Well-structured separation of sensitive attributes.

The introduction of CommitteeMemberSensitive type and CommitteeMemberSensitiveAttributes() function cleanly isolates email as a sensitive field. This enables the attribute-level visibility control described in the PR objectives.


423-434: Basic member type correctly excludes sensitive data.

CommitteeMemberBasicWithReadonlyAttributes includes all the base attributes via CommitteeMemberBaseAttributes() but intentionally omits CommitteeMemberSensitiveAttributes(), ensuring email is not exposed to non-privileged users.


450-459: Contact type provides minimal exposure for auditor access.

CommitteeMemberContactWithReadonlyAttributes includes only UID, CommitteeUID, sensitive attributes (email), and timestamps - appropriate for the dedicated contact endpoint that requires auditor privileges.

charts/lfx-v2-committee-service/templates/ruleset.yaml (1)

258-283: Correct authorization for sensitive data endpoint.

The new rule uses auditor relation for the contact endpoint, appropriately restricting access to sensitive email data. This aligns with the PR's ABAC approach where basic member info uses viewer (line 248) but contact info requires elevated auditor privileges.

cmd/committee-api/service/committee_service.go (2)

254-255: Correctly restricts member GET to basic info.

The change from convertMemberDomainToFullResponse to convertMemberDomainBasicResponse properly implements the visibility restriction, ensuring email is not exposed through the standard member GET endpoint.


267-293: New contact endpoint implementation follows established patterns.

GetCommitteeMemberContact correctly:

  • Uses the same reader orchestrator as GetCommitteeMember
  • Returns only contact info via convertMemberDomainToContactResponse
  • Includes ETag handling for caching/concurrency
  • Follows consistent error handling with wrapError
cmd/committee-api/design/committee.go (2)

351-355: Result type correctly reflects basic-only response.

Changing from CommitteeMemberFullWithReadonlyAttributes to CommitteeMemberBasicWithReadonlyAttributes ensures the API contract reflects that sensitive data (email) is not returned from this endpoint.


379-420: Well-defined contact endpoint with appropriate access control documentation.

The new get-committee-member-contact method:

  • Clearly documents auditor access requirement in the description
  • Uses the dedicated CommitteeMemberContactWithReadonlyAttributes result type
  • Follows the established endpoint pattern with ETag support
  • Routes to a logical sub-resource path /contact
cmd/committee-api/service/committee_service_response.go (3)

329-331: Email correctly mapped to new sensitive structure.

The payload-to-domain conversion properly places email in CommitteeMemberSensitive, aligning with the domain model changes.


415-417: Update payload mapping consistent with create.

Same correct pattern for the update conversion path.


593-617: Clean and minimal contact response converter.

convertMemberDomainToContactResponse appropriately returns only UID, CommitteeUID, Email, and timestamps - exactly what's needed for the contact endpoint without exposing other member data.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Warning

Review ran into problems

🔥 Problems

Errors were encountered while retrieving linked issues.

Errors (1)
  • UTF-8: Request failed with status code 404

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

Copy link

Copilot AI left a comment

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 implements attribute-level visibility control for committee member profiles by splitting the CommitteeMember model to separate sensitive data (email) from basic information. The GET member endpoint now returns only basic information, while sensitive data is indexed separately in OpenSearch with restricted access (auditor/writer privileges).

Purpose: Restrict email visibility for non-privileged users while maintaining full access for create/update operations and privileged users.

Key Changes:

  • Split CommitteeMember into CommitteeMemberBase and CommitteeMemberSensitive
  • Created CommitteeMemberBasicWithReadonlyAttributes response type excluding email
  • Updated GET endpoint to return basic info only
  • Split indexer messages into two subjects with different access controls
  • Updated all tests to use the new structure

Reviewed changes

Copilot reviewed 19 out of 21 changed files in this pull request and generated no comments.

Show a summary per file
File Description
pkg/constants/subjects.go Added IndexCommitteeMemberSensitiveSubject constant for sensitive data indexing
internal/domain/model/committee_member.go Split model into CommitteeMemberBase and CommitteeMemberSensitive with embedded structure
internal/service/committee_member_writer.go Refactored message publishing to create separate indexer messages for base and sensitive data
cmd/committee-api/service/committee_service.go Updated GET endpoint to use convertMemberDomainBasicResponse instead of full response
cmd/committee-api/design/type.go Added new types for basic member info and sensitive attributes
gen/http/openapi3.yaml Added CommitteeMemberBasicWithReadonlyAttributes schema and updated GET response
Test files Updated all test data to use new CommitteeMemberSensitive structure

No issues were identified. The implementation is clean, well-tested, and follows the stated design approach of separating sensitive data at the model layer for access control in the indexer.


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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

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)

878-911: The sync parameter is unused; event publishing always runs async.

The function accepts sync bool but hardcodes false on line 910. If synchronous event publishing is expected when sync=true, this is a bug.

🔎 Proposed fix
 	return func() error {
-		return uc.committeePublisher.Event(ctx, eventMessageBuild.Subject, eventMessageBuild, false)
+		return uc.committeePublisher.Event(ctx, eventMessageBuild.Subject, eventMessageBuild, sync)
 	}, nil
🧹 Nitpick comments (2)
internal/domain/model/committee_member.go (1)

21-25: Consider clarifying or removing the UID duplication comment.

The comment on line 24 mentions "uid is duplicated here for storing sensitive info separately," but CommitteeMemberSensitive (lines 48-51) only contains the Email field without a UID. This comment may be misleading or referring to a future implementation that wasn't added yet.

🔎 Suggested fix
 // CommitteeMember represents the complete committee member business entity
 type CommitteeMember struct {
 	// CommitteeMemberBase represents the basic profile information
 	CommitteeMemberBase
-	// CommitteeMemberSensitive represents sensitive profile information with
-	// embedded sensitive information - uid is duplicated here for storing sensitive info separately
+	// CommitteeMemberSensitive represents sensitive profile information (e.g., email)
+	// that requires restricted access control
 	CommitteeMemberSensitive
 }
internal/service/committee_member_writer.go (1)

790-793: Consider computing capacity dynamically.

The capacity 4 is a magic number that assumes exactly 2 indexer messages + 1 event + 1 access control message. If memberMessageIndexer is extended to produce more messages, this will silently under-allocate.

🔎 Suggested fix
-	messages := make([]func() error, 0, 4)
+	messages := make([]func() error, 0, len(indexerMessages)+2)
 	messages = append(messages, indexerMessages...)
 	messages = append(messages, eventMessage)
 	messages = append(messages, accessControlMessage)
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between bfb883e and 788e12f.

⛔ Files ignored due to path filters (8)
  • gen/committee_service/service.go is excluded by !**/gen/**
  • 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 (13)
  • 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
  • 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
  • pkg/constants/subjects.go
🧰 Additional context used
🧬 Code graph analysis (8)
cmd/committee-api/design/committee.go (2)
cmd/committee-api/design/type.go (1)
  • CommitteeMemberBasicWithReadonlyAttributes (424-434)
gen/committee_service/service.go (1)
  • CommitteeMemberBasicWithReadonlyAttributes (170-224)
cmd/committee-api/service/committee_service_response_test.go (2)
internal/domain/model/committee_member.go (1)
  • CommitteeMemberSensitive (49-51)
cmd/committee-api/design/type.go (1)
  • CommitteeMemberSensitive (391-394)
cmd/committee-api/service/committee_service_test.go (1)
cmd/committee-api/design/type.go (1)
  • CommitteeMemberSensitive (391-394)
internal/service/committee_member_writer_test.go (6)
internal/domain/model/committee_member.go (4)
  • CommitteeMemberSensitive (49-51)
  • CommitteeMember (20-26)
  • CommitteeMemberBase (29-46)
  • CommitteeMemberOrganization (68-72)
cmd/committee-api/design/type.go (2)
  • CommitteeMemberSensitive (391-394)
  • CommitteeMemberBase (385-388)
internal/domain/model/committee_message.go (4)
  • MessageAction (19-19)
  • ActionCreated (24-24)
  • ActionUpdated (26-26)
  • ActionDeleted (28-28)
pkg/constants/subjects.go (2)
  • PutMemberCommitteeSubject (57-57)
  • RemoveMemberCommitteeSubject (61-61)
pkg/constants/http.go (2)
  • AuthorizationContextID (27-27)
  • PrincipalContextID (14-14)
internal/infrastructure/mock/committee.go (1)
  • NewMockCommitteePublisher (809-811)
internal/domain/model/committee_member_test.go (2)
internal/domain/model/committee_member.go (3)
  • CommitteeMemberSensitive (49-51)
  • CommitteeMember (20-26)
  • CommitteeMemberBase (29-46)
cmd/committee-api/design/type.go (2)
  • CommitteeMemberSensitive (391-394)
  • CommitteeMemberBase (385-388)
internal/domain/model/committee_member.go (1)
cmd/committee-api/design/type.go (2)
  • CommitteeMemberBase (385-388)
  • CommitteeMemberSensitive (391-394)
internal/infrastructure/mock/committee.go (2)
internal/domain/model/committee_member.go (1)
  • CommitteeMemberSensitive (49-51)
cmd/committee-api/design/type.go (1)
  • CommitteeMemberSensitive (391-394)
cmd/committee-api/service/committee_service_response.go (3)
internal/domain/model/committee_member.go (2)
  • CommitteeMemberSensitive (49-51)
  • CommitteeMember (20-26)
cmd/committee-api/design/type.go (3)
  • CommitteeMemberSensitive (391-394)
  • CommitteeMemberFullWithReadonlyAttributes (437-448)
  • CommitteeMemberBasicWithReadonlyAttributes (424-434)
gen/committee_service/service.go (2)
  • CommitteeMemberFullWithReadonlyAttributes (228-284)
  • CommitteeMemberBasicWithReadonlyAttributes (170-224)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: CodeQL analysis (go)
  • GitHub Check: MegaLinter
🔇 Additional comments (22)
pkg/constants/subjects.go (1)

43-45: LGTM!

The new constant for sensitive data indexing follows the established naming and documentation patterns. The value is consistent with the separation strategy outlined in the PR objectives.

cmd/committee-api/service/committee_service_test.go (1)

329-331: LGTM!

The test data structure correctly reflects the new domain model where Email is nested under CommitteeMemberSensitive. The test assertions continue to work correctly through Go's struct embedding.

internal/domain/model/committee_member_test.go (2)

52-54: LGTM!

The test fixtures have been systematically updated to use the new CommitteeMemberSensitive structure for the Email field. The changes are consistent across all test cases and maintain the same test coverage.

Also applies to: 128-130, 151-153, 172-174, 189-191, 213-215, 239-241


278-280: LGTM!

The BuildIndexKey tests have been updated to use CommitteeMemberSensitive and continue to correctly verify that the index key is computed from the committee UID and email combination. The tests validate both consistency and uniqueness.

Also applies to: 292-294, 305-307, 318-320, 358-360, 368-370, 377-379

cmd/committee-api/service/committee_service_response_test.go (4)

700-702: LGTM!

The TestConvertMemberPayloadToDomain test cases have been updated to use CommitteeMemberSensitive for the email field. The tests cover complete, minimal, and partial payload scenarios and correctly reflect the new domain model structure.

Also applies to: 719-721, 745-747, 778-780, 809-812


870-872: LGTM!

The TestConvertMemberDomainToFullResponse test cases correctly verify that the email field from the nested CommitteeMemberSensitive structure is properly mapped to the response. This aligns with the current behavior where Create/Update operations return full member data.

Also applies to: 924-926, 955-957, 996-998


1115-1117: LGTM!

The TestConvertPayloadToUpdateMember test cases have been updated to correctly place the email field in the CommitteeMemberSensitive structure. The tests cover various update scenarios including complete, minimal, and partial payloads.

Also applies to: 1136-1138, 1169-1171, 1203-1205


1243-1245: LGTM!

The LinkedIn profile test cases have been updated to use CommitteeMemberSensitive for the email field, maintaining consistency with the rest of the test suite. All scenarios (with profile, without profile, empty profile, etc.) correctly reflect the new domain model structure.

Also applies to: 1266-1268, 1289-1291, 1312-1314, 1335-1337, 1374-1376, 1402-1404, 1452-1454, 1475-1477, 1498-1500

internal/infrastructure/mock/committee.go (1)

155-157: LGTM! Mock data correctly updated to use the new CommitteeMemberSensitive structure.

The Email field is now properly initialized under CommitteeMemberSensitive for both sample members, aligning with the domain model refactor that separates sensitive attributes from base attributes.

Also applies to: 184-186

internal/service/committee_member_writer_test.go (3)

240-243: LGTM! Test fixtures correctly updated for the new structure.

All test member initializations now properly nest Email under CommitteeMemberSensitive, maintaining consistency with the domain model changes.

Also applies to: 261-264, 289-291, 333-335


1112-1283: Good test coverage for the dual-indexing message flow.

The memberMessageIndexer tests correctly validate that 2 messages are generated (one for base data, one for sensitive data), which aligns with the PR objective of creating separate OpenSearch documents for different access control policies.


1735-1857: Comprehensive integration test for message functions.

The integration test thoroughly validates all three message functions (indexer, event, access control) across create, update, and delete actions. This ensures the message publishing refactor works correctly end-to-end.

internal/domain/model/committee_member.go (1)

48-51: LGTM! Clean separation of sensitive data.

The new CommitteeMemberSensitive struct properly encapsulates the Email field, enabling attribute-level visibility control as required by the PR objectives.

cmd/committee-api/design/committee.go (1)

351-355: LGTM! API response correctly restricts sensitive data for GET requests.

The get-committee-member endpoint now returns CommitteeMemberBasicWithReadonlyAttributes which excludes the email field, implementing the attribute-level visibility control for non-privileged users. This aligns with the PR objective to hide sensitive attributes based on policy logic.

internal/service/committee_reader_test.go (1)

726-728: LGTM! Test fixtures correctly updated for the new model structure.

The test member fixtures properly initialize Email within CommitteeMemberSensitive, and the existing assertions (e.g., member.Email) continue to work correctly due to Go's embedded field promotion.

Also applies to: 833-835

cmd/committee-api/service/committee_service_response.go (1)

329-331: LGTM! Payload conversion correctly populates the new sensitive structure.

Email is now properly initialized within CommitteeMemberSensitive when converting from GOA payloads to domain models.

Also applies to: 415-417

cmd/committee-api/design/type.go (3)

390-394: LGTM! Clean separation of sensitive attributes in DSL definitions.

The new CommitteeMemberSensitive type and CommitteeMemberSensitiveAttributes() function properly encapsulate the email field, enabling clear separation between base and sensitive data in the API schema.

Also applies to: 410-413


423-434: LGTM! Basic response type correctly excludes sensitive attributes.

CommitteeMemberBasicWithReadonlyAttributes includes all base member attributes and readonly fields but intentionally omits CommitteeMemberSensitiveAttributes(), ensuring the email is not exposed to non-privileged users.


445-445: LGTM! Full response and mutation types correctly include sensitive attributes.

The CommitteeMemberFullWithReadonlyAttributes, CommitteeMemberCreateAttributes, and CommitteeMemberUpdateAttributes all include CommitteeMemberSensitiveAttributes(), ensuring email is available for create/update operations and privileged access scenarios.

Also applies to: 453-453, 459-459

internal/service/committee_member_writer.go (3)

816-842: LGTM with a minor observation.

The helper function properly handles different actions. Note that for ActionDeleted, the memberData parameter is intentionally ignored since only the UID is needed for deletion indexing. This is correct behavior, but a brief comment would clarify this intent for future maintainers.


915-936: LGTM!

The access control message helper is clean and correctly passes through the sync parameter for proper synchronous/asynchronous behavior.


855-873: Good design for sensitive data indexing.

The inline struct correctly embeds sensitive fields along with the necessary identifiers (UID, CommitteeUID) to enable proper document correlation and access control in OpenSearch.

…ganization details conditionally

Jira Ticket: https://linuxfoundation.atlassian.net/browse/LFXV2-932

Reviewed with [GitHub Copilot](https://github.com/features/copilot)

Signed-off-by: Mauricio Zanetti Salomao <mauriciozanetti86@gmail.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 788e12f and eecfd54.

📒 Files selected for processing (1)
  • cmd/committee-api/service/committee_service_response.go
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: MegaLinter
🔇 Additional comments (3)
cmd/committee-api/service/committee_service_response.go (3)

329-331: LGTM! Email properly stored in sensitive struct.

The Email field is now correctly stored in the CommitteeMemberSensitive struct, aligning with the PR's objective to separate sensitive attributes from base member data.


415-417: LGTM! Consistent Email handling in update path.

The update conversion correctly mirrors the create path by storing Email in CommitteeMemberSensitive.


497-497: No action needed. The code is correct.

The Email field is properly accessed as member.Email because CommitteeMemberSensitive is embedded as an anonymous field in CommitteeMember, which enables Go's field promotion mechanism. The Email field from the embedded struct is automatically promoted to the parent struct level, making direct access valid.

Likely an incorrect or invalid review comment.

Jira Ticket: https://linuxfoundation.atlassian.net/browse/LFXV2-932

Reviewed with [GitHub Copilot](https://github.com/features/copilot)

Signed-off-by: Mauricio Zanetti Salomao <mauriciozanetti86@gmail.com>
Jira Ticket: https://linuxfoundation.atlassian.net/browse/LFXV2-932

Signed-off-by: Mauricio Zanetti Salomao <mauriciozanetti86@gmail.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
charts/lfx-v2-committee-service/Chart.yaml (1)

8-8: Consider a minor version bump for API contract changes.

The PR introduces significant changes to the API response structure (new CommitteeMemberBasicWithReadonlyAttributes type that excludes email, dual-document indexing). A minor version bump (e.g., 0.3.0) would more clearly signal these API contract modifications compared to a patch increment.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between b970c64 and 13a04fe.

📒 Files selected for processing (1)
  • charts/lfx-v2-committee-service/Chart.yaml
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: MegaLinter

- Introduced a new GET endpoint `/committees/{uid}/members/{member_uid}/contact` to fetch contact details for a specific committee member.
- Added parameters for API version, committee UID, and member UID.
- Defined response schemas for successful and error responses, including 200, 400, 404, 500, and 503 status codes.
- Created a new schema `CommitteeMemberContactWithReadonlyAttributes` to structure the contact information response.

Jira Ticket: https://linuxfoundation.atlassian.net/browse/LFXV2-932

Assisted by [Claude Code](https://claude.ai/code)

Signed-off-by: Mauricio Zanetti Salomao <mauriciozanetti86@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