fix(workflows): harden workflow API auth routes#3559
fix(workflows): harden workflow API auth routes#3559PlaneInABottle wants to merge 34 commits intosimstudioai:stagingfrom
Conversation
PR SummaryMedium Risk Overview Hardens API-key behavior and auditing: workspace-scoped API keys now return Improves deploy/activation/revert robustness by treating MCP sync, webhook cleanup, and MCP tool cleanup failures as non-fatal (log-and-continue) after the primary operation succeeds; adds Adds/updates targeted Vitest coverage for the new middleware and the updated auth/audit paths, and clarifies docs that lifecycle/deployment endpoints can be used programmatically with session auth or API keys where supported. Written by Cursor Bugbot for commit 7fdff82. This will update automatically on new commits. Configure here. |
|
Someone is attempting to deploy a commit to the Sim Team on Vercel. A member of the Team first needs to authorize it. |
Greptile SummaryThis PR hardens the workflow API auth layer by introducing a shared Key observations:
Confidence Score: 3/5
Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Incoming Request] --> B{Auth Type?}
B -->|Bearer JWT| C[verifyInternalToken\ndeployed/route only]
C -->|valid| D[Skip validateWorkflowAccess\nload deployed state directly]
C -->|invalid| E[validateWorkflowAccess]
B -->|Session / API Key / Internal Secret| E
E --> F[getWorkflowById]
F -->|not found| G[404 Workflow not found]
F -->|found, no workspaceId| H[403 Personal workflow deprecated]
F -->|requireDeployment: false| I[checkHybridAuth]
I -->|fail / no userId| J[401 Unauthorized]
I -->|success + userId| K[authorizeWorkflowByWorkspacePermission\naction: read / write / admin]
K -->|denied| L[403 / 404 Access denied]
K -->|allowed| M[Return workflow + auth]
F -->|requireDeployment: true| N{isDeployed?}
N -->|false| O[403 Workflow not deployed]
N -->|true| P{Internal Secret header?}
P -->|valid + allowInternalSecret| Q[Return workflow — no userId]
P -->|missing / invalid| R[Check X-Api-Key header]
R -->|missing| S[401 API key required]
R -->|present| T[authenticateApiKeyFromHeader\nworkspace or personal]
T -->|fail| U[401 Invalid API key]
T -->|success| V[updateApiKeyLastUsed\nReturn workflow]
M --> W[Route handler\nDELETE / PUT / POST / PATCH / GET]
V --> W
Q --> W
|
888a246 to
aab58cb
Compare
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
|
@PlaneInABottle this PR might have to wait till our v0.6 release. Lot of auth changes there that might overwrite this potentially |
|
@PlaneInABottle can you rebase against staging |
Co-authored-by: GitHub Copilot <github-copilot[bot]@users.noreply.github.com>
Co-authored-by: GitHub Copilot <github-copilot[bot]@users.noreply.github.com>
Co-authored-by: GitHub Copilot <github-copilot[bot]@users.noreply.github.com>
Use the shared audit actor helper consistently so workflow deletion matches deploy behavior and remove the redundant deploy wrapper raised in review.
Call validateWorkflowAccess directly in workflow deployment lifecycle routes and clean up the related test helper formatting raised in review.
a264ee9 to
2c267c0
Compare
ce2ed79 to
1f61db8
Compare
| logger.warn(`[${requestId}] Unable to resolve actor user for deployment activation: ${id}`) | ||
| return createErrorResponse('Unable to determine activating user', 400) | ||
| } | ||
| const actorUserId = auth?.userId |
There was a problem hiding this comment.
Non-null assertions on potentially undefined actorUserId variable
Low Severity
actorUserId is assigned via auth?.userId (making it string | undefined) then used with non-null assertions (actorUserId!) at multiple call sites. While the earlier guard if (!auth?.userId) return ensures it's defined at runtime, TypeScript can't narrow across the two separate if (isActive) blocks. Extracting the validated actorUserId once after the guard or using a definite assignment would eliminate all the ! assertions and make the code resilient to future refactoring of the guard.
Additional Locations (2)
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
| parallels: data.parallels, | ||
| variables: data.variables, | ||
| } | ||
| } catch (error) { |
There was a problem hiding this comment.
Removed graceful null fallback for deployed state loading
Medium Severity
The previous code wrapped loadDeployedWorkflowState in a try-catch that returned deployedState: null inside a 200 response on failure. The new code removed that inner try-catch, so any throw from loadDeployedWorkflowState (e.g., no active deployment, race condition during undeploy) now propagates to the outer catch and returns a 500 error. Internal callers authenticated via Bearer token bypass the validateWorkflowAccess check entirely and go straight to loadDeployedWorkflowState, so they lose the graceful null-state fallback and will now receive 500 errors in cases that previously succeeded.


Summary
What changed
stagingname/emailinto deploy, activate, and revert audit recordsValidation