Skip to content

feat(auth): refuse observers every endpoint that changes the organization - #2546

Open
Makisuo wants to merge 1 commit into
observer/policyfrom
observer/enforcement
Open

Makisuo wants to merge 1 commit into
observer/policyfrom
observer/enforcement

Conversation

@Makisuo

@Makisuo Makisuo commented Nov 12, 2026 •

Copy link
Copy Markdown
Collaborator

Second layer of the Observer role, on top of #2543. An observer is now refused every HTTP endpoint that changes the organization.

What changed

  • The auth middlewares (internal, public and /v2) refuse a session or credential whose highest role is observer unless the request only reads or changes the caller's own state.
  • It is refused by default. A request passes when it is a GET, when the endpoint is marked ObserverAccess, or on /v2 when the scope registry already names it a read. A new endpoint is refused until someone marks it.
  • Marked read: the dashboard query groups (query engine, users, AI sessions, MCP servers, AI models, session replays) and the previews that sit beside writes (digest previews, escalation policy dry run, billing price preview, two integration reads).
  • Marked personal: the caller's digest subscription, mobile and widget devices, device credentials, their own MCP connection, agent feedback, and the CLI and MCP sign-in approvals. Those approvals pin the caller's role, so the credential they mint is read-only too.
  • Internal routes answer 403 read_only_role, /v2 answers 403 insufficient_permissions, both with "Observers can read Maple but not change it. Ask an org admin for the Member role to make changes." API key denials are audited like the other denials.
  • /v2 endpoints now list insufficient_permissions as a possible 403, so the iOS OpenAPI document is regenerated.

Of the 412 non-GET endpoints across the APIs, 186 stay open to observers, 17 more through the /v2 read registry, and 209 are refused.

Verified

  • packages/domain: observer-access.test.ts, policy, anticipated errors, OpenAPI and v2 contract tests, 94 passed.
  • packages/backend: observer-gate.test.ts, 5 passed.
  • apps/api: the whole package, 689 passed. New cases run the real /v2 middleware with an observer-pinned key: it reads an issue and is refused a transition (403, nothing recorded), searches traces by POST, and registers and removes its own device.
  • Typecheck for @maple/domain, @maple/backend, @maple/api and @maple/web; the CI lint config on every changed file.
  • An independent review of the diff. Its findings are fixed here: sign-in approvals are marked per endpoint instead of per group, chat account linking is not started for observers because its completion step is refused, and the public API key path audits the denial.

Known gaps, handled in later layers or follow-ups

  • The chat message routes and MCP tools do not go through these middlewares. The next layer makes them read-only by role. Nobody can hold the role until the Members layer above that.
  • The six query groups are marked at group level, so a write added to one of them later would be open to observers. Those groups exist only to query.
  • Revoking a widget credential is keyed by organization and installation id, not by user. That is already true for members and needs another person's installation id, which nothing exposes.
  • A role key the code does not know reads as observer. Before the Members layer merges, the role keys in use on the dev and prd instances need a check.

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

…tion

The auth middlewares now refuse a session or credential whose highest role is observer any request that is not a read or a change to the caller's own state. It is refused by default: an endpoint passes only as a GET, when it is marked ObserverAccess, or on /v2 when the scope registry already names it a read.

Marked read: the dashboard query groups and the previews that sit beside writes. Marked personal: the caller's digest, devices, MCP connection and CLI and MCP sign-in approvals, which pin the caller's role. Internal routes answer with ReadOnlyRoleError, /v2 with insufficient_permissions, and API key denials are audited.
@coderabbitai

coderabbitai Bot commented Nov 12, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

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: 4f754e67-680e-46d1-93ab-f62a1bd02f7f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • Trigger review
  • 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

@maple-review-bot

maple-review-bot Bot commented Nov 12, 2026 •

Copy link
Copy Markdown

Maple review

🟢 Confidence 8/10 · likely safe to merge. Fail-closed gate with readable markers; the internal and public middleware wiring has no end-to-end observer test, only the gate functions.
no findings · tests partial −1 · risk high −1

Refuses a read-only role every non-GET endpoint the internal, public and /v2 auth middlewares serve, unless the endpoint (or its group) is marked ObserverAccess or /v2 already names the route a read. The markers match what those handlers change, and the default is refuse.

  • refuseObserverWrites fails 403 ReadOnlyRoleError in the internal and public auth layers
  • refuseObserverWritesV2 reuses the /v2 scope registry's read-only POST list
  • ObserverAccess marks the six query groups plus the caller's own devices and approvals
  • Endpoint-level read marks cover digest, billing, integrations and the escalation dry run

Before merge

  • Manual · Verify every Clerk role key in dev and prd is known to roleOf; an unknown key reads as observer

11938f4 · What was checked, coverage and telemetry details are on the Maple / review check. Reply "won't fix" to dismiss a finding, or mention @maple-review-bot to ask about one.

This branch has not been deployed

No deployments
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