Skip to content

fix(invitations): scope list, self-invite guard, open-signup gate (#3833)#3855

Open
PierreBrisorgueil wants to merge 4 commits into
masterfrom
fix/3833-invitations-scope-self-referral
Open

fix(invitations): scope list, self-invite guard, open-signup gate (#3833)#3855
PierreBrisorgueil wants to merge 4 commits into
masterfrom
fix/3833-invitations-scope-self-referral

Conversation

@PierreBrisorgueil

@PierreBrisorgueil PierreBrisorgueil commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Summary

  • What changed: (1) GET /api/invitations list() is now role-keyed in the service — admins read platform-global; non-admins read only invitations they sent (invitedBy-scoped) — so the later referral-phase ability widening can never leak invitee emails (PII). (2) create() rejects 422 "You cannot invite yourself" before the registered-email check; the same-account grant-side floor is pinned in billing unit tests (alias/multi-account self-invites are a documented accepted residual, out of scope). (3) docs: referral rewards require closed signup — on open-signup deployments a token is resolved but never claimed/finalized (P2 invariant preserved, NOT changed).
  • Why: Prevents PII leakage in multi-tenant list endpoint; hardens self-referral abuse vector; documents the open-signup gate invariant explicitly.
  • Related issues: Closes 🔒 GATE for referral phase (#5): /api/invitations must be invitedBy-scoped for non-admins (+ self-referral guard, open-signup note) #3833

Scope

  • Module(s) impacted: modules/invitations (repository, service, controller), modules/billing (referral.service unit tests)
  • Cross-module impact: none
  • Risk level: low

Validation

  • npm run lint
  • npm test
  • Manual checks done (if applicable)

Guardrails check

  • No secrets or credentials introduced (.env*, secrets/**, keys, tokens)
  • No risky rename/move of core stack paths
  • Changes remain merge-friendly for downstream projects
  • Tests added or updated when behavior changed

Notes for reviewers

  • Security considerations: The list() scope fix is the primary security surface — non-admin callers previously could enumerate all invitations platform-wide. Now scoped to invitedBy: callerId. The self-invite guard fires at 422 before any DB read.
  • Mergeability considerations: Pure additive — no schema changes, no breaking API contract changes.
  • Follow-up tasks (optional): Alias/multi-account self-referral (same email, different account) is an accepted residual documented in the PR; a hardening pass can address it in a separate issue if desired.

Summary by CodeRabbit

  • New Features

    • Implemented self-referral prevention to block users from inviting themselves.
    • Added role-based access control for invitation listings—non-admin users now see only their own invitations, while admins retain global visibility.
  • Documentation

    • Updated referral gates documentation with shipping status and constraint clarifications.
  • Tests

    • Added coverage for self-referral rejection and role-scoped invitation access.

GET /api/invitations stays admin-only by CASL, but the controller now
threads req.user and the service keys the repository filter on the
caller's role: admins read the platform-global list, anyone else only
the invitations they sent (the invitedBy index covers it). Prep so the
referral phase can widen the Invitation abilities without leaking
invitee emails platform-wide. refs #3833
create() now fails fast with 'You cannot invite yourself' when the
invitee email equals the inviter's own. E9 already caught the exact
match as a registered email, but with a misleading message; this makes
the intent explicit. Also pins the grant-side floor (#3833 belt):
expectedGrantKeys implies no referrer key when invitedBy equals
acceptedUserId (same-account pairs only — alias multi-account
self-invites remain an accepted residual risk, out of scope here).
refs #3833
Documents the open-signup invariant kept on purpose: with sign.up true
a presented token is resolved but never claimed/finalized, so
invitation.accepted never fires and referral grants are a silent
no-op. Marks gates 1-2 shipped in the README and mirrors the caveat in
the billing referral config block. refs #3833
@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@PierreBrisorgueil, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 31 minutes and 15 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more credits in the billing tab to continue.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: d8b7025b-3f7c-4129-adf5-9a61b4dfa499

📥 Commits

Reviewing files that changed from the base of the PR and between 08119cc and af141a0.

📒 Files selected for processing (1)
  • modules/invitations/services/invitations.service.js

Walkthrough

This PR implements the blocking gate (#3833) for the referral phase: it adds a self-referral guard to invitation creation, implements role-keyed list scoping to prevent non-admin PII leakage of invitee emails, and documents the shipped gating constraints and open-signup hole that remains intentionally unchanged.

Changes

Referral Gating (#3833)

Layer / File(s) Summary
Self-Invite Guard in Create
modules/invitations/services/invitations.service.js, modules/invitations/tests/invitations.service.unit.tests.js
InvitationService.create() normalizes inviter and invitee emails and rejects with a 422 error when they match, preventing self-referral claims. Tests verify email normalization and guard execution before database lookups.
Repository Filter Foundation
modules/invitations/repositories/invitations.repository.js, modules/invitations/tests/invitations.repository.unit.tests.js
InvitationRepository.list() now accepts an optional filter parameter and applies it to the Mongo query, allowing scoped listing while stripping tokens and maintaining population/sort order. Tests verify filter wiring.
Service List Scoping by Role
modules/invitations/services/invitations.service.js, modules/invitations/tests/invitations.service.unit.tests.js
InvitationService.list() implements role-keyed branching: admins receive the global list, non-admins get results filtered by invitedBy, and missing caller id returns an empty array to prevent leakage. Tests cover all branches.
Controller Integration
modules/invitations/controllers/invitations.controller.js, modules/invitations/tests/invitations.controller.unit.tests.js
InvitationsController.list() now forwards authenticated req.user to the service to enable role-keyed scoping. Tests verify user object is passed through correctly.
Documentation and Configuration
modules/invitations/README.md, modules/billing/config/billing.development.config.js, modules/billing/tests/billing.referral.service.unit.tests.js
README documents shipped role-scoped access and self-referral guard with noted residual alias risks; billing config clarifies signup constraints on referral grants; billing test verifies self-referral excludes referrer grant key.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Controller
  participant CreateService
  participant Repository
  Client->>Controller: create invitation (invitedBy, email)
  Controller->>CreateService: create(invitedBy, email)
  CreateService->>CreateService: normalize emails
  alt inviter email == invitee email
    CreateService-->>Client: 422 AppError (self-invite)
  else
    CreateService->>Repository: check user exists
    Repository-->>CreateService: result
    CreateService->>Repository: persist invitation
    Repository-->>CreateService: created
    CreateService-->>Client: success
  end
Loading
flowchart TD
  A[InvitationService.list user] --> B{User.roles includes admin?}
  B -->|Yes| C[Repository.list unscoped]
  B -->|No| D{User id exists?}
  D -->|Yes| E[Repository.list invitedBy=userId]
  D -->|No| F[Return empty array]
  C --> G[Return results]
  E --> G
  F --> H[Prevent PII leak]
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related issues

  • #3834: Overlaps with changes to InvitationService.create() self-invite rejection logic and InvitationRepository.list() token-stripping behavior, suggesting shared concerns about invitation handling edge cases and token security.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly summarizes the main changes: scoped list, self-invite guard, and open-signup gate documentation, directly referencing issue #3833.
Description check ✅ Passed The PR description covers all required template sections: summary, scope, validation, guardrails, and notes for reviewers with appropriate detail for each area.
Linked Issues check ✅ Passed All coding objectives from #3833 are met: list() is role-keyed with invitedBy scoping for non-admins [#3833], self-referral guard returns 422 [#3833], and open-signup gate is documented [#3833].
Out of Scope Changes check ✅ Passed All changes are directly aligned with #3833 requirements: invitations list scoping, self-invite guard, and referral open-signup documentation, with no unrelated modifications.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/3833-invitations-scope-self-referral

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@codecov

codecov Bot commented Jun 13, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.97%. Comparing base (755af6e) to head (af141a0).

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #3855      +/-   ##
==========================================
+ Coverage   91.96%   91.97%   +0.01%     
==========================================
  Files         160      160              
  Lines        5289     5296       +7     
  Branches     1698     1704       +6     
==========================================
+ Hits         4864     4871       +7     
  Misses        337      337              
  Partials       88       88              
Flag Coverage Δ
integration 59.81% <60.00%> (-0.03%) ⬇️
unit 72.71% <100.00%> (+0.03%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 755af6e...af141a0. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@PierreBrisorgueil PierreBrisorgueil marked this pull request as ready for review June 13, 2026 07:10
Copilot AI review requested due to automatic review settings June 13, 2026 07:10

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR hardens the invitations/referrals surface ahead of widening referral abilities by ensuring the invitations list endpoint can’t leak invitee-email PII to non-admins, adds a clearer self-invite rejection path, and documents the “open signup disables referral rewards” invariant.

Changes:

  • Scoped InvitationService.list() so admins get a global list while non-admins are restricted to { invitedBy: callerId } (and unknown callers get []).
  • Added an explicit self-invite guard in InvitationService.create() that rejects with a 422 before the registered-email lookup/persistence path.
  • Updated docs/config commentary and added/updated unit tests to pin the above behaviors (including the grant-side self-referral floor).

Reviewed changes

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

Show a summary per file
File Description
modules/invitations/tests/invitations.service.unit.tests.js Adds unit coverage for self-invite guard ordering/normalization and role-keyed list scoping.
modules/invitations/tests/invitations.repository.unit.tests.js Verifies repository list can accept and thread a { invitedBy } filter while still stripping tokens.
modules/invitations/tests/invitations.controller.unit.tests.js Ensures the controller passes req.user into the service for scoping.
modules/invitations/services/invitations.service.js Implements self-invite guard and role-keyed/scoped list behavior.
modules/invitations/repositories/invitations.repository.js Adds optional filter param to list() to support scoped queries.
modules/invitations/README.md Updates referral “gates” documentation to reflect shipped scoping + self-invite guard and clarifies open-signup gating.
modules/invitations/controllers/invitations.controller.js Passes the authenticated caller to InvitationService.list(req.user).
modules/billing/tests/billing.referral.service.unit.tests.js Pins same-account self-referral floor behavior in expectedGrantKeys.
modules/billing/config/billing.development.config.js Clarifies that referral grants require closed signup and documents shipped #3833 gates.

@PierreBrisorgueil

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

The admin role-check mirrors invitations.policy.js; any CASL ability

widening for this route must be reviewed against this seam. refs #3833
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.

🔒 GATE for referral phase (#5): /api/invitations must be invitedBy-scoped for non-admins (+ self-referral guard, open-signup note)

2 participants