Skip to content

feat(marketing): enforce fga access on marketing bff endpoints#1018

Open
dealako wants to merge 12 commits into
mainfrom
feat/LFXV2-2235
Open

feat(marketing): enforce fga access on marketing bff endpoints#1018
dealako wants to merge 12 commits into
mainfrom
feat/LFXV2-2235

Conversation

@dealako

@dealako dealako commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

LFXV2-2235 — Enforce Marketing Ops access on Self Serve APIs (BFF)

Previously the Marketing Impact and Campaigns BFF endpoints were gated only in the Angular UI — any authenticated user could call them directly. This PR enforces the marketing_* FGA relations server-side.

Summary

  • New middlewarerequireProjectAccess(relation) factory reads foundationSlug from req.query, resolves slug → project UID via NATS (cached on req to prevent redundant lookups when stacked), posts to /access-check on LFX_V2_SERVICE, and fails closed on any error. Gated behind MARKETING_ACCESS_ENFORCEMENT=true env flag for safe rollout.
  • Route gatingmarketing_dashboard_viewer on all Marketing Impact analytics endpoints (/web-activities-summary, /email-ctr, /social-reach, /keyword-performance, /social-media, /social-media/monthly, /revenue-impact, /marketing-attribution); campaign_viewer on GET campaign routes; campaign_manager on write routes.
  • Access probeGET /projects/:slug?marketing_access=true returns marketingDashboardViewer, campaignViewer, campaignManager booleans for the UI guards in LFXV2-2236.
  • checkMultipleAccess() — new batched method on AccessCheckService for multi-relation checks on a single resource; avoids the Map-by-id collision bug in checkAccess().
  • Campaign contextCampaignService attaches foundationSlug from ProjectContextService to all campaign requests so the BFF can resolve and check the selected foundation.

Authorization model

FGA model 14.1.0 (LFXV2-1760): marketing_dashboard_viewer, campaign_viewer, campaign_manager all resolve to executive_director or marketing_ops on the project type. No ED-persona fast-path — FGA relation is the sole source of truth.

Scope

Area Ticket
BFF enforcement (this PR) LFXV2-2235
Angular route guards + nav gating LFXV2-2236
ED-view server enforcement (Health Metrics, ED dashboard) LFXV2-2483

Deploy instructions

Do not set MARKETING_ACCESS_ENFORCEMENT=true until FGA tuples are confirmed. Before enabling:

  1. Verify executive_director / marketing_ops tuples exist for all ED and marketing users across all foundations in play (not just CNCF / test users).
  2. Set MARKETING_ACCESS_ENFORCEMENT=true in the Helm values for the target environment.
  3. LFXV2-2236 (Angular guards) should land at the same time or before enabling enforcement to avoid the "nav visible → content 403" gap for root-writer pseudo-EDs.

Trade-offs documented

  • Commit 9f495289 header is 80 chars — exceeds the 72-char style target (commitlint allows ≤100, CI will pass).
  • New middleware file is in the protected-files listrequire-project-access.middleware.ts is intentional new infrastructure for this feature; reviewed in this PR.
  • Type assertion on checkMultipleAccess fallback — typed as full Record<AccessCheckAccessType, boolean> but only contains requested keys; all current consumers are safe. Deferred narrowing to follow-up.
  • Per-request FGA caching — each analytics endpoint makes its own /access-check call. Optimization deferred.
  • event-growth / brand-reach / brand-health not gated — ED-dashboard routes, tracked under LFXV2-2483.

Test plan

  • As a user without marketing_dashboard_viewer on foundation X → direct calls to /api/analytics/web-activities-summary?foundationSlug=X return 403 (with enforcement on)
  • As an ED or marketing_ops on foundation X → 200
  • As a root-writer pseudo-ED without the FGA relation → 403
  • GET /api/projects/:slug?marketing_access=true returns the three access booleans
  • Campaign write routes (/brief/generate, /brief/refine, etc.) require campaign_manager
  • With MARKETING_ACCESS_ENFORCEMENT=false (default) → all routes pass through unchanged

Related

🤖 Generated with Claude Code

dealako added 6 commits June 23, 2026 10:05
Add server-side authorization for the Marketing Impact and Campaigns
BFF endpoints (LFXV2-2235). Previously these were UI-gated only.

Changes:
- Extend AccessCheckAccessType with marketing_dashboard_viewer,
  campaign_viewer, campaign_manager
- Add Project interface fields for the marketing access probe
  (marketingDashboardViewer, campaignViewer, campaignManager)
- Add AccessCheckService.checkMultipleAccess() for batched
  multi-relation checks on a single resource (avoids Map-by-id
  collision bug in checkAccess())
- New requireProjectAccess() middleware factory: reads foundationSlug
  from req.query, resolves via NATS (cached on req to prevent N+1),
  checks FGA relation fail-closed; gated behind
  MARKETING_ACCESS_ENFORCEMENT=true env flag for safe rollout
- Mount marketing_dashboard_viewer on the 5 Marketing Impact analytics
  endpoints; campaign_viewer on GET campaign routes; campaign_manager
  on write routes
- Wire marketing access probe through project controller and service
  (?marketing_access=true opt-in flag)
- CampaignService sends foundationSlug on all campaign requests so
  the server can resolve and check the selected foundation

No ED-persona fast-path — FGA relation is sole source of truth.
Deploy flag-gated until tuples are confirmed across all foundations.
ED-view enforcement tracked in LFXV2-2483; UI guards in LFXV2-2236.

Refs: LFXV2-2235

Signed-off-by: David Deal <ddeal@linuxfoundation.org>
Replace `error: error.message` with `err: error` in warning-level
catch handlers so Pino serializes the full Error object including
stack trace and custom properties.

Refs: LFXV2-2235

Signed-off-by: David Deal <ddeal@linuxfoundation.org>
- Fix checkMultipleAccess docstring: accurately describes positional
  indexing into a Record<AccessCheckAccessType, boolean> rather than
  the incorrect "id#access keying" claim
- Add comment in requireProjectAccess explaining why pre-error WARN
  logs are intentional on each deny path (security audit events, not
  accidental double-logging)
- Document in campaigns.route.ts that campaign_manager implies
  campaign_viewer in FGA model 14.1.0 (both resolve to
  executive_director or marketing_ops), preventing a theoretical
  create-but-cannot-poll gap

Refs: LFXV2-2235

Signed-off-by: David Deal <ddeal@linuxfoundation.org>
…iant source

Co-authored-by: David Deal <ddeal@linuxfoundation.org>
Signed-off-by: David Deal <ddeal@linuxfoundation.org>
Signed-off-by: David Deal <ddeal@linuxfoundation.org>
- Add requireMarketingDashboardViewer to /revenue-impact and
  /marketing-attribution routes (missed in original pass; both back
  the Marketing Impact page per LFXV2-2235 plan)
- Guard checkMultipleAccess against upstream result-count mismatch:
  log warning and fail-closed when response.results.length differs
  from accessTypes.length
- Normalize AuthorizationError client messages to a single generic
  string to eliminate slug-validity and slug-existence enumeration
- Fix misleading PROJECT_UID_CACHE comment (cache is per-request, not
  per-page-load; helps when two requireProjectAccess instances stack
  on the same route)

Refs: LFXV2-2235

Signed-off-by: David Deal <ddeal@linuxfoundation.org>
@dealako dealako requested a review from a team as a code owner June 23, 2026 19:01
Copilot AI review requested due to automatic review settings June 23, 2026 19:01
@dealako dealako added the ai-assisted A task or activity that was supported by AI, such as CoPilot, ChatGPT, or other AI technology. label Jun 23, 2026
@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

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

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c2d60276-e26c-417d-bd47-cd0eacf669e6

📥 Commits

Reviewing files that changed from the base of the PR and between 2232faa and 70620bf.

📒 Files selected for processing (2)
  • apps/lfx-one/src/app/shared/services/campaign.service.ts
  • apps/lfx-one/src/server/services/access-check.service.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/lfx-one/src/app/shared/services/campaign.service.ts
  • apps/lfx-one/src/server/services/access-check.service.ts

Walkthrough

The PR adds FGA-based access control for campaign and analytics server routes via a new requireProjectAccess middleware factory that gates requests by foundationSlug and relation. It expands shared access-type and project interfaces with marketing fields, enriches project service lookups with an optional marketing access check, and scopes all frontend campaign API calls by the selected foundation.

Changes

Marketing Access Control — FGA gating for campaign and analytics routes

Layer / File(s) Summary
Shared access type and project interface contracts
packages/shared/src/interfaces/access-check.interface.ts, packages/shared/src/interfaces/project.interface.ts
AccessCheckAccessType is expanded with marketing_dashboard_viewer, campaign_viewer, and campaign_manager literals; the Project interface gains three optional boolean marketing access probe fields with the same true/false/undefined semantics as meetingCoordinator.
AccessCheckService.checkMultipleAccess
apps/lfx-one/src/server/services/access-check.service.ts
New public method calls /access-check for multiple access types on a single resource, validates the result count, parses each result string into a Record<AccessCheckAccessType, boolean>, and logs success metrics including a granted count.
ProjectService marketing access enrichment
apps/lfx-one/src/server/services/project.service.ts
getProjectById and getProjectBySlug accept a new includeMarketingAccess parameter; when opted in, a separate FGA check populates marketingDashboardViewer, campaignViewer, and campaignManager on the returned project, with failures logging and omitting fields rather than throwing.
Project controller reads marketing_access query flag
apps/lfx-one/src/server/controllers/project.controller.ts
Reads req.query['marketing_access'] === 'true' and forwards the resulting includeMarketingAccess boolean to both getProjectById and getProjectBySlug lookup paths.
requireProjectAccess middleware factory
apps/lfx-one/src/server/middleware/require-project-access.middleware.ts
New exported factory requireProjectAccess(relation) validates foundationSlug, resolves it to a project UID (with per-request caching via Symbol), and calls checkSingleAccess for the given FGA relation; all failure paths fail closed with AuthorizationError. Enforcement is gated by MARKETING_ACCESS_ENFORCEMENT=true.
Analytics routes gated with marketing dashboard middleware
apps/lfx-one/src/server/routes/analytics.route.ts
Marketing dashboard routes (web-activities-summary, email-ctr, social-reach, keyword-performance, social-media, social-media/monthly) and executive director routes (revenue-impact, marketing-attribution) are wrapped with requireMarketingDashboardViewer middleware. Previously all these routes had no middleware-based access enforcement.
Campaign routes gated with FGA middleware
apps/lfx-one/src/server/routes/campaigns.route.ts
Campaign read routes (job status, HubSpot lookup, monitoring, keywords, audience) are wrapped with requireCampaignViewer; write routes (brief generation, campaign creation, HubSpot UTM creation, keyword actions) are wrapped with requireCampaignManager. Previously routes had no middleware-based access enforcement.
Frontend CampaignService foundation scoping
apps/lfx-one/src/app/shared/services/campaign.service.ts
CampaignService now injects ProjectContextService, derives foundationSlug, and conditionally passes it as a query parameter or POST param to all campaign API calls including brief generation, campaign creation, monitoring, lookups, and job polling.
Dashboard documentation
docs/user/dashboards/index.md
Introductory description is reflow across two lines without wording change.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant AnalyticsRoute
  participant requireMarketingDashboardViewer
  participant AccessCheckService
  participant Backend

  Client->>AnalyticsRoute: GET /web-activities-summary?foundationSlug=acme
  AnalyticsRoute->>requireMarketingDashboardViewer: Check access
  requireMarketingDashboardViewer->>AccessCheckService: checkSingleAccess(resource: project, relation: marketing_dashboard_viewer)
  AccessCheckService->>Backend: POST /access-check
  Backend-->>AccessCheckService: {granted: true}
  AccessCheckService-->>requireMarketingDashboardViewer: Access granted
  requireMarketingDashboardViewer->>AnalyticsRoute: next()
  AnalyticsRoute->>Backend: Fetch analytics data
  Backend-->>AnalyticsRoute: Data
  AnalyticsRoute-->>Client: 200 with analytics
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • linuxfoundation/lfx-self-serve#641: Backend adds FGA marketing_dashboard_viewer middleware to /revenue-impact analytics endpoint, which the frontend's Marketing Impact component consumes through analyticsService.getRevenueImpact(slug).
  • linuxfoundation/lfx-self-serve#839: Both PRs modify apps/lfx-one/src/app/shared/services/campaign.service.ts, specifically the same campaign service methods by conditionally scoping requests with foundationSlug.
  • linuxfoundation/lfx-self-serve#840: Main PR scopes campaign brief/creation, HubSpot UTM, and job-status requests with foundationSlug, which directly overlaps with this PR's campaign-proxy backend implementations for the same workflow pieces.

Suggested labels

deploy-preview

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main objective of the PR—enforcing FGA access control on marketing BFF endpoints.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, providing clear context about the FGA enforcement, architectural decisions, deployment instructions, and test plan.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/LFXV2-2235

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration.


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

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 enforces Marketing-related FGA relations server-side in the Self Serve BFF (analytics “Marketing Impact” endpoints and campaign endpoints), closing a gap where access was previously gated only in the Angular UI. It also adds an opt-in project “access probe” surface for the UI to query Marketing permissions.

Changes:

  • Added requireProjectAccess(relation) middleware (flag-gated by MARKETING_ACCESS_ENFORCEMENT) and applied it to marketing analytics routes and campaign routes.
  • Extended shared access-check types and project response shape to support marketing_* / campaign_* access probes (?marketing_access=true).
  • Updated the Angular CampaignService to include foundationSlug on campaign requests so the server can resolve the target foundation and enforce FGA.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
packages/shared/src/interfaces/project.interface.ts Adds response-only marketing access probe booleans on Project.
packages/shared/src/interfaces/access-check.interface.ts Extends AccessCheckAccessType union with marketing/campaign relations.
apps/lfx-one/src/server/services/project.service.ts Adds optional marketing access probe behavior when fetching a project.
apps/lfx-one/src/server/services/access-check.service.ts Adds checkMultipleAccess() for checking multiple relations for one resource.
apps/lfx-one/src/server/routes/campaigns.route.ts Applies project-scoped FGA middleware to campaign read/write routes.
apps/lfx-one/src/server/routes/analytics.route.ts Applies marketing_dashboard_viewer middleware to marketing analytics endpoints.
apps/lfx-one/src/server/middleware/require-project-access.middleware.ts Introduces new flag-gated middleware enforcing project access via slug→UID→FGA.
apps/lfx-one/src/server/controllers/project.controller.ts Wires ?marketing_access=true through to ProjectService.
apps/lfx-one/src/app/shared/services/campaign.service.ts Attaches foundationSlug query param to campaign requests from selected foundation context.

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

Comment thread apps/lfx-one/src/server/services/access-check.service.ts
Comment thread apps/lfx-one/src/server/services/access-check.service.ts
Comment thread apps/lfx-one/src/server/middleware/require-project-access.middleware.ts Outdated
Comment thread apps/lfx-one/src/app/shared/services/campaign.service.ts

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/lfx-one/src/server/services/project.service.ts`:
- Around line 346-362: The issue is that checkMultipleAccess() may return
all-false fallback values on internal failure, which would then be assigned to
the marketing fields even though the check didn't succeed cleanly. This
contradicts the Project contract where failed checks should remain undefined. In
the getProjectById method where checkMultipleAccess is called, instead of
relying on the catch block to handle errors, you need to either use a strict
access check method that throws on failure (ensuring fields stay undefined on
any failure), or add validation logic to verify that marketingResults represents
a clean successful check before assigning the marketingDashboardViewer,
campaignViewer, and campaignManager properties, ensuring transient FGA failures
do not get serialized as definitive false values.
🪄 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: 9ea9edb4-b82a-4088-a8d7-07991069a917

📥 Commits

Reviewing files that changed from the base of the PR and between 5425601 and db7e0b9.

📒 Files selected for processing (9)
  • apps/lfx-one/src/app/shared/services/campaign.service.ts
  • apps/lfx-one/src/server/controllers/project.controller.ts
  • apps/lfx-one/src/server/middleware/require-project-access.middleware.ts
  • apps/lfx-one/src/server/routes/analytics.route.ts
  • apps/lfx-one/src/server/routes/campaigns.route.ts
  • apps/lfx-one/src/server/services/access-check.service.ts
  • apps/lfx-one/src/server/services/project.service.ts
  • packages/shared/src/interfaces/access-check.interface.ts
  • packages/shared/src/interfaces/project.interface.ts

Comment thread apps/lfx-one/src/server/services/project.service.ts
Address review comments from copilot-pull-request-reviewer and coderabbitai:

- access-check.service.ts: checkMultipleAccess now throws on upstream
  errors and result-count mismatches instead of returning all-false
  fallback; probe callers (project.service.ts) already wrap in .catch()
  and return undefined, so fields are omitted rather than set to false
  on transient FGA failures (per copilot-pull-request-reviewer,
  coderabbitai)
- require-project-access.middleware.ts: rephrase FGA deny log from
  "denied" to "did not grant access (denied or upstream failure)" since
  checkSingleAccess is fail-closed and the log fires for both denial and
  upstream error (per copilot-pull-request-reviewer)
- campaign.service.ts: capture this.foundationSlug once per method
  (getMonitorData, getLinkedInAccounts, getLinkedInMonitorData,
  getRedditAccounts, getRedditMonitorData, getMetaAccounts,
  getMetaMonitorData, getKeywords, getAudience) to avoid double signal
  evaluation (per copilot-pull-request-reviewer)
- docs/user/dashboards/index.md: fix Prettier prose-wrap formatting
  (line 12 exceeded printWidth:160 in CI environment)

Resolves 5 review threads.

Refs: LFXV2-2235

Signed-off-by: David Deal <ddeal@linuxfoundation.org>
@dealako

dealako commented Jun 23, 2026

Copy link
Copy Markdown
Contributor Author

Review Feedback Addressed

Commit: $SHA

Changes Made

  • access-check.service.ts: checkMultipleAccess now throws on upstream errors and result-count mismatches instead of returning all-false fallback. project.service.ts already wraps in .catch(() => undefined), so probe fields are omitted on transient FGA failures rather than set to false (per copilot-pull-request-reviewer, coderabbitai)
  • require-project-access.middleware.ts: Rephrased FGA deny log from "denied" to "did not grant access (denied or upstream failure)" — checkSingleAccess is fail-closed and the log fires for both (per copilot-pull-request-reviewer)
  • campaign.service.ts: Captured this.foundationSlug once per method in all 9 affected GET/read methods to avoid double signal evaluation (per copilot-pull-request-reviewer)
  • docs/user/dashboards/index.md: Fixed Prettier prose-wrap formatting (line exceeded printWidth:160 in CI environment)

Threads Resolved

5 of 5 unresolved threads addressed in this iteration.

Collapse multi-line throw to single line; fits within printWidth:160.

Refs: LFXV2-2235

Signed-off-by: David Deal <ddeal@linuxfoundation.org>
Copilot AI review requested due to automatic review settings June 23, 2026 19:20

@coderabbitai coderabbitai Bot 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.

Caution

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

⚠️ Outside diff range comments (1)
apps/lfx-one/src/server/services/access-check.service.ts (1)

129-132: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Update the JSDoc fail-closed contract to match runtime behavior.

Line 131 says this method returns all-false on upstream errors, but Lines 171-174 and Lines 200-205 now throw. Please align the doc so callers know they must handle exceptions and deny access explicitly.

Suggested doc fix
- * Fails-closed: returns all-false on upstream error.
+ * Fails-closed: throws on upstream error; callers should deny access when this method errors.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/lfx-one/src/server/services/access-check.service.ts` around lines 129 -
132, The JSDoc comment starting around line 129 incorrectly documents that this
method fails-closed by returning all-false on upstream errors, but the actual
implementation at lines 171-174 and 200-205 throws exceptions instead. Update
the JSDoc comment to remove or correct the fail-closed statement and add
documentation that clearly indicates the method throws exceptions on upstream
errors rather than returning all-false values, so callers understand they must
handle exceptions and implement their own access denial logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@apps/lfx-one/src/server/services/access-check.service.ts`:
- Around line 129-132: The JSDoc comment starting around line 129 incorrectly
documents that this method fails-closed by returning all-false on upstream
errors, but the actual implementation at lines 171-174 and 200-205 throws
exceptions instead. Update the JSDoc comment to remove or correct the
fail-closed statement and add documentation that clearly indicates the method
throws exceptions on upstream errors rather than returning all-false values, so
callers understand they must handle exceptions and implement their own access
denial logic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f9e4f919-bb38-4765-945e-9a03ab6e8078

📥 Commits

Reviewing files that changed from the base of the PR and between db7e0b9 and 9b0fe7f.

📒 Files selected for processing (4)
  • apps/lfx-one/src/app/shared/services/campaign.service.ts
  • apps/lfx-one/src/server/middleware/require-project-access.middleware.ts
  • apps/lfx-one/src/server/services/access-check.service.ts
  • docs/user/dashboards/index.md
✅ Files skipped from review due to trivial changes (1)
  • docs/user/dashboards/index.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/lfx-one/src/server/middleware/require-project-access.middleware.ts
  • apps/lfx-one/src/app/shared/services/campaign.service.ts

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 10 out of 10 changed files in this pull request and generated 3 comments.

Comment thread apps/lfx-one/src/server/services/access-check.service.ts Outdated
Comment thread apps/lfx-one/src/server/routes/campaigns.route.ts
@dealako dealako self-assigned this Jun 23, 2026
Address review comments from copilot-pull-request-reviewer:

- access-check.service.ts: update checkMultipleAccess JSDoc — method now
  throws on upstream error / result-count mismatch, not returns all-false;
  clarify callers must catch and decide how to fail
  (per copilot-pull-request-reviewer)
- require-project-access.middleware.ts: clarify factory JSDoc — slug→UID
  cache is per-request (stacked middleware), not shared across parallel
  requests; remove misleading "parallel analytics calls" language
  (per copilot-pull-request-reviewer)

Resolves 2 review threads. Third thread (job store IDOR) documented as
pre-existing limitation, addressed in thread response.

Signed-off-by: David Deal <ddeal@linuxfoundation.org>
@dealako

dealako commented Jun 23, 2026

Copy link
Copy Markdown
Contributor Author

Review Feedback Addressed

Commit: bb287b8

Changes Made

  • access-check.service.ts: Updated checkMultipleAccess JSDoc — method now throws on upstream error / result-count mismatch; removed stale "returns all-false" language (per copilot-pull-request-reviewer)
  • require-project-access.middleware.ts: Clarified factory JSDoc — slug→UID cache is per-request (stacked middleware only), not shared across parallel requests; removed misleading "parallel analytics calls" text (per copilot-pull-request-reviewer)

Deferred / Still Open

  • campaigns.route.ts — job store IDOR: The jobs Map in campaign-proxy.service.ts is unscoped by foundation/user and predates this PR. This PR made the endpoint stricter (was any-authenticated-user, now requires FGA campaign_viewer). Proper fix (bind jobId to foundationSlug/userId at creation and verify in getJobStatus) is a follow-up. Tracking as a known limitation — thread left open for discussion.

Threads Resolved

2 of 3 unresolved threads addressed in this iteration.

Copilot AI review requested due to automatic review settings June 23, 2026 20:08

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 10 out of 10 changed files in this pull request and generated 2 comments.

Comment thread apps/lfx-one/src/server/services/access-check.service.ts Outdated
Comment thread apps/lfx-one/src/app/shared/services/campaign.service.ts
…nsistency

Address review comments from copilot-pull-request-reviewer:

- access-check.service.ts: make checkMultipleAccess generic over T extends
  AccessCheckAccessType; return type is now Record<T, boolean> instead of
  Record<AccessCheckAccessType, boolean>, removing the unsound cast that
  implied every access type key was present regardless of what was requested
  (per copilot-pull-request-reviewer)
- campaign.service.ts: capture this.foundationSlug once in lookupHubSpotUtm
  before building params, consistent with every other method in the service
  (per copilot-pull-request-reviewer)

Resolves 2 review threads.

Signed-off-by: David Deal <ddeal@linuxfoundation.org>
@dealako

dealako commented Jun 23, 2026

Copy link
Copy Markdown
Contributor Author

Review Feedback Addressed (iteration 2)

Commit: 70620bf

Changes Made

  • access-check.service.ts: Made checkMultipleAccess generic over T extends AccessCheckAccessType; return type is now Record<T, boolean> — reflects exactly the requested keys, removes the unsafe as Record<AccessCheckAccessType, boolean> cast on the fallback (per copilot-pull-request-reviewer)
  • campaign.service.ts: lookupHubSpotUtm now captures this.foundationSlug once before building params, consistent with all other methods in the service (per copilot-pull-request-reviewer)

Threads Resolved

2 of 2 new unresolved threads addressed in this iteration.

Still Open (from prior iteration)

  • campaigns.route.ts — job store IDOR: Pre-existing design limitation, tracked as a follow-up. See earlier thread response for full context.

@MRashad26 MRashad26 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.

Review — PR #1018 feat/LFXV2-2235

Verdict: PASS

Overview

Enforces FGA access server-side on Marketing Impact and Campaign BFF endpoints via a new requireProjectAccess(relation) middleware. Adds checkMultipleAccess() for multi-relation checks on a single resource. Frontend attaches foundationSlug from ProjectContextService on all marketing/campaign requests. Flag-gated behind MARKETING_ACCESS_ENFORCEMENT=true for safe rollout.

Secrets check

Clean — no credentials or API keys.

Code standards

  • ✅ New interfaces in packages/shared/src/interfaces/
  • ✅ No HTML template changes — backend-only PR
  • ✅ No constants in component files

FGA correctness — all clear

Access-check call shapecheckSingleAccess() and checkMultipleAccess() both POST to /access-check with the correct resourceType:id#relation shape, consistent with how AccessCheckService.checkAccess() is used elsewhere in the codebase ✅

Slug → UID resolution — Middleware validates foundationSlug against SLUG_PATTERN = /^[a-z0-9-]+$/ before resolution, type-checks the value, and validates the NATS result. Frontend sources from ProjectContextService.selectedFoundation().slug (canonical state) ✅

Fail-closed behavior — Every error path (invalid slug, slug not found, NATS failure, FGA denied, unexpected exception) calls next(AuthorizationError) → 403. Enforcement is opt-in (MARKETING_ACCESS_ENFORCEMENT=false by default), so no accidental lockout on rollout ✅

Per-request cachingreq[PROJECT_UID_CACHE] uses a Symbol key scoped to each request object — no cross-request contamination or stale data ✅

checkMultipleAccess() collision fix — Old checkAccess() keyed results by resource ID, causing collisions when the same ID appeared with multiple access types. New method takes a single ID + accessTypes[] and returns Record<T, boolean> keyed by access type — no collision possible ✅

Route ordering — FGA middleware applied after auth middleware (existing pattern), so req.user is set before FGA checks run ✅

Access probe endpointGET /projects/:slug?marketing_access=true calls checkMultipleAccess(['marketing_dashboard_viewer', 'campaign_viewer', 'campaign_manager']) and spreads the result booleans onto the response correctly ✅

IDOR — Any user can probe any slug, but the FGA relation check is against the authenticated user server-side — not exploitable ✅

FGA efficiency note 🔵

The PR body acknowledges per-endpoint FGA calls ("optimization deferred"). This is safe and correct — the per-request Symbol cache prevents redundant slug resolutions within a single request, but each endpoint does make its own /access-check POST. Acceptable for now; a batched check or middleware-level result caching would reduce latency if multiple FGA-gated routes are hit in the same user session.

@luismoriguerra

luismoriguerra commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Audit: PR #1018 — feat(marketing): enforce FGA access on marketing BFF endpoints

Audited head: 70620bf6 · Ticket: LFXV2-2235

Scope: Server-side FGA enforcement on Marketing Impact analytics + Campaign BFF routes; marketing access probe on GET /api/projects/:slug?marketing_access=true.

Lanes: CI gates · manual secrets pass · architecture review · code-standards review · prior bot/human threads on head.


Verdict: ✅ Approved

Merge readiness: Safe to merge as-is. Do not set MARKETING_ACCESS_ENFORCEMENT=true in prod until FGA tuples are verified and LFXV2-2236 UI guards land (or accept temporary 403 for marketing_ops users who can API-call but not navigate).


1. Why this change

Marketing Impact and Campaigns were UI-gated only — any authenticated user could call BFF endpoints directly. This PR closes that gap with server-side FGA on the BFF, dark-deployed behind an env flag.

2. How it was done

Layer Mechanism
Middleware requireProjectAccess(relation) — validates foundationSlug, resolves slug→UID (per-request cache), /access-check via checkSingleAccess, fail-closed → AuthorizationError
Rollout MARKETING_ACCESS_ENFORCEMENT=true (off by default)
Analytics 8 endpoints gated with marketing_dashboard_viewer (incl. /revenue-impact, /marketing-attribution)
Campaigns Reads → campaign_viewer; writes → campaign_manager
Probe ?marketing_access=true → batched checkMultipleAccess for LFXV2-2236 guards
Client CampaignService threads foundationSlug from ProjectContextService

No ED-persona fast-path on the server — FGA is the gate when enforcement is on.

3. Is it correct

Yes. GitHub CI on head passes (Code Quality, CodeQL, lint/analyze, license, DCO). Prior review threads (Copilot / CodeRabbit) addressed on 70620bf6: checkMultipleAccess throws on upstream/count mismatch; probe fields omit on failure; generic 403 messages; audit-log wording; slug capture consistency; generic return type.

@MRashad26 approved at 70620bf6.

4. Gaps (non-blocking)

  1. Ops rollout — flag read in code but not wired in Helm/ArgoCD in this PR; follow PR deploy checklist (tuples → flag → LFXV2-2236).
  2. Pre-existing job IDOR — in-memory jobs Map unscoped by foundation/user; this PR tightens access (campaign_viewer required); proper scoping is a follow-up (already discussed inline).
  3. LFXV2-2483 carve-outevent-growth / brand-reach / brand-health remain auth-only (intentional); revenue/attribution are gated.
  4. Client/server split — Angular routes still use executiveDirectorGuard until LFXV2-2236.
  5. Convention nits — dedupe SLUG_PATTERN to shared constants; trim LFXV2-* ticket refs from shipped comments; consider client fail-fast when foundationSlug is unset before flag flip.

5. Q&A

  • Why checkMultipleAccess? checkAccess keys by resource ID — multiple relations on the same project UID collide.
  • Transient FGA failure in probe? Throws → caller omits fields (undefined), preserving tri-state semantics on Project.
  • Missing foundationSlug when enforcement on? Middleware denies with generic 403 — fail-closed by design.

6. Business rules & ADRs

  • Marketing Impact endpoints require project:{uid}#marketing_dashboard_viewer when enforcement is on.
  • Campaign reads/writes split on campaign_viewer / campaign_manager.
  • Complies with ADR-0015 / proposed ADR-0031 relation model; BFF middleware is established Self Serve pattern (soft PERMISSIONS.md gap per ADR-0018).
  • Recommend: promote ADR-0031, log BFF third auth plane + LFXV2-2483 partial gating.

7. Open questions (confirm)

  • Is BFF middleware + /access-check the long-term enforcement plane for Self Serve Snowflake routes?
  • Will MARKETING_ACCESS_ENFORCEMENT Helm wiring land before prod enablement?
  • Is the LFXV2-2483 API window (ED overview metrics open to any authenticated caller with valid slug) acceptable until that ticket ships?

Cursor audit — comment-only review.

@luismoriguerra luismoriguerra 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.

Audit inline — one ops hardening note not covered in existing threads. Full report in top-level comment.

body: request,
});
const slug = this.foundationSlug;
const url = slug ? `/api/campaigns/brief/generate?foundationSlug=${encodeURIComponent(slug)}` : '/api/campaigns/brief/generate';

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.

Audit (non-blocking, pre-flag-flip): When selectedFoundation() is null, campaign requests proceed without foundationSlug. That is fine while MARKETING_ACCESS_ENFORCEMENT is off, but once enforcement is enabled every call here will 403. Before flipping the flag, consider failing fast client-side (or blocking calls until foundation context is set) so users get a clear UX rather than opaque 403s. Pair with LFXV2-2236 guards.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-assisted A task or activity that was supported by AI, such as CoPilot, ChatGPT, or other AI technology.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants