Skip to content

feat(auth): let analysts read and build dashboards, and refuse every other change - #2546

Merged
Makisuo merged 4 commits into
observer/policyfrom
observer/enforcement
Nov 13, 2026
Merged

Makisuo merged 4 commits into
observer/policyfrom
observer/enforcement

Conversation

@Makisuo

@Makisuo Makisuo commented Nov 12, 2026 •

Copy link
Copy Markdown
Collaborator

Second layer of the Analyst role, on top of #2543. An analyst reads everything and builds dashboards; every other HTTP endpoint that changes the organization is refused.

What changed

  • The auth middlewares (internal, public and /v2) refuse a session or credential whose highest role is analyst unless the request reads, changes the caller's own state, or builds a dashboard.
  • It is refused by default. A request passes when it is a GET, when the endpoint is marked AnalystAccess, or on /v2 when the scope registry already names it a read. A new endpoint is refused until someone marks it.
  • Marked author: the twelve dashboard writes on /v2/dashboards (create, import, create from a template, update, delete, restore a version, and the share links for a dashboard and for a widget: create, rotate, revoke). They are marked one by one, so a write added to that group later is refused until it is marked.
  • 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 an analyst's too.
  • Internal routes answer 403 with LimitedRoleError, /v2 answers 403 insufficient_permissions, both with "Analysts can read Maple and build dashboards, but not change anything else. Ask an org admin for the Member role to do more." 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.

Verified

  • packages/domain: analyst-access.test.ts and the policy tests, 22 passed. Cases: every dashboard write is open to an analyst; issue triage, a new SLO and a new alert rule are not.
  • packages/backend: analyst-gate.test.ts, 6 passed.
  • apps/api: the dashboards, phase 1 resources, telemetry and mobile device HTTP tests, 115 passed. They run the real /v2 middleware with an analyst-pinned key: it creates, renames, publishes a share link for and deletes a dashboard; reads an issue and is refused a transition (403, nothing recorded); searches traces by POST; registers and removes its own device.
  • Typecheck for @maple/domain and @maple/backend; the CI lint config on every changed file; the generated error list and iOS OpenAPI document are up to date.
  • Two independent reviews of the diff, one of the original gate and one of the dashboard change. The second enumerated every non-GET endpoint open to an analyst and found only the twelve dashboard writes marked author, with handlers that touch dashboards, versions, shares and audit rows only.

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 applies the same rule there. 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 analysts. Those groups exist only to query.
  • An analyst can publish a public share link for a dashboard, as a member can. That is a product decision made for this role.
  • 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 analyst. Before this layer merges, the role keys in use on the production auth instance 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

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 17 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: 16b14208-28e0-4da8-81c7-dbee501835a1
Commits

Reviewing files that changed from the base of the PR and between 6fab3c2 and f72b00b.

Files ignored due to path filters: 1

  • packages/domain/src/generated/anticipated-error-identifiers.ts is excluded by !**/generated/**
Files selected for processing: 35

  • CLAUDE.md
  • apps/api/src/routes/v2/dashboards.http.test.ts
  • apps/api/src/routes/v2/mobile-devices.http.test.ts
  • apps/api/src/routes/v2/phase1-resources.http.test.ts
  • apps/api/src/routes/v2/telemetry.http.test.ts
  • apps/ios/Packages/MapleAPI/Sources/MapleAPI/openapi.json
  • packages/backend/src/services/auth/ApiAuthorizationLive.ts
  • packages/backend/src/services/auth/ApiAuthorizationV2Live.ts
  • packages/backend/src/services/auth/SessionAuthorizationLive.ts
  • packages/backend/src/services/auth/analyst-gate.test.ts
  • packages/backend/src/services/auth/analyst-gate.ts
  • packages/domain/src/http/ai-models.ts
  • packages/domain/src/http/ai-sessions.ts
  • packages/domain/src/http/analyst-access.test.ts
  • packages/domain/src/http/analyst-access.ts
  • packages/domain/src/http/auth.ts
  • packages/domain/src/http/billing.ts
  • packages/domain/src/http/current-tenant.ts
  • packages/domain/src/http/digest.ts
  • packages/domain/src/http/errors.ts
  • packages/domain/src/http/index.ts
  • packages/domain/src/http/integrations.ts
  • packages/domain/src/http/mcp.ts
  • packages/domain/src/http/query-engine.ts
  • packages/domain/src/http/session-replay.ts
  • packages/domain/src/http/users.ts
  • packages/domain/src/http/v2/agent-feedback.ts
  • packages/domain/src/http/v2/auth.ts
  • packages/domain/src/http/v2/dashboards.ts
  • packages/domain/src/http/v2/device-credentials.ts
  • packages/domain/src/http/v2/integrations-chat.ts
  • packages/domain/src/http/v2/mcp-connections.ts
  • packages/domain/src/http/v2/mobile-devices.ts
  • packages/domain/src/http/v2/widget-credentials.ts
  • packages/domain/src/policy/index.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

@maple-review-bot

This comment has been minimized.

# Conflicts:
#	packages/domain/src/policy/roles.ts
Dashboard writes are marked AnalystAccess author, so the auth middlewares let an analyst through them and still refuse every other change. The refusal is now LimitedRoleError (limited_role), since the role is no longer read-only.
@Makisuo Makisuo changed the title feat(auth): refuse observers every endpoint that changes the organization feat(auth): let analysts read and build dashboards, and refuse every other change Nov 13, 2026
@maple-review-bot

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

Copy link
Copy Markdown

Maple review

🟢 Confidence 10/10 · safe to merge. The gate is one four-line pure function wired into every auth path, and real middleware tests pin both the refusal and the allowed reads.
no findings · tests covered · risk high · 1/1 new units observable

Refuses an analyst-role session or credential every non-GET endpoint on the internal and /v2 APIs unless the endpoint is annotated AnalystAccess or the /v2 scope registry already calls it a read. The check lives in the auth layers, so an unmarked endpoint fails closed, and it is covered by middleware-level tests.

  • checkAnalystAccess refuses analysts by default in all three auth middlewares
  • AnalystAccess annotation opens reads, own-state writes and dashboard authoring
  • /v2 dashboard writes are marked author: analysts may create, share and delete
  • LimitedRoleError (403 limited_role) added to the middleware error unions

Before merge

  • Manual · Check the Clerk role keys on dev and prd map to a known clerkKey: roleOf falls back to analyst, which now refuses every write

f72b00b · 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.

@Makisuo
Makisuo added this pull request to stack #2556 November 13, 2026 12:45
@Makisuo
Makisuo merged commit 53271f5 into main Nov 13, 2026
77 of 92 checks passed
@maple-review-bot

Copy link
Copy Markdown

Merged with 1 step from "Before merge" still open. If it is done, tick it on the review comment; if not, now is the time.

  • Manual · Check the Clerk role keys on dev and prd map to a known clerkKey: roleOf falls back to analyst, which now refuses every write

@Makisuo
Makisuo deleted the observer/enforcement branch November 13, 2026 12:56
@maple-review-bot

Copy link
Copy Markdown

Maple: after this shipped

⚠️ Something changed after this shipped. Details below.

Deployed to maple-api (pr-2544) at 2026-11-13 13:17 UTC as cd8f430, the first version after the merge (the merge commit itself never reported). Compared the 60 minutes before and after.

New errors since the deploy:

  • @maple/http/cost-connections/CostConnectionPersistenceError (maple-api, 12/h)
  • @maple/http/cost-connections/CostConnectionPersistenceError (maple-api, 4/h)
Operation Calls/h Errors p95
GET 0 → 0 0.0% → 0.0% 0 → 0 ms
GET 7 → 3 0.0% → 0.0% 734 → 500 ms
preview 0 → 0 0.0% → 0.0% 0 → 0 ms
POST /v2/device_tokens 3 → 3 0.0% → 0.0% 40 → 121 ms
GET /v2/dashboards 2 → 1 0.0% → 0.0% 162 → 40 ms

Maple looks once, an hour after the first deploy that carries the merge.

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