Skip to content

feat(github) - Authenticate By Organization Membership#664

Open
scottwater wants to merge 9 commits into
ColeMurray:mainfrom
scottwater:github-organizations
Open

feat(github) - Authenticate By Organization Membership#664
scottwater wants to merge 9 commits into
ColeMurray:mainfrom
scottwater:github-organizations

Conversation

@scottwater
Copy link
Copy Markdown

@scottwater scottwater commented May 21, 2026

This PR adds GitHub organization membership as a sign-in allowlist option.

Previously, access could be restricted by explicit GitHub usernames or email domains. This adds a third option: ALLOWED_GITHUB_ORGS, which allows any user with an active membership in one of the configured GitHub organizations to sign in.

This should be less tedious than managing a list of usernames or relying on email domains.

How It Works

The web app now reads a new comma-separated allowlist:

ALLOWED_GITHUB_ORGS=your-org

During GitHub sign-in, access is granted if any configured policy matches:

  • the GitHub username is in ALLOWED_USERS
  • the email domain is in ALLOWED_EMAIL_DOMAINS
  • the user has active membership in one of ALLOWED_GITHUB_ORGS
  • all allowlists are empty and UNSAFE_ALLOW_ALL_USERS=true

The organization check calls GitHub's authenticated membership endpoint:

GET /user/memberships/orgs/{org}

Only state: "active" is accepted. Pending membership, missing membership, denied responses, and API failures all fail closed.

Org names are normalized through the same allowlist parser as users/domains, so config is case-insensitive. We should still use the canonical lowercase GitHub org slug in Terraform.

GitHub App Setup

Because the app now checks authenticated organization membership, the GitHub OAuth request includes the read:org scope.

The GitHub App must also be updated with:

  • Organization permissions: Members: Read-only

After changing the app permissions, the org installation may will need to approve the updated permission request. Users should sign out and sign back in so GitHub grants the new OAuth scope.

Terraform / Deployment

Terraform now supports:

allowed_github_orgs = "your-org"

For an org-only live test, configure:

allowed_users         = ""
allowed_email_domains = ""
allowed_github_orgs   = "your-org"

Keeping allowed_users or allowed_email_domains populated will still allow matching users through those policies because access control is OR-based.

The new env var is wired through both supported web deployment paths:

  • Vercel env vars
  • Cloudflare/OpenNext generated Wrangler config

Terraform's access-control gate now treats allowed_github_orgs as a valid allowlist, so production deploys can use org membership without setting usernames or email domains.

Follow-Up Work

This PR only checks org membership during sign-in.

If a user is removed from the GitHub organization after they already have an active NextAuth session, they will keep access until that session expires. Periodic session/JWT revalidation should be handled in a follow-up PR.

Summary by CodeRabbit

  • New Features

    • Added GitHub organization-based sign-in allowlist via ALLOWED_GITHUB_ORGS.
  • Documentation

    • Expanded setup, deployment, and README docs to document org allowlist, required GitHub App permissions, and updated env/terraform guidance.
  • Tests

    • Added and expanded tests covering org membership checks, OAuth scope behavior, and sign-in decision paths.
  • Chores

    • Updated environment templates, CI/terraform/workflow inputs, and deployment variable wiring to surface the new allowlist.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 21, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 862e083e-257e-4c4c-8665-bc64cb6ac1ff

📥 Commits

Reviewing files that changed from the base of the PR and between 7b81461 and 8f01c4a.

📒 Files selected for processing (10)
  • .github/workflows/terraform.yml
  • docs/GETTING_STARTED.md
  • docs/SETUP_GUIDE.md
  • packages/web/.env.example
  • packages/web/README.md
  • packages/web/src/lib/access-control.test.ts
  • packages/web/src/lib/access-control.ts
  • packages/web/src/lib/auth.test.ts
  • packages/web/src/lib/auth.ts
  • terraform/environments/production/terraform.tfvars.example
✅ Files skipped from review due to trivial changes (2)
  • packages/web/README.md
  • docs/SETUP_GUIDE.md

📝 Walkthrough

Walkthrough

Adds optional GitHub organization membership checks for access control. Users can now restrict sign-in to members of specified GitHub organizations via ALLOWED_GITHUB_ORGS. The async check queries GitHub's membership API during sign-in and is deployed through Terraform to both Vercel and Cloudflare.

Changes

GitHub Organization-based Access Control

Layer / File(s) Summary
Organization access-control interfaces
packages/web/src/lib/access-control.ts
AccessControlConfig and AccessCheckParams gain optional allowedOrganizations and activeOrganizations; new exported types/unions and GitHub access params added.
Static access evaluation
packages/web/src/lib/access-control.ts
checkAccessAllowed refactored to getAccessAllowReason usage; empty-allowlist gate updated to include org allowlist and org matching is case-insensitive.
Organization membership checking
packages/web/src/lib/access-control.ts
New checkGitHubOrganizationAccess queries GitHub memberships API with fetch injection, per-request timeout, and structured availability/decision results.
Access-control tests
packages/web/src/lib/access-control.test.ts
Adds tests for allowedOrganizations, OR-logic combos, unsafeAllowAllUsers regression, and comprehensive async membership tests (membership states, API errors, timeouts, URL encoding).
Authentication integration
packages/web/src/lib/auth.ts
Adds BASE_GITHUB_OAUTH_SCOPE/buildGitHubOAuthScope, requests read:org conditionally, makes signIn async with two-stage policy (static then optional org check), logs decisions, and normalizes org decision reasons.
Auth tests
packages/web/src/lib/auth.test.ts
New tests covering conditional OAuth scope and signIn paths: token-missing, 404/pending, rate-limit/server/network/malformed JSON, and unsafe-allow regressions.
Terraform variables and checks
terraform/environments/production/variables.tf, terraform/environments/production/checks.tf
Adds allowed_github_orgs variable, broadens allowlist descriptions, updates unsafe_allow_all_users wording to reference all allowlists, and extends the access-control precondition to accept the org allowlist.
Terraform deployment wiring
terraform/environments/production/terraform.tfvars.example, terraform/environments/production/web-vercel.tf, terraform/environments/production/web-cloudflare.tf, .github/workflows/terraform.yml
Wires ALLOWED_GITHUB_ORGS into example tfvars, Vercel and Cloudflare envs, and injects TF_VAR_allowed_github_orgs from CI secrets for plan/apply.
Documentation and templates
README.md, docs/GETTING_STARTED.md, docs/SETUP_GUIDE.md, packages/web/.env.example, packages/web/README.md
Updates deployment recommendations, GitHub App setup (Organization Members: Read-only), CI secrets table, .env templates, and troubleshooting to document org allowlist and permission requirements.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 I hop through docs and Terraform vines,
I check orgs where membership shines,
Async hops and scopes that grow,
Tokens whisper yes or no,
Now sign-in gates in tidy lines.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: adding GitHub organization membership as an authentication method for sign-in control.
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 unit tests (beta)
  • Create PR with unit tests
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch github-organizations

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.

Copy link
Copy Markdown

@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: 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 `@packages/web/src/lib/access-control.ts`:
- Around line 91-126: Add a named exported timeout constant (e.g.,
GITHUB_MEMBERSHIP_CHECK_TIMEOUT_MS) and modify checkGitHubOrganizationAccess to
bound each fetch with an AbortController: create an AbortController for each org
check, set a timer with setTimeout to call controller.abort() after the constant
timeout, pass controller.signal to fetchImpl, and clear the timer when the
request completes or errors; you can also add an optional timeoutMs parameter to
checkGitHubOrganizationAccess (defaulting to the new constant) so callers can
override the default. Ensure you reference the existing symbols
checkGitHubOrganizationAccess, fetchImpl, allowedOrganizations and userAgent,
and keep the same error handling (catch returning false) while cleaning up
timers and not leaving dangling timeouts.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: b8b6425c-e1f3-44b7-a814-26157a58c4af

📥 Commits

Reviewing files that changed from the base of the PR and between 377b195 and 7b81461.

📒 Files selected for processing (13)
  • README.md
  • docs/GETTING_STARTED.md
  • docs/SETUP_GUIDE.md
  • packages/web/.env.example
  • packages/web/README.md
  • packages/web/src/lib/access-control.test.ts
  • packages/web/src/lib/access-control.ts
  • packages/web/src/lib/auth.ts
  • terraform/environments/production/checks.tf
  • terraform/environments/production/terraform.tfvars.example
  • terraform/environments/production/variables.tf
  • terraform/environments/production/web-cloudflare.tf
  • terraform/environments/production/web-vercel.tf

Comment thread packages/web/src/lib/access-control.ts Outdated
Copy link
Copy Markdown
Owner

@ColeMurray ColeMurray left a comment

Choose a reason for hiding this comment

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

[Automated Review]

Multi-perspective security review of the new GitHub-org-membership allowlist. Below is the summary of cross-cutting findings; individual issues are left as inline comments.

Blocking findings

  • H1 (inline on auth.ts:42): read:org is granted unconditionally to every user, even when ALLOWED_GITHUB_ORGS is empty. Combined with the existing token-persistence path (see auth.ts:67 comment and the control plane's scmToken storage at packages/control-plane/src/router.ts:898), this is a permanent scope increase across every deployment, baked into encrypted tokens in D1 and per-DO SQLite.
  • H2 — CI workflow does not propagate ALLOWED_GITHUB_ORGS: .github/workflows/terraform.yml:205-206 and :336-337 export TF_VAR_allowed_users and TF_VAR_allowed_email_domains only. The new TF_VAR_allowed_github_orgs is missing in both plan and apply jobs, but docs/GETTING_STARTED.md:692 instructs operators to add ALLOWED_GITHUB_ORGS as a GitHub Actions secret. Operators following the docs will see Terraform silently fall back to default = "", either failing the access-control gate or (worse) succeeding with the new allowlist unapplied because allowed_users is still set. Fix the workflow before merge.
  • H4 (inline on auth.ts:48): No session.maxAge is configured; NextAuth defaults to 30 days. A user removed from the allowlisted org retains web access AND a usable accessToken for up to a month. The PR description acknowledges this as follow-up, but it should at minimum land with a short maxAge (e.g. 24h) and an operator-facing doc warning.
  • H5 (inline on auth.ts:72): No audit logging on sign-in. For a system that grants users sandbox-spawning rights with their GitHub tokens, denied attempts and the matching allowlist should be logged server-side (never the token itself).

Other concerns (inline)

  • M1: Promise.all fanout with no short-circuit (access-control.ts:101).
  • M2: All error modes (401/403/404/429/5xx/network) collapse to false with no logging, making operator debugging impossible (access-control.ts:121).
  • M3: GitHub App "Members: Read-only" doc claim conflates OAuth scope with App installation permission (docs/GETTING_STARTED.md:219).
  • M4: OR semantics across three allowlists is a footgun; needs a bold callout (packages/web/README.md:95).
  • M5: Pre-existing — user.email from NextAuth's GitHub provider is not checked for verified (auth.ts:60).

Other notes (not inline)

  • M6packages/web/src/lib/auth.ts:35 enables NextAuth debug when NODE_ENV === "development" OR NEXTAUTH_DEBUG === "true". In next-auth/core/routes/callback.js:55, logger.debug("OAUTH_CALLBACK_RESPONSE", { profile, account, ... }) logs the full account object, which contains access_token and refresh_token. With the expanded scope, those logged tokens now carry read:org. Consider gating NEXTAUTH_DEBUG to non-production environments only.
  • Retracted finding — An earlier draft flagged "existing users with stale pre-PR tokens get locked out by checkGitHubOrganizationAccess failing 403." After verification, this is incorrect: NextAuth's signIn callback only runs at fresh sign-in, not on each request, so pre-existing sessions are untouched (which is actually the H4 concern in disguise). Withdrawing that one.

Things done well

  • Endpoint choice (/user/memberships/orgs/{org}) is authoritative from the user's perspective and distinguishes active from pending.
  • Fail-closed throughout (401/403/404/5xx/network/JSON-parse → false).
  • X-GitHub-Api-Version: 2022-11-28 pinned.
  • encodeURIComponent on the org name prevents URL/path-traversal injection.
  • fetchImpl dependency-injected for testability.
  • Terraform safety gate correctly extended (checks.tf:20-26).
  • Three-way OR logic with all-empty guard preserved (access-control.ts:54-62).

Recommendation

Block on H1, H2, H4, H5 before merge — each is small (conditional scope string, two workflow lines, one config line, one log statement, one doc warning) and meaningfully changes the security posture. M1–M5 are appropriate fast follow-ups.

Comment thread packages/web/src/lib/auth.ts Outdated
authorization: {
params: {
scope: "read:user user:email repo",
scope: "read:user user:email repo read:org",
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

[Automated Review] H1 (HIGH) — read:org scope granted unconditionally

This literal grants read:org to every user even when ALLOWED_GITHUB_ORGS is empty, meaning operators who only use ALLOWED_USERS / ALLOWED_EMAIL_DOMAINS now ask all their users for org-membership read access on the consent screen and persist tokens carrying that scope.

Fix: compute the scope dynamically — only append read:org when parseAllowlist(process.env.ALLOWED_GITHUB_ORGS).length > 0. NextAuth supports a function for authorization.params.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Thanks, agreed. This is fixed in c31d15d.

read:org is now added only when ALLOWED_GITHUB_ORGS is configured. The base scope remains read:user user:email repo, and buildGitHubOAuthScope() appends read:org only when the parsed org allowlist is non-empty.

return true;

const isAllowedByOrgMembership = await checkGitHubOrganizationAccess({
accessToken: account?.access_token,
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

[Automated Review] H1 follow-on — expanded-scope token is persisted broadly

The same account.access_token used here is also stored in the JWT at auth.ts:76, forwarded to the control plane as scmToken (see packages/web/src/app/api/sessions/route.ts:56-71 and packages/web/src/app/api/sessions/[id]/ws-token/route.ts:47-48), AES-encrypted with TOKEN_ENCRYPTION_KEY at packages/control-plane/src/router.ts:898, and persisted to D1 + per-DO SQLite for the session lifetime.

A TOKEN_ENCRYPTION_KEY compromise or sandbox-side exfiltration now exposes every user's private org/team graph in addition to the existing repo blast radius. Making read:org conditional (see H1 on line 42) eliminates this for the majority of deployments.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed by the same conditional-scope change in c31d15d.

Deployments that only use ALLOWED_USERS or ALLOWED_EMAIL_DOMAINS no longer request or persist
tokens with read:org. When ALLOWED_GITHUB_ORGS is configured, the scope is still required because
the sign-in flow needs to check the signing-in user's org membership.

The follow-up auth-boundary tests in ac396b6 verify that the org-membership path uses
account.access_token for the membership request and keeps the scope conditional.

],
callbacks: {
async signIn({ profile, user }) {
async signIn({ account, profile, user }) {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

[Automated Review] H4 (HIGH) — no session lifetime configured; org removal takes up to 30 days to revoke access

The PR description acknowledges this as follow-up work, but the access-control implication is significant: NextAuth's default maxAge is 30 * 24 * 60 * 60 (see next-auth/core/init.js), and this codebase doesn't override session.maxAge. A user removed from allowed_github_orgs retains web access AND a usable accessToken (in the JWT + D1) for up to 30 days.

Minimum action (this PR): set an explicit session: { maxAge: 60 * 60 * 24 } (24h) when ALLOWED_GITHUB_ORGS is used, OR re-run checkGitHubOrganizationAccess inside the jwt callback with a short cache TTL.

Minimum doc action: call this out prominently in docs/GETTING_STARTED.md near the new allowed_github_orgs instructions.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I fully agree that this is an issue, but this existed before this PR. I would be happy (and plan to) submit an additional PR to better control and validate sessions. We should be able to periodically check whether the current user is still valid and expire the session if not.

Comment thread packages/web/src/lib/auth.ts Outdated
userAgent: process.env.NEXT_PUBLIC_APP_NAME?.trim() || DEFAULT_APP_NAME,
});

return isAllowedByOrgMembership;
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

[Automated Review] H5 (HIGH) — no audit logging on sign-in decisions

Neither signIn nor checkGitHubOrganizationAccess logs anything: no record of who attempted to sign in, who was granted, who was denied, or which allowlist matched. For a system that grants users sandbox-spawning rights with their GitHub tokens, brute-force username enumeration against ALLOWED_USERS is undetectable, and operators have no audit trail for org-membership decisions.

Fix: add a server-side console.info (or your structured logger of choice) with { login, decision, reason }. Never log the access token.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in f28f20b.

signIn now logs server-side decisions with non-secret metadata:

{ login, decision, reason }

The reasons distinguish username allowlist, email-domain allowlist, org membership, no matching
policy, denied org membership, and unavailable org verification. Tokens are never logged.

ac396b6 adds tests that assert the decision log is emitted and does not include the OAuth token.

Comment thread packages/web/src/lib/access-control.ts Outdated
return false;
}

const checks = allowedOrganizations.map(async (org) => {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

[Automated Review] M1 (MEDIUM) — Promise.all fanout with no short-circuit and no cap

Every sign-in fires one parallel GitHub API call per allowed org. With N orgs configured, that's N requests on the user's 5000/hr token budget. There's no early-exit on first match, no cap on N, and no distinction between 429 (rate-limit) and 404 (not a member) — both collapse to false in the catch block at line 121.

Failure modes:

  • A noisy operator config (e.g. 20+ orgs) measurably increases sign-in latency and rate-limit pressure.
  • Transient secondary rate limits silently deny legitimate users with no operator signal.

Fix: iterate sequentially with early return, or wrap Promise.any with appropriate rejection handling. Consider a sane upper bound on allowedOrganizations.length and log a warning above it.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in f28f20b.

Org membership checks now run sequentially and return immediately when the first active membership is
found. That avoids unnecessary calls for users who match an earlier configured org and reduces rate
limit pressure.

I did not add a hard cap on ALLOWED_GITHUB_ORGS in this PR; the sequential early-return behavior
addresses the direct fanout issue without adding a new config limit.

Comment thread packages/web/src/lib/access-control.ts Outdated

const membership = (await response.json()) as { state?: string };
return membership.state === "active";
} catch {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

[Automated Review] M2 (MEDIUM) — all error modes silently collapse to false

401 (token invalid), 403 (SAML SSO required, App lacks Members permission), 404 (not a member), 429 (rate limited), 5xx (GitHub outage), JSON parse errors, and network errors all return false indistinguishably.

An operator who configures ALLOWED_GITHUB_ORGS=acme and reports "users get Access denied" has no way to tell which root cause is in play, and SAML-SSO-enforced orgs are a particularly common silent failure.

Fix: log at least { org, status } on the non-OK branch (line 115). Do not log the access token.

if (!response.ok) {
  console.warn(`[org-access] org=${org} status=${response.status}`);
  return false;
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed across f28f20b and 8f01c4a.

The org check now logs non-secret diagnostics for GitHub responses and request errors, including:

  • org
  • HTTP status
  • GitHub request id
  • rate-limit headers
  • retry-after
  • elapsed time

It also returns a typed result so auth.ts can distinguish normal denials from operational failures:

  • not_member
  • unavailable
  • active membership

The sign-in decision log now reports org_membership_unavailable for 403/429/5xx, network errors,
aborts, malformed JSON, and unexpected response shapes instead of collapsing those into an ordinary
org_membership_denied.

Comment thread docs/GETTING_STARTED.md Outdated
7. Note the **App ID** and **Client ID** (top of page)
8. Under **"Client secrets"**, click **"Generate a new client secret"** and note the **Client
6. Set **Organization permissions**:
- Members: **Read-only** _(required when using `ALLOWED_GITHUB_ORGS`)_
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

[Automated Review] M3 (MEDIUM) — docs claim conflates OAuth scope with GitHub App permission

The runtime check uses an OAuth user-to-server token (via the NextAuth GitHub provider), not the GitHub App installation token. What primarily enforces the read at runtime is the user's OAuth read:org scope, which Open-Inspect now requests automatically (packages/web/src/lib/auth.ts:42).

The "Organization permissions: Members: Read-only" requirement is correct only because the OAuth provider here is a GitHub App (App user-tokens are gated by the App's declared user-level permissions). It is not correct that the org admin must "approve" the new permission for users to sign in via OAuth in all cases, and the App does not need to be installed on the target org for the user's own membership lookup to work.

Suggested rewrite: clarify that this requirement applies because Open-Inspect uses a GitHub App as the OAuth provider, and that the user's read:org OAuth grant is what authorizes the membership check at runtime. Operators following these docs against a classic OAuth App will be confused.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 07a834b.

The docs now clarify that the runtime membership check uses the signing-in user's OAuth token and
that read:org is requested only when org access is configured.

They also clarify why the GitHub App permission matters here: Open-Inspect uses a GitHub App as the
OAuth provider, so operators using org access need Organization permissions: Members read-only, and
existing GitHub Apps need the permission change republished/requested/approved before testing.

Comment thread packages/web/README.md
> denied unless `UNSAFE_ALLOW_ALL_USERS=true`. For Terraform-managed production deploys, Terraform
> also fails validation unless you set at least one allowlist or explicitly opt in with
> `unsafe_allow_all_users = true`.
> **Access Control**: If `ALLOWED_USERS`, `ALLOWED_EMAIL_DOMAINS`, and `ALLOWED_GITHUB_ORGS` are all
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

[Automated Review] M4 (MEDIUM) — OR semantics is a footgun; needs a bold callout

The current phrasing ("If ALLOWED_USERS, ALLOWED_EMAIL_DOMAINS, and ALLOWED_GITHUB_ORGS are all empty, sign-in is denied unless UNSAFE_ALLOW_ALL_USERS=true") doesn't make the union semantics obvious.

A reasonable operator instinct is AND: they set ALLOWED_EMAIL_DOMAINS=acme.com AND ALLOWED_GITHUB_ORGS=acme expecting "must be in the org AND on the corp domain." In reality they get a union — a GitHub account with an unverified someone@acme.com primary email (see M5) passes via the weaker email check and never reaches the server-enforced org check.

Suggested addition: a bold callout near this block: "⚠️ These lists are OR-combined. Configuring multiple allowlists widens access, not narrows it. Each list adds an independent way to be granted access." Same callout belongs in packages/web/.env.example and docs/GETTING_STARTED.md.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 07a834b.

The docs now explicitly call out that allowlists use OR semantics: matching any configured GitHub
username, email domain, or active GitHub org membership grants access. This is documented in:

  • docs/GETTING_STARTED.md
  • packages/web/README.md
  • packages/web/.env.example
  • docs/SETUP_GUIDE.md
  • terraform/environments/production/terraform.tfvars.example

Copy link
Copy Markdown
Owner

@ColeMurray ColeMurray left a comment

Choose a reason for hiding this comment

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

see comments

@scottwater
Copy link
Copy Markdown
Author

For the M5 and M6 -

I agree these are worth addressing, but I am keeping them out of this PR because they are pre-existing
auth hardening items rather than regressions introduced by GitHub org allowlisting.

@scottwater scottwater requested a review from ColeMurray May 25, 2026 20:50
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