Add extra validations for endpoints that do not use principal to get the resources#2112
Add extra validations for endpoints that do not use principal to get the resources#2112MarceloRGonc merged 10 commits intomainfrom
Conversation
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
| flowVersion: FlowVersion, | ||
| projectId: string, | ||
| ): Promise<void> { | ||
| const flow = await flowService.getOne({ |
There was a problem hiding this comment.
hm so, if the flowservice does not return a flow for the version it explicitly means that the flow version does not belong to the project?
There was a problem hiding this comment.
I didn't add logic; I just moved the method and renamed it. But I think it makes sense that we have a FlowVersion object with a flowId if we search the DB for the flowId, and a projectId. If we don't have any results, we can assume it's because of the project. It's debatable, but it's not new code.
There was a problem hiding this comment.
Ahh gotcha, i missed the collapsed deleted part
packages/server/api/src/app/flows/flow/flow-version.controller.ts
Outdated
Show resolved
Hide resolved
|
There was a problem hiding this comment.
Pull request overview
This PR strengthens project-scoped authorization for flow-related endpoints that previously fetched resources without constraining them to the principal’s project, aligning error handling with the global ApplicationError -> { code, params } response shape.
Changes:
- Introduces shared flow/flow-version project ownership assertions (
assertFlowBelongsToProject,assertFlowVersionBelongsToProject) and replaces ad-hoc validation/reply logic. - Adds project scoping to endpoints that were missing it (form retrieval by
flowId, flow version test-output routes, flow-run retry). - Updates/introduces unit + integration tests to validate the new authorization/error response behavior.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/server/api/src/app/flows/common/flow-validations.ts | Adds shared ownership assertion helpers that throw ApplicationError for consistent error handling. |
| packages/server/api/src/app/flows/common/flow-version-validation.ts | Removes old reply-based validation helper. |
| packages/server/api/src/app/flows/test/test.controller.ts | Switches to assertion-based validation for test endpoints. |
| packages/server/api/src/app/flows/flow/form/form.service.ts | Adds projectId parameter and asserts flow ownership before returning form config. |
| packages/server/api/src/app/flows/flow/form/form.controller.ts | Passes principal.projectId into form service to enable ownership validation. |
| packages/server/api/src/app/flows/flow/flow-version.controller.ts | Adds ownership assertions for additional routes and relies on global ApplicationError handler. |
| packages/server/api/src/app/flows/flow-run/flow-run-service.ts | Requires projectId for retry and scopes flow-run lookup accordingly. |
| packages/server/api/src/app/flows/flow-run/flow-run-controller.ts | Passes principal.projectId into retry call. |
| packages/server/api/src/app/flows/flow/flow.controller.ts | Updates delete-flow permission to Permission.DELETE_FLOW. |
| packages/server/api/test/unit/flow/flow-validations.test.ts | Adds unit tests for the new assertion helpers. |
| packages/server/api/test/integration/ce/flows/flow-version.test.ts | Updates integration assertions to match { code, params } error responses. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| expect(() => assertFlowBelongsToProject(flow, projectId)).toThrow( | ||
| expect.objectContaining({ | ||
| error: expect.objectContaining({ | ||
| code: ErrorCode.AUTHORIZATION, |
There was a problem hiding this comment.
Similarly here, toThrow(expect.objectContaining(...)) is unlikely to validate the thrown ApplicationError.error contents (it mostly matches the error message/type). Use toThrow(new ApplicationError(...)) or wrap the call and assert with toMatchObject on the caught error to ensure the code/params are actually checked.
| } | ||
|
|
||
| assertFlowBelongsToProject(flow, projectId); | ||
| logger.info(flow.version.trigger.settings); |
There was a problem hiding this comment.
logger.info(flow.version.trigger.settings) logs the full trigger settings on every form fetch. This can be noisy in production and may log user-supplied form configuration. Consider removing this log or downgrading it to debug/trace with a minimal, non-sensitive summary (e.g., flowId/triggerName).
| logger.info(flow.version.trigger.settings); | |
| logger.debug( | |
| { flowId: flow.id, triggerName: flow.version.trigger.settings.triggerName }, | |
| 'Fetched form trigger settings', | |
| ); |
| const flowVersion = await flowVersionService.getOneOrThrow(flowVersionId); | ||
|
|
||
| const isValid = await validateFlowVersionBelongsToProject( | ||
| flowVersion, | ||
| projectId, | ||
| reply, | ||
| ); | ||
| await assertFlowVersionBelongsToProject(flowVersion, projectId); | ||
|
|
There was a problem hiding this comment.
assertFlowVersionBelongsToProject throws an ApplicationError (mapped to 403 by the global error handler). Because this call is inside the surrounding try/catch, that ApplicationError will be caught and turned into a 400 later in the handler. Update the catch block to re-throw ApplicationError (or move this call outside the try) so auth failures keep the correct status code/shape.
| if (flowVersion.flowId !== updateData.flowId) { | ||
| await reply.status(StatusCodes.BAD_REQUEST).send({ | ||
| success: false, | ||
| message: | ||
| 'It is not possible to update the flowId of a flow version', | ||
| }); | ||
| } |
There was a problem hiding this comment.
After sending an error response (e.g., when flowVersion.flowId !== updateData.flowId), the handler doesn't return, so execution continues and may attempt to send additional responses (locked/conflict/success), causing "Reply was already sent" or unintended updates. Add return after each reply.status(...).send(...) in these validation branches (or restructure with else).
| ).rejects.toThrow( | ||
| expect.objectContaining({ | ||
| error: expect.objectContaining({ | ||
| code: ErrorCode.AUTHORIZATION, | ||
| params: { | ||
| message: | ||
| 'The flow and version are not associated with the project', | ||
| }, | ||
| }), | ||
| }), | ||
| ); |
There was a problem hiding this comment.
These assertions use rejects.toThrow(expect.objectContaining(...)). toThrow primarily matches against the thrown error’s message/class, so this pattern is brittle and may not assert the ApplicationError.error payload as intended. Prefer rejects.toMatchObject({ error: { code: ..., params: ... } }) or rejects.toThrow(new ApplicationError(...)) (as used in other tests).
| ).rejects.toThrow( | |
| expect.objectContaining({ | |
| error: expect.objectContaining({ | |
| code: ErrorCode.AUTHORIZATION, | |
| params: { | |
| message: | |
| 'The flow and version are not associated with the project', | |
| }, | |
| }), | |
| }), | |
| ); | |
| ).rejects.toMatchObject({ | |
| error: { | |
| code: ErrorCode.AUTHORIZATION, | |
| params: { | |
| message: | |
| 'The flow and version are not associated with the project', | |
| }, | |
| }, | |
| }); |



Fixes OPS-3895.