8 Commits
Author SHA1 Message Date
Juan Denis 6400b76a7d Drop imports that shadowed module-level names
Function-local re-imports of User, WorkspaceService and secrets, and a
local list named secrets, hid the module names they duplicated.
2026-10-10 22:20:03 -04:00
Juan DenisandClaude Fable 5 6337066fad refactor(secrets): vaults + webhook gateway onto the typed-error contract
SecretVaultService, SecretService, and WebhookGatewayService raise typed
errors and return bare data; secrets_webhooks.py drops all twelve
{'success': bool} translations including its two prose sniffs. Bulk
create keeps per-item error collection (catching ApplicationError).
Replay now answers 200 with success:false when the forward target refuses
— previously a failed forward was a 404 with error:null — and the
WebhooksTab toast distinguishes the two. The public /webhooks/receive
wire protocol is intentionally unchanged (external senders depend on it).
Sniffing census 6 -> 4.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 00:00:36 -04:00
Juan DenisandClaude Opus 5 fe02ce8eeb fix(auth): resolve the caller through one identity door, not the raw JWT
Plan 76 milestone A, drift/bug row 2 — scheduled as a fix, not a refactor,
because it is a behaviour difference shipping today.

rbac.get_current_user() is the only lookup that knows about both auth
mechanisms: it returns g.api_key_user for an X-API-Key request and falls back
to the JWT. 73 call sites answered "who is calling" with
`User.query.get(get_jwt_identity())` instead, behind 19 near-identical private
helpers (12 named get_current_user(), shadowing the real door).

The audit that scheduled this row predicted those return None for API-key
callers. Measured, it is worse: auth_required() deliberately skips
verify_jwt_in_request() once the key middleware has authenticated, so there is
no JWT context and get_jwt_identity() *raises*. Nine handlers reached through
an API-key-capable policy decorator therefore returned 500 to every API-key
caller:

  bandwidth.get_app_bandwidth          @viewer_required
  servers.set_server_workspace         @developer_required
  servers.create_server                @developer_required
  sso.get_sso_config / update_provider_config / update_general_settings
  source_connections.update_{github,gitlab,bitbucket}_admin_config
  source_connections.complete_github_app_manifest
  databases.execute_{mysql,pg,sqlite,docker}_query   (see below)

Three service helpers had the same lookup inside a blanket `except Exception`,
which converted the raise into a silent user_id=None — the write landed and
the audit trail lost the actor (shared_resource_service, cf_ops_change_service,
secrets_webhooks).

The four databases.execute_*_query sites were a second admin check *inside* an
already-@admin_required route: dead code that always passed for JWT callers and
crashed for API-key ones. They now call require_admin_user() rather than
re-deriving the answer.

Also folds in a sixth _require_admin() found in the cloudflare-ops extension
source (29 gates, the same inverted contract as jobs.py), and fixes
serverkit-gui's blueprint at its sibling-repo source rather than in this repo's
live copy.

Ratchet (second-wave rule 1 — a migration without one regrows):
tests/identity_door_census.py counts both bypass shapes by AST, direct and the
two-line `uid = get_jwt_identity(); User.query.get(uid)` variant. The ceiling is
87, all of them on @jwt_required()-only routes where reading the JWT is correct
today; the ratchet's third assertion holds the API-key-capable population at
exactly zero, so migrating one of those routes to a policy decorator without
fixing its lookup now fails. Verified non-vacuous: an injected bypass fails both
assertions and passes again on revert.

The first wave's controller-boundary baseline is regenerated — this removes 74
persistence crossings (593 -> 519), a debt reduction with no new entries.

Gate: tests/test_api_key_identity.py (7, each red before the fix),
test_identity_door_ratchet.py (10), test_api_controller_boundaries.py (4), plus
the 65 test files covering every touched module: 1024 passed, 10 skipped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 19:33:57 -04:00
Juan Denis d961bd23f0 fix(api): close IDOR/authz gaps across deployment jobs, vaults, queues, docker, databases
Proactive hardening from an internal audit of the same broken-authorization
class as GHSA-6w78-q5vm-rfmh (authentication checked, app/workspace-level
authorization skipped):

- deployment_jobs: job detail/logs/retry resolve the job and gate on
  can_access_app / can_operate_app (app-less jobs: requester or admin);
  global job listing is admin-only
- secrets_webhooks: every vault/secret/webhook-endpoint/delivery by-id
  route resolves to its workspace and 404-seals non-members; secret
  reveal and signing-secret regeneration require workspace owner/admin;
  dead local admin_required placeholder removed
- shared_resources: variable-group CRUD and attach/detach gate on the
  resolved scope (workspace role / can_edit_app / can_operate_app),
  closing deploy-time env injection into foreign apps
- queue_bus: message receive/complete/fail/list on system-owned groups
  are admin-only; user-owned groups restricted to their owner
- docker: raw container inspect (exposes env vars) and container logs
  are admin-only, matching Decision 7; per-app logs stay app-gated
- databases: raw SQL query endpoints, host-level metadata reads, and
  process lists are admin-only; per-app managed database flows unchanged

Adds 4 persona-matrix regression suites (74 tests) and bumps the
test-count ratchet.
2026-08-04 08:53:27 -04:00
Juan Denis 0e29978e7b Truncate large telemetry payloads & UI robustness fixes
Telemetry: ensure sanitized payloads bigger than the 32KiB threshold are replaced with a {_truncated: True} marker before storage, and add a unit test covering that behavior.

Backend API: adjust imports in secrets_webhooks to use WorkspaceService and include User model.

Frontend: make Sidebar resilient to malformed active_workspace JSON in localStorage (try/catch), and ensure socketService.disconnect() is called when user becomes unauthenticated in NotificationsProvider to clear notifications and unread count.

Files: backend/services/telemetry_service.py, backend/tests/test_telemetry.py, backend/app/api/secrets_webhooks.py, frontend/src/components/Sidebar.jsx, frontend/src/contexts/NotificationsContext.jsx
2026-06-22 01:45:15 -04:00
Juan Denis 45f754abc0 feat(workspaces): improve workspace switching and secrets/webhooks UI
- Update workspace API and service for workspace context handling
- Refine Sidebar and WorkspaceSwitcher components
- Update sidebar navigation items for workspace-aware routing
- Enhance SecretsWebhooks page and API client integration
2026-06-21 18:32:25 -04:00
Juan DenisandClaude Opus 4.8 cb83d24f7c feat(secrets): scope vaults & webhooks to the active workspace
Vaults and webhook endpoints now carry a nullable workspace_id FK and are filtered by the active workspace context (X-Workspace-Id header or ?workspace_id), mirroring the servers/apps scoping (#33). New rows are stamped with the resolved workspace; with no context the view stays unscoped. Migration 021 adds the columns + indexes and backfills existing rows to the Default workspace.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 12:47:37 -04:00
Juan Denis bf9fee131e feat: add secrets manager and inbound webhook gateway
- Encrypted vault + secret models with Fernet encryption
- Webhook endpoint + delivery log models with HMAC verification
- SecretVaultService / WebhookGatewayService business logic
- /api/v1/vaults, /secrets, /webhooks REST endpoints (JWT + public receive)
- Migration 020_secrets_webhooks
- Frontend Secrets & Webhooks page, API methods, sidebar, styles
- Add SERVERKIT_ENCRYPTION_KEY to .env.example
2026-06-19 01:33:46 -04:00