Skip to content

Commit e6abefc

Browse files
committed
Standardize UUID validation across all REST API controllers
Extract a shared validate_uuid/1 function into LightningWeb.API.Helpers using the existing Ecto.UUID.dump/1 pattern. Replace private duplicate implementations in WorkflowsController and CredentialController. Apply validation to all controllers that accept UUID path or query params, so malformed UUIDs return 400 instead of raising Ecto.Query.CastError (500). Closes #4588
1 parent 3fe81d8 commit e6abefc

16 files changed

Lines changed: 129 additions & 29 deletions

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ and this project adheres to
4848
[PR#4551](https://github.com/OpenFn/lightning/pull/4551)
4949
- Fix AI assistant authorization for support users on projects with support
5050
access enabled [#4571](https://github.com/OpenFn/lightning/issues/4571)
51+
- REST API controllers now return 400 instead of 500 for malformed UUID
52+
parameters. Extracts a shared `validate_uuid/1` helper used across all API
53+
controllers. [#4588](https://github.com/OpenFn/lightning/issues/4588)
5154

5255
## [2.16.0] - 2026-03-24
5356

lib/lightning_web/controllers/api/ai_assistant_controller.ex

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ defmodule LightningWeb.API.AiAssistantController do
99
alias Lightning.Policies.Permissions
1010
alias Lightning.Projects
1111
alias Lightning.Workflows
12+
alias LightningWeb.API.Helpers
1213
alias LightningWeb.Channels.AiAssistantJSON
1314

1415
action_fallback LightningWeb.FallbackController
@@ -75,17 +76,21 @@ defmodule LightningWeb.API.AiAssistantController do
7576
end
7677

7778
defp get_resource("job_code", %{"job_id" => job_id}) do
78-
{:ok, job_id}
79+
with :ok <- Helpers.validate_uuid(job_id) do
80+
{:ok, job_id}
81+
end
7982
end
8083

8184
defp get_resource("job_code", _params) do
8285
{:error, :bad_request}
8386
end
8487

8588
defp get_resource("workflow_template", %{"project_id" => project_id}) do
86-
case Projects.get_project(project_id) do
87-
nil -> {:error, :not_found}
88-
project -> {:ok, project}
89+
with :ok <- Helpers.validate_uuid(project_id) do
90+
case Projects.get_project(project_id) do
91+
nil -> {:error, :not_found}
92+
project -> {:ok, project}
93+
end
8994
end
9095
end
9196

lib/lightning_web/controllers/api/credential_controller.ex

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ defmodule LightningWeb.API.CredentialController do
2525
alias Lightning.Policies.Permissions
2626
alias Lightning.Policies.ProjectUsers
2727
alias Lightning.Projects
28+
alias LightningWeb.API.Helpers
2829

2930
action_fallback LightningWeb.FallbackController
3031

@@ -61,7 +62,8 @@ defmodule LightningWeb.API.CredentialController do
6162
def index(conn, %{"project_id" => project_id}) do
6263
current_user = conn.assigns.current_resource
6364

64-
with project when not is_nil(project) <- Projects.get_project(project_id),
65+
with :ok <- Helpers.validate_uuid(project_id),
66+
project when not is_nil(project) <- Projects.get_project(project_id),
6567
:ok <-
6668
ProjectUsers
6769
|> Permissions.can(
@@ -166,15 +168,15 @@ defmodule LightningWeb.API.CredentialController do
166168
def delete(conn, %{"id" => id}) do
167169
current_user = conn.assigns.current_resource
168170

169-
with :ok <- validate_uuid(id),
171+
with :ok <- Helpers.validate_uuid(id),
170172
credential when not is_nil(credential) <-
171173
Credentials.get_credential(id),
172174
:ok <- validate_credential_ownership(credential, current_user),
173175
{:ok, _} <- Credentials.delete_credential(credential) do
174176
send_resp(conn, :no_content, "")
175177
else
176-
{:error, :invalid_uuid} ->
177-
{:error, :not_found}
178+
{:error, :bad_request} ->
179+
{:error, :bad_request}
178180

179181
nil ->
180182
{:error, :not_found}
@@ -187,13 +189,6 @@ defmodule LightningWeb.API.CredentialController do
187189
end
188190
end
189191

190-
defp validate_uuid(id) do
191-
case Ecto.UUID.dump(to_string(id)) do
192-
{:ok, _bin} -> :ok
193-
:error -> {:error, :invalid_uuid}
194-
end
195-
end
196-
197192
defp validate_credential_ownership(credential, current_user) do
198193
if credential.user_id == current_user.id do
199194
:ok

lib/lightning_web/controllers/api/helpers.ex

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,20 @@ defmodule LightningWeb.API.Helpers do
5151
}
5252
|> URI.to_string()
5353
end
54+
55+
@doc """
56+
Validates that the given value is a well-formed UUID.
57+
58+
Returns `:ok` on success or `{:error, :bad_request}` when the value
59+
cannot be parsed as a UUID. Use this in API controllers before passing
60+
an ID to the database layer, which would raise `Ecto.Query.CastError`
61+
for invalid values.
62+
"""
63+
@spec validate_uuid(any()) :: :ok | {:error, :bad_request}
64+
def validate_uuid(id) do
65+
case Ecto.UUID.dump(to_string(id)) do
66+
{:ok, _bin} -> :ok
67+
:error -> {:error, :bad_request}
68+
end
69+
end
5470
end

lib/lightning_web/controllers/api/job_controller.ex

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ defmodule LightningWeb.API.JobController do
2424
alias Lightning.Policies.Permissions
2525
alias Lightning.Policies.ProjectUsers
2626
alias Lightning.Workflows
27+
alias LightningWeb.API.Helpers
2728

2829
action_fallback LightningWeb.FallbackController
2930

@@ -64,7 +65,8 @@ defmodule LightningWeb.API.JobController do
6465
def index(conn, %{"project_id" => project_id} = params) do
6566
pagination_attrs = Map.take(params, ["page_size", "page"])
6667

67-
with project <- Lightning.Projects.get_project(project_id),
68+
with :ok <- Helpers.validate_uuid(project_id),
69+
project <- Lightning.Projects.get_project(project_id),
6870
:ok <-
6971
ProjectUsers
7072
|> Permissions.can(
@@ -115,7 +117,8 @@ defmodule LightningWeb.API.JobController do
115117
"""
116118
@spec show(Plug.Conn.t(), map()) :: Plug.Conn.t()
117119
def show(conn, %{"id" => id}) do
118-
with job <- Jobs.get_job!(id),
120+
with :ok <- Helpers.validate_uuid(id),
121+
job <- Jobs.get_job!(id),
119122
job_with_project <- Lightning.Repo.preload(job, workflow: :project),
120123
:ok <-
121124
ProjectUsers

lib/lightning_web/controllers/api/project_controller.ex

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ defmodule LightningWeb.API.ProjectController do
2020
alias Lightning.Policies.Permissions
2121
alias Lightning.Policies.ProjectUsers
2222
alias Lightning.Projects
23+
alias LightningWeb.API.Helpers
2324

2425
action_fallback LightningWeb.FallbackController
2526

@@ -78,7 +79,8 @@ defmodule LightningWeb.API.ProjectController do
7879
"""
7980
@spec show(Plug.Conn.t(), map()) :: Plug.Conn.t()
8081
def show(conn, %{"id" => id}) do
81-
with project <- Projects.get_project(id),
82+
with :ok <- Helpers.validate_uuid(id),
83+
project <- Projects.get_project(id),
8284
:ok <-
8385
ProjectUsers
8486
|> Permissions.can(

lib/lightning_web/controllers/api/provisioning_controller.ex

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ defmodule LightningWeb.API.ProvisioningController do
2020
alias Lightning.Projects.Provisioner
2121
alias Lightning.Workflows
2222
alias Lightning.WorkflowVersions
23+
alias LightningWeb.API.Helpers
2324

2425
action_fallback(LightningWeb.FallbackController)
2526

@@ -129,7 +130,8 @@ defmodule LightningWeb.API.ProvisioningController do
129130
"""
130131
@spec show(Plug.Conn.t(), map()) :: Plug.Conn.t()
131132
def show(conn, params) do
132-
with project = %Project{} <-
133+
with :ok <- Helpers.validate_uuid(params["id"]),
134+
project = %Project{} <-
133135
Projects.get_project(params["id"]) || {:error, :not_found},
134136
:ok <-
135137
Permissions.can(
@@ -186,7 +188,8 @@ defmodule LightningWeb.API.ProvisioningController do
186188
"""
187189
@spec show_yaml(Plug.Conn.t(), map()) :: Plug.Conn.t()
188190
def show_yaml(conn, %{"id" => id} = params) do
189-
with %Projects.Project{} = project <-
191+
with :ok <- Helpers.validate_uuid(id),
192+
%Projects.Project{} = project <-
190193
Projects.get_project(id) || {:error, :not_found},
191194
:ok <-
192195
Permissions.can(

lib/lightning_web/controllers/api/run_controller.ex

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ defmodule LightningWeb.API.RunController do
2525
alias Lightning.Policies.Permissions
2626
alias Lightning.Policies.ProjectUsers
2727
alias Lightning.Runs
28+
alias LightningWeb.API.Helpers
2829

2930
action_fallback LightningWeb.FallbackController
3031

@@ -72,7 +73,8 @@ defmodule LightningWeb.API.RunController do
7273
def index(conn, %{"project_id" => project_id} = params) do
7374
pagination_attrs = Map.take(params, ["page_size", "page"])
7475

75-
with :ok <-
76+
with :ok <- Helpers.validate_uuid(project_id),
77+
:ok <-
7678
Invocation.Query.validate_datetime_params(params, [
7779
"inserted_after",
7880
"inserted_before",
@@ -140,7 +142,8 @@ defmodule LightningWeb.API.RunController do
140142
"""
141143
@spec show(Plug.Conn.t(), map()) :: Plug.Conn.t()
142144
def show(conn, %{"id" => id}) do
143-
with run <- Runs.get(id, include: [work_order: [workflow: :project]]),
145+
with :ok <- Helpers.validate_uuid(id),
146+
run <- Runs.get(id, include: [work_order: [workflow: :project]]),
144147
:ok <-
145148
ProjectUsers
146149
|> Permissions.can(

lib/lightning_web/controllers/api/work_orders_controller.ex

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ defmodule LightningWeb.API.WorkOrdersController do
2525
alias Lightning.Policies.Permissions
2626
alias Lightning.Policies.ProjectUsers
2727
alias Lightning.WorkOrders
28+
alias LightningWeb.API.Helpers
2829

2930
action_fallback LightningWeb.FallbackController
3031

@@ -72,7 +73,8 @@ defmodule LightningWeb.API.WorkOrdersController do
7273
def index(conn, %{"project_id" => project_id} = params) do
7374
pagination_attrs = Map.take(params, ["page_size", "page"])
7475

75-
with :ok <-
76+
with :ok <- Helpers.validate_uuid(project_id),
77+
:ok <-
7678
Invocation.Query.validate_datetime_params(params, [
7779
"inserted_after",
7880
"inserted_before",
@@ -140,7 +142,8 @@ defmodule LightningWeb.API.WorkOrdersController do
140142
"""
141143
@spec show(Plug.Conn.t(), map()) :: Plug.Conn.t()
142144
def show(conn, %{"id" => id}) do
143-
with work_order <-
145+
with :ok <- Helpers.validate_uuid(id),
146+
work_order <-
144147
WorkOrders.get(id, include: [workflow: :project, runs: []]),
145148
:ok <-
146149
ProjectUsers

lib/lightning_web/controllers/api/workflows_controller.ex

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ defmodule LightningWeb.API.WorkflowsController do
4444
alias Lightning.Workflows.Edge
4545
alias Lightning.Workflows.Presence
4646
alias Lightning.Workflows.Workflow
47+
alias LightningWeb.API.Helpers
4748
alias LightningWeb.ChangesetJSON
4849

4950
action_fallback LightningWeb.FallbackController
@@ -451,10 +452,10 @@ defmodule LightningWeb.API.WorkflowsController do
451452
end
452453
end
453454

454-
defp validate_uuid(project_id) do
455-
case Ecto.UUID.dump(to_string(project_id)) do
456-
{:ok, _bin} -> :ok
457-
:error -> {:error, :invalid_id, project_id}
455+
defp validate_uuid(id) do
456+
case Helpers.validate_uuid(id) do
457+
:ok -> :ok
458+
{:error, :bad_request} -> {:error, :invalid_id, id}
458459
end
459460
end
460461

0 commit comments

Comments
 (0)