Skip to content

fix(integrations): require account admin for GitHub installs, explicit callback origins - #1685

Merged
Makisuo merged 1 commit into
mainfrom
sec/1022b-github-install-and-origin
Oct 22, 2026
Merged

Makisuo merged 1 commit into
mainfrom
sec/1022b-github-install-and-origin

Conversation

@Makisuo

@Makisuo Makisuo commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Issue Fix Test
GitHub connect treated an installation listed in GET /user/installations as owned by the user. That endpoint lists installations the user can access, not ones they administer. A new binding also requires the user to administer the installation's account: an active admin membership via GET /user/memberships/orgs/{org} for an org, or the authenticated user's own id for a personal account. Fails closed when the role cannot be read. adminInstallationIds renamed to accessibleInstallationIds. Same-org reconnects are unchanged. GithubConnectService.test.ts: listed but member / pending / role unreadable / other user's account -> refused, no binding, no sync job; org admin and own account -> bound. Asserts the role endpoint called.
isTrustedCallbackOrigin accepted any host under the app's parent domain. Explicit allow list: configured MAPLE_APP_BASE_URL and MAPLE_API_BASE_URL origins (exact scheme/host/port), that PR's own app-pr-<n> / api-pr-<n>.maple.dev only when MAPLE_ENVIRONMENT is pr-<n>, and local hosts only in development with a local app origin. Callers now pass env. integrations-origin.test.ts (sibling lookalike, other PR number, port, scheme, spoofed host refused; configured and preview origins accepted). integrations.http.test.ts: connect from a sibling host returns 503.
PlanetScale webhook_config built its URL from the forwarded host. Built from MAPLE_API_BASE_URL. integrations.http.test.ts: forwarded host ignored.

Overlap note: #1673 and #1675 also touch GithubConnectService.completeConnect and routes/internal/integrations.http.ts. This diff keeps the service change to the admin verification block inside completeConnect (token kept as an Option, check added after the installation detail) and only changes the helper call in internal/integrations.http.ts, so it should rebase cleanly in either order.

Breaking-change risk

  • GitHub App permission required. The org admin check needs the App's Organization permission Members: Read (no other way for a user token to read an org role). Until it is added, connecting a new organization installation is refused with a clear message; personal-account installs and same-org reconnects keep working. docs/github-app-setup.md updated.
  • Self-hosted installs that served callbacks from a sibling host without setting MAPLE_API_BASE_URL now need it set to their API origin. All alchemy stages (prd, EU, previews, dev) already set both URLs from their domains.

Verification

  • bun run --cwd packages/backend test -- .../github/__tests__/ (77 passed), bun run --cwd apps/api test -- integrations-origin integrations.http chat-destinations integrations-chat.identity integrations-gcp (all passed).
  • New tests fail on main (7 in the connect suite, 2 in the HTTP suite) and pass here.
  • typecheck for packages/backend and apps/api, bun run lint, oxfmt on changed files.

Needs a human

  • Add Organization permissions -> Members: Read to the prod (and dev/EU, if separate) GitHub Apps, and accept it on existing installations that are not yet linked. Without it, new org connects are refused.

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

…t callback origins

GitHub connect: a new installation binding now also checks that the
connecting user administers the installation's account (active org admin
via GET /user/memberships/orgs/{org}, or the personal account itself).
Listing in GET /user/installations only proves access. The App needs the
Organization "Members: Read" permission for the org check; without it the
connect fails closed.

Callback origins: isTrustedCallbackOrigin now accepts only the configured
app and API origins, the deploy's own PR preview hosts, and local hosts in
development, instead of any host under the app's parent domain. The
PlanetScale webhook_config URL is built from MAPLE_API_BASE_URL instead of
forwarded headers.
@maple-review-bot

maple-review-bot Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Maple review

🟢 Confidence 9/10 · safe to merge
Admin check sits on the real callback path and the origin allow list fails closed; the only weight is that this is authorization code with no alternate path exercised.
no findings · tests covered · risk high −1 · 1/1 new units observable

Tightens GitHub connect to require admin of the installation's account and replaces the sibling-parent-domain callback rule with an exact allow list plus a PlanetScale webhook URL built from MAPLE_API_BASE_URL. The logic is sound and well covered; safe to merge once the App permission is granted.

  • checkAccountAdmin verifies org admin membership or the personal-account owner
  • completeConnect refuses a new binding unless the OAuth user administers the account
  • isTrustedCallbackOrigin now takes the deploy config and allows only exact origins
  • PlanetScale webhook_config URL comes from MAPLE_API_BASE_URL, not forwarded headers

Before merge

  • Manual · Add Organization permissions → Members: Read to the prd, EU and dev GitHub Apps and accept the request on existing installations
  • Manual · Set MAPLE_API_BASE_URL on any self-hosted install that served callbacks from a sibling host

Production impact

Telemetry this change adds and removes (1)
  • ➕ span name GithubAppClient.checkAccountAdmin · packages/backend/src/services/integrations/vcs/vendor/github/GithubAppClient.ts:2179
What was checked
  • Every isTrustedCallbackOrigin caller passes env (internal/integrations.http.ts:120, integrations-chat.http.ts:121,155); no stale signature remains
  • Dev stages resolve MAPLE_APP_BASE_URL to https://<prefix>web.localhost (alchemy.run.ts:98), so the development branch still accepts local hosts
  • New GET /user and /user/memberships/orgs/{org} calls go through tracedFetch with peer.service: github (GithubAppClient.ts:637), so they appear on the service map
Observability coverage: 1 of 1 changes observable
Change Kind Observable Evidence
GitHub account-admin lookup (GET /user, GET /user/memberships/orgs/{org}) outbound call yes GithubAppClient.ts:637 tracedFetch, kind client, peer.service github

49b5c79 · Updated on every push. Reply "won't fix" to dismiss a finding, or mention @maple-review-bot to ask about one.

@coderabbitai

coderabbitai Bot commented Oct 22, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 37 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 4 included reviews currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 41b68ee8-667d-4d44-9d04-9697487ed829
📥 Commits

Reviewing files that changed from the base of the PR and between fba9aed and 49b5c79.

📒 Files selected for processing: 9
  • apps/api/src/routes/internal/integrations.http.ts
  • apps/api/src/routes/v2/integrations-chat.http.ts
  • apps/api/src/routes/v2/integrations-origin.test.ts
  • apps/api/src/routes/v2/integrations.http.test.ts
  • apps/api/src/routes/v2/integrations.http.ts
  • docs/github-app-setup.md
  • packages/backend/src/services/integrations/vcs/vendor/github/GithubAppClient.ts
  • packages/backend/src/services/integrations/vcs/vendor/github/GithubConnectService.ts
  • packages/backend/src/services/integrations/vcs/vendor/github/__tests__/GithubConnectService.test.ts
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

@Makisuo
Makisuo merged commit 85326f2 into main Oct 22, 2026
27 checks passed
@Makisuo
Makisuo deleted the sec/1022b-github-install-and-origin branch October 22, 2026 15:55
@maple-review-bot

Copy link
Copy Markdown

Merged with 2 steps from "Before merge" still open. If they are done, tick them on the review comment; if not, now is the time.

  • Manual · Add Organization permissions → Members: Read to the prd, EU and dev GitHub Apps and accept the request on existing installations
  • Manual · Set MAPLE_API_BASE_URL on any self-hosted install that served callbacks from a sibling host

Thegreatsura pushed a commit to Thegreatsura/maple-analytics that referenced this pull request Oct 23, 2026
…sts (MapleTechLabs#1698)

MapleTechLabs#1685's account-admin cases called completeConnect without the initiator MapleTechLabs#1675 made
required, so the call failed before the role lookup and every case read no role URLs.
Main has failed these 6 tests since both landed.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Thegreatsura pushed a commit to Thegreatsura/maple-analytics that referenced this pull request Oct 23, 2026
…#1635)

* feat(integrations): connect an AWS account as a source

An AWS connection is a pull source backed by an IAM role in the customer's
account. Maple mints an external id, the customer deploys a generated
CloudFormation stack (CloudShell script, console template or Terraform), and
Maple assumes the role from its own identity to confirm the setup. No AWS
secret is stored.

- aws_connections table (source_id required, cascades with its source),
  AwsConnectionService and /v2/integrations/aws (status, create, update,
  setup, check, delete).
- The role trusts Maple's account root, conditioned on the exact principal
  ARN and the connection's external id. Its name is derived from the
  connection id, so a caller can never point Maple at another role.
- Each future module (metrics, inventory, security, cost) is its own policy
  on the same role; the check reads which are attached and the setup
  version tag, so a new module is a stack update, not a reconnect.
- Hourly alerting lane re-checks connections: paused sources are skipped in
  SQL, the batch is claimed in one statement, two checks run at a time.
  A deleted stack reports the source disconnected; AWS outages only record
  the error.
- Connect card under Sources: account form, setup panel that re-checks every
  10s while open, connection health, disconnect with a cleanup script kept
  until dismissed.
- Docs page integrations/aws.

* chore(aws): regenerate the iOS OpenAPI spec and anticipated error identifiers

* fix(aws): the unavailable card drops the steps it cannot offer

* fix(aws): a background check only closes its own setup; pending setup is not a failure

* fix(aws): the hourly sweep runs batches until nothing is due, never-checked first

* fix(aws): a sweep checks its batch in due order, never-checked first

* fix(aws): no check starts past the sweep budget

* fix(aws): a disconnect closes only its own dialog; document and test PATCH

A completed disconnect read the last-clicked connection, so it could close
another connection's dialog and setup. The update request now documents that
omitted modules stay unchanged, and update is tested in the service and route.

* test(web): the in-flight click case starts a poll before it clicks

The case clicked a page with no check in flight, so it never reached the
flip that makes a background check the user's.

* test(aws): the budget sweep allows a check that timed out unassumed

Two checks run at once and each fake call jumps the shared clock 9s, so a
call can pass its 20s timeout and count as checked without an AssumeRole.

* fix(web): an AWS stack that needs updating reads as needing attention

The hub card, sources picker and header now agree with the connection row.

* test(aws): another org's admin cannot see or touch a connection; members cannot check it

* test(github): pass the initiator in the install admin cases

Same one-line fix as MapleTechLabs#1698: main's MapleTechLabs#1675 and MapleTechLabs#1685 crossed, so these cases
called completeConnect without the initiator MapleTechLabs#1675 made required.
Thegreatsura pushed a commit to Thegreatsura/maple-analytics that referenced this pull request Oct 23, 2026
…ct cases (MapleTechLabs#1699)

MapleTechLabs#1675 made completeConnect require the signed-in initiator; the admin-role
cases from MapleTechLabs#1685 still called it with three arguments, so every case died
on initiator.orgId before reaching the membership check. Backend test files
are outside typecheck, so only the test run caught it.
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.

1 participant