Skip to content

feat(demo): lock down identity, egress, and credential surfaces#185

Merged
TerrifiedBug merged 1 commit intomainfrom
feat/demo-mode-backend-guards
Apr 27, 2026
Merged

feat(demo): lock down identity, egress, and credential surfaces#185
TerrifiedBug merged 1 commit intomainfrom
feat/demo-mode-backend-guards

Conversation

@TerrifiedBug
Copy link
Copy Markdown
Owner

Summary

Closes the demo-mode coverage gaps surfaced during a deep audit. PR #184 added denyInDemo() to a handful of admin/service-account/alert-webhook mutations; this extends the guard to every mutation that could let a public demo user mint credentials, escalate privileges, exfiltrate data, configure outbound HTTP, or stand up real fleet infrastructure.

Backend coverage added

Identity / privilege escalation

  • user.* self-edit: changePassword, updateProfile, setupTotp, verifyAndEnableTotp, disableTotp
  • team.* membership: create/delete/rename, addMember, removeMember, updateMemberRole, lockMember, unlockMember, resetMemberPassword, updateRequireTwoFactor, updateAvailableTags, updateDefaultEnvironment, linkMemberToOidc

Egress / outbound HTTP / credential storage

  • team.updateAiConfig, team.testAiConnection (LLM API keys + base URL)
  • settings.updateOidc*, settings.testOidc, settings.updateScim, settings.generateScimToken
  • settings.updateFleet, settings.updateAnomalyConfig
  • settings.createBackup, previewBackup, deleteBackup, restoreBackup, updateBackupSchedule, testS3Connection, updateStorageBackend
  • environment.create/update/delete, generateEnrollmentToken, revokeEnrollmentToken
  • gitSync.retryAllFailed, retryJob
  • alertChannels.createChannel/updateChannel/deleteChannel/testChannel
  • webhookEndpoint.create/update/delete/toggleEnabled/testDelivery
  • secret.create/update/delete
  • certificate.upload/delete

REST surface

  • POST /api/agent/enroll short-circuits with 403 in demo before any database lookup. Demo enrollment tokens are now example-only.

Risk notes

  • denyInDemo() already exists in src/trpc/init.ts and is mocked as a passthrough in every router test file from PR feat(demo): harden hosted demo against egress + identity mutations #184, so existing tests pass unchanged.
  • Read-only and demo-data-shaping endpoints (pipelines, deploys, alerts, queries) are intentionally not blocked — those are part of the demo experience.

Test plan

  • pnpm vitest run -- 256 files / 2532 tests pass
  • tsc --noEmit clean
  • New test: enroll route rejects with 403 in demo without touching the database
  • Smoke test in demo mode: confirm AI/Backup/GitSync/Enrollment/2FA/password mutations all return FORBIDDEN
  • Confirm non-demo deployments are unaffected (default NEXT_PUBLIC_VF_DEMO_MODE is unset)

…mo mode

Adds denyInDemo() middleware to mutations that would otherwise let a public
demo user mint credentials, escalate privileges, configure outbound HTTP
destinations, exfiltrate data, or stand up real fleet infrastructure.

Routers covered:
- user: changePassword, updateProfile, setupTotp, verifyAndEnableTotp,
  disableTotp
- team: create, delete, rename, addMember, removeMember, updateMemberRole,
  lockMember, unlockMember, resetMemberPassword, updateRequireTwoFactor,
  updateAvailableTags, updateDefaultEnvironment, linkMemberToOidc,
  updateAiConfig, testAiConnection
- settings: updateOidc/RoleMapping/TeamMappings, updateFleet,
  updateAnomalyConfig, testOidc, all backup/restore/storage mutations,
  updateScim, generateScimToken
- environment: create, update, delete, generateEnrollmentToken,
  revokeEnrollmentToken
- git-sync: retryAllFailed, retryJob
- alert-channels: createChannel, updateChannel, deleteChannel, testChannel
- webhook-endpoint: create, update, delete, toggleEnabled, testDelivery
- secret: create, update, delete
- certificate: upload, delete

REST surface:
- POST /api/agent/enroll: short-circuits with 403 in demo mode before any
  database lookup, so demo enrollment tokens are example-only.

All existing router tests pass unchanged because vi.mock factories already
expose denyInDemo as a passthrough. Adds a focused test confirming the
enroll endpoint rejects in demo without touching the database.
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 27, 2026

Greptile Summary

This PR extends the existing denyInDemo() middleware to every mutation that could let a public demo user mint credentials, escalate privileges, configure outbound HTTP, or store real infrastructure state — covering 9 routers and ~35 procedures. The REST enrollment endpoint (POST /api/agent/enroll) is also hardened with an early-exit 403 before rate-limiting or any database access, accompanied by a targeted test that verifies no DB calls are made in demo mode.

Confidence Score: 5/5

Safe to merge — changes are mechanical middleware additions using an already-tested, well-understood guard with no logic or security issues found.

All changes are additive (inserting an existing middleware before existing auth middleware), the denyInDemo() implementation is simple and correct, the REST guard short-circuits before any side effects, and the new test correctly verifies the DB-isolation property. No P0 or P1 issues found.

No files require special attention.

Important Files Changed

Filename Overview
src/app/api/agent/enroll/route.ts Demo guard added as the very first check before rate-limiting and any DB work; short-circuit is correct and safe.
src/app/api/agent/enroll/tests/route.test.ts New demo-mode describe block correctly captures ORIGINAL_ENV at parse time, restores it in afterEach, and asserts 403 + no DB access.
src/server/routers/settings.ts denyInDemo() added before requireSuperAdmin() on all OIDC, SCIM, backup, fleet, anomaly, and S3 procedures; previewBackup (a .query()) is also guarded intentionally to prevent demo users from reading backup content.
src/server/routers/team.ts denyInDemo() applied to all membership, AI config, and team lifecycle mutations; placement before withTeamAccess/requireSuperAdmin avoids unnecessary DB lookups.
src/server/routers/user.ts denyInDemo() added to changePassword, updateProfile, setupTotp, verifyAndEnableTotp, and disableTotp; updateProfile has .input() before .use(denyInDemo()) which is functionally equivalent in tRPC v11.
src/server/routers/environment.ts create, update, delete, generateEnrollmentToken, and revokeEnrollmentToken all guarded; covers the full enrollment token lifecycle.
src/server/routers/webhook-endpoint.ts All five webhook mutations (create, update, delete, toggleEnabled, testDelivery) are guarded correctly.
src/server/routers/secret.ts create, update, delete all guarded; prevents demo users from writing or overwriting secrets used in pipeline resolution.
src/server/routers/certificate.ts upload and delete guarded correctly.
src/server/routers/alert-channels.ts createChannel, updateChannel, deleteChannel, and testChannel all guarded; testChannel notably blocked to prevent demo users from probing external URLs.
src/server/routers/git-sync.ts retryAllFailed and retryJob guarded; prevents demo users from triggering outbound git operations.

Sequence Diagram

sequenceDiagram
    participant Client
    participant EnrollRoute as POST /api/agent/enroll
    participant tRPC as tRPC Mutation
    participant DenyInDemo as denyInDemo()
    participant Auth as withTeamAccess / requireSuperAdmin
    participant Handler as Procedure Handler
    participant DB as PostgreSQL

    Note over Client,DB: Demo mode ON (NEXT_PUBLIC_VF_DEMO_MODE=true)

    Client->>EnrollRoute: POST {token, hostname}
    EnrollRoute->>EnrollRoute: isDemoMode() → true
    EnrollRoute-->>Client: 403 {error: disabled in demo}

    Client->>tRPC: mutation (e.g. secret.create)
    tRPC->>DenyInDemo: isDemoMode() → true
    DenyInDemo-->>Client: FORBIDDEN

    Note over Client,DB: Demo mode OFF (normal deployment)

    Client->>EnrollRoute: POST {token, hostname}
    EnrollRoute->>EnrollRoute: checkIpRateLimit()
    EnrollRoute->>DB: environment.findMany + vectorNode.create
    EnrollRoute-->>Client: 200 {nodeId, nodeToken}

    Client->>tRPC: mutation (e.g. secret.create)
    tRPC->>DenyInDemo: isDemoMode() → false
    DenyInDemo->>Auth: pass through
    Auth->>DB: team/membership lookup
    Auth->>Handler: authorized
    Handler->>DB: write
    Handler-->>Client: result
Loading

Reviews (1): Last reviewed commit: "feat(demo): lock down identity, egress, ..." | Re-trigger Greptile

@TerrifiedBug TerrifiedBug merged commit 8052363 into main Apr 27, 2026
13 checks passed
@TerrifiedBug TerrifiedBug deleted the feat/demo-mode-backend-guards branch April 27, 2026 08:23
TerrifiedBug added a commit that referenced this pull request Apr 27, 2026
… UI (#187)

* feat(demo): add disabled badges and read-only fieldsets to demo settings UI

Backend already rejects credential and identity mutations in demo mode
(PR #185), but the UI still let users type into fields and click Save,
producing toast errors at submit time. This adds frontend signaling so
the read-only state is obvious before the user interacts.

Adds three small primitives in src/components/demo-disabled.tsx:
- DemoDisabledBadge -- inline "Demo" lock badge for card titles
- DemoDisabledNotice -- amber info banner above a section
- DemoDisabledFieldset -- wraps content in <fieldset disabled> + notice;
  passthrough outside demo mode

Integrates them across the five settings surfaces flagged in the audit
plus the per-environment enrollment token panel:
- /settings/ai (AI provider + API key)
- /settings/backup (S3 storage + manual backups + schedule)
- /settings/auth (OIDC + IdP team mappings)
- /settings/scim (SCIM provisioning + bearer token)
- /environments/[id] (Git Integration tab + Agent Enrollment tab)

For agent enrollment specifically: the Generate / Revoke buttons are
disabled in demo, the Quick Start install snippets remain visible
(now showing a clearly-fake "vf_enroll_demo_example_..." placeholder)
so the demo still demonstrates the install flow without minting real
tokens. The /api/agent/enroll endpoint already returns 403 in demo, so
the placeholder cannot be redeemed.

Components are no-ops outside demo mode (isDemoMode() short-circuits),
so non-demo deployments are unchanged.

* Update src/app/(dashboard)/settings/_components/backup-settings.tsx

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* fix(demo): repair backup-settings JSX after malformed Greptile suggestion

The Greptile suggestion accepted on PR #187 dropped the function's
`return (` wrapper and the outer `<div>` while leaving two opening
`<DemoDisabledFieldset>` tags, producing a JSX parse error in CI.

Restores the return wrapper and consolidates to a single
`<DemoDisabledFieldset message=...>` (which already renders the notice
internally via its `message` prop, matching the suggestion's intent).
Also drops the now-unused DemoDisabledFieldset / DemoDisabledNotice
imports flagged by no-unused-vars.

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant