feat(org-selector): foundation-level auditor sees all member orgs - #1127
feat(org-selector): foundation-level auditor sees all member orgs#1127manishdixitlfx wants to merge 5 commits into
Conversation
…XV2-2750) Signed-off-by: Manish Dixit <mdixit@linuxfoundation.org>
…ads (LFXV2-2750) Signed-off-by: Manish Dixit <mdixit@linuxfoundation.org>
… auditors (LFXV2-2750) Signed-off-by: Manish Dixit <mdixit@linuxfoundation.org>
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Pull request overview
Adds flag-gated foundation-auditor organization access to the org selector.
Changes:
- Resolves audited foundations and their active member organizations.
- Adds view-only foundation-auditor personas to API and UI state.
- Adds merge unit tests and selector E2E coverage.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
packages/shared/src/utils/index.ts |
Exports the merge utility. |
packages/shared/src/utils/foundation-auditor.utils.ts |
Merges auditor organizations into grants. |
packages/shared/src/utils/foundation-auditor.utils.spec.ts |
Tests merge behavior. |
packages/shared/src/interfaces/org-selector.interface.ts |
Adds foundation-auditor contracts. |
packages/shared/src/interfaces/access-check.interface.ts |
Adds auditor access checks. |
packages/shared/src/constants/org-selector.constants.ts |
Defines limits and concurrency settings. |
apps/lfx-one/src/server/services/org-role-grants.service.ts |
Integrates foundation organizations into grants. |
apps/lfx-one/src/server/services/foundation-auditor-orgs.service.ts |
Discovers foundations and member organizations. |
apps/lfx-one/src/app/shared/services/org-role-grants.service.ts |
Stores foundation-auditor personas. |
apps/lfx-one/src/app/shared/services/account-context.service.ts |
Enables selector access for foundation auditors. |
apps/lfx-one/src/app/shared/components/org-selector/org-selector.component.ts |
Renders the view-only persona. |
apps/lfx-one/e2e/org-selector.spec.ts |
Tests foundation-auditor row presentation. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| } | ||
| }; | ||
| const poolSize = Math.min(FOUNDATION_AUDITOR_MEMBERSHIP_FETCH_CONCURRENCY, safeFoundations.length); | ||
| await Promise.all(Array.from({ length: poolSize }, () => worker())); |
| const membershipsByIndex: ProjectMembershipDoc[][] = new Array(safeFoundations.length); | ||
| let cursor = 0; | ||
| const worker = async (): Promise<void> => { | ||
| while (cursor < safeFoundations.length) { | ||
| const index = cursor++; |
|
|
||
| /** Foundation-lens enumeration (mirrors NavigationService's foundation query), paged to completion (fail-hard), then capped. */ | ||
| private async enumerateFoundations(req: Request): Promise<AuditedFoundation[]> { | ||
| const projects = await fetchAllQueryResources<Project>( |
| // Direct/inherited grants always win — never override an existing resolved row. | ||
| if (resolved.has(entry.uid)) continue; | ||
| // Bound only the additive rows; never evict a base row to make room. | ||
| if (resolved.size >= cap) { |
…FXV2-2750) Signed-off-by: Manish Dixit <mdixit@linuxfoundation.org>
…sters (LFXV2-2750) Replaces the eager enumerate-everything design, which deep-paginated every audited foundation's roster and hung the dropdown for minutes on a broad-access caller. The lookup is now foundation-scoped and strictly bounded: enumerate foundations (a small set), batch access-check project:<uid>#auditor on the user token, then pull only the first roster page per audited foundation via a bounded-concurrency pool, capped and memoized per caller. Rows carry roleSource so the client renders them view-only without a precomputed uid set, and /api/orgs/me/role-grants reverts to grants-only. Verified against prod data: project:<uid>#auditor resolves correctly (50/50 foundations audited), M2M verify-then-elevate and token restore work, and the flag-off path is a clean no-op. Known blocker, documented in the service: the query-service index cannot list a foundation's member orgs. b2b_org is not name-searchable, and project_membership is only queryable by b2b_org_uid tag -- there is no project_uid/project_slug tag to pivot foundation -> members. The feature stays behind FOUNDATION_AUDITOR_ORG_SELECTOR_ENABLED (default off) until an upstream indexer change lands. Signed-off-by: Manish Dixit <mdixit@linuxfoundation.org>
Status update: blocked upstream (LFXV2-2752)Live-tested against prod data on 2026-07-18. The authorization half works; the data half is blocked by the query-service index. What's proven working ✅
What's blocked ❌There is no way to list a foundation's member orgs from the query service. Both directions verified empty against prod:
Filed LFXV2-2752 for the upstream capability (a Design history (why the current shape)
RecommendationKeep this draft with Known limits (documented in the service)
|
| * LFXV2-2750 note: foundation-auditor rows are resolved per-search, so foundation-level auditor | ||
| * status is not knowable up front (there is no per-user auditor index on projects). A caller whose | ||
| * ONLY access is a foundation-auditor grant — no direct org grant and no persona seed — therefore | ||
| * won't see the selector. Tracked as a follow-up. |
| public async findAuditedMemberOrgs(req: Request, username: string, searchTerm: string | undefined): Promise<FoundationAuditorOrgEntry[]> { | ||
| const term = searchTerm?.trim().toLowerCase() ?? ''; | ||
| if (term.length < FOUNDATION_AUDITOR_SEARCH_MIN_TERM_LENGTH) { | ||
| logger.debug(req, 'find_audited_member_orgs', 'Skipped — search term below minimum length', { term_length: term.length }); | ||
| return []; |
| /** Resolve (and memoize) the caller's full audited member-org pool. */ | ||
| private async resolveMemberOrgs(req: Request, username: string): Promise<FoundationAuditorOrgEntry[]> { | ||
| const memo = FoundationAuditorOrgsService.memberOrgsMemo.get(username); | ||
| if (memo && memo.expiresAt > Date.now()) { | ||
| return memo.orgs; | ||
| } | ||
|
|
||
| const foundations = await this.enumerateFoundations(req); |
| // LFXV2-2750 — foundation-auditor rows are resolved per-search and carry their role source on the row | ||
| // itself (they're absent from the cached grants sets), so the row wins when present. | ||
| const persona = item.roleSource ?? this.resolvePersona(item.uid, writerSet, auditorSet, inheritedWriterSet, inheritedAuditorSet); |
Summary
Adds a foundation-level Auditor path to the org-selector dropdown (LFXV2-2750). If the caller holds the FGA
auditorrelation on a foundation (aproject), the dropdown additionally lists all member organizations of every foundation they audit, rendered with a view-only (eye) persona — never the Edit (pen) affordance. Everyone else keeps exactly today's behavior (directb2b_org_settingsgrants + cascading children remain the catch-all).How it works
Both
/api/nav/org-items(the list) and/api/orgs/me/role-grants(the persona sets) derive from one resolver —OrgRoleGrantsService.getAccessAwareOrgs. The new augmentation folds foundation-auditor member orgs into that resolved map with afoundation-auditorrole source, so a single integration point lights up both the row and the persona.NavigationService) → batch access-checkproject:<uid>#auditor(chunked).checkAccessfails closed (all-false) on upstream error.project_membershipbyproject_slug→b2b_orgdocs), then restore infinally. Elevation is required because a project auditor does not inheritauditoronb2b_organdb2b_orghas no public viewer.mergeFoundationAuditorOrgs— direct/inherited grants always win, additive rows capped atORG_ROLE_GRANTS_HARD_CAP. Onlyactive/purchasedmemberships surface (mirrorsOrgMembershipResolverService/OrgPeopleKeyContactsService).Design decisions (agreed with requester)
Rollout / kill-switch
Gated behind env var
FOUNDATION_AUDITOR_ORG_SELECTOR_ENABLED(default off). When off, behavior is byte-for-byte today's. Turning it on is a capacity decision: on a cold 30s cache, every org-selector caller (including the ~99% who audit no foundation) pays an enumerate-all-foundations + batched access-check, since auditor status lives in FGA and can't be pre-filtered from the already-fetchedb2b_org_settings. Mitigations: 30s per-username Valkey cache, per-username in-flight coalescing, explicit hard caps, chunked batches, and the M2M member-org fetch runs only when ≥1 foundation is audited.Contract reference
project#auditoris a real, grantable FGA relation that cascades from parent — verified inlinuxfoundation/lfx-v2-helmcharts/lfx-platform/files/model.fga(define auditor: [user, team#member] or executive_director or writer or auditor from parent) andlfx-v2-project-servicedocs/fga-contract.md. This PR adds'auditor'toAccessCheckAccessType; the/access-checkendpoint resolves it against the OpenFGA model. No upstream change required.Documented trade-offs / follow-ups
apps/lfx-onehas no server-side unit-test runner (Vitest lives only inpackages/shared), soFoundationAuditorOrgsService's I/O orchestration is not unit-tested. The riskiest pure logic (mergeFoundationAuditorOrgs) has a 7-case Vitest spec, and the render path is covered by e2eS10b. Structural repo constraint./api/orgs/:orgUid/lens/*READ routes are already unenforced for everyone today (any authenticated user can read any org's lens by SFID; only writes are gated). A foundation auditor can therefore already view lens pages, and "nobody else gains anything" already holds. Per-org read-authz hardening across all lens routes is a separate ticket.hasOrgSelectorAccessnow opens the org-selector / Org-Overview surfaces to foundation-only auditors (behind the kill-switch).per_page: mirrors the established, deployed query-service read pattern used across sibling services; not changed here.Validation
yarn check-types,yarn lint,yarn format,yarn buildall pass. Newfoundation-auditor.utils.spec.tspasses (7/7). Note: rootyarn testalso runsapps/lfx-oneng test(no runner configured — pre-existing failure, not introduced here) and one pre-existing broken shared spec (meeting-privacy.utils.spec.ts, missing its vitest import) — both independent of this branch.🤖 Generated with Claude Code