Skip to content

Commit 516e7a6

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 08ba6e1 commit 516e7a6

16 files changed

Lines changed: 132 additions & 29 deletions

CHANGELOG.md

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

285288
## [2.16.0] - 2026-03-24
286289

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: 9 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(
@@ -72,6 +74,9 @@ defmodule LightningWeb.API.CredentialController do
7274
credentials = Credentials.list_credentials(project)
7375
render(conn, "index.json", credentials: credentials)
7476
else
77+
{:error, :bad_request} ->
78+
{:error, :bad_request}
79+
7580
nil ->
7681
{:error, :not_found}
7782

@@ -166,15 +171,15 @@ defmodule LightningWeb.API.CredentialController do
166171
def delete(conn, %{"id" => id}) do
167172
current_user = conn.assigns.current_resource
168173

169-
with :ok <- validate_uuid(id),
174+
with :ok <- Helpers.validate_uuid(id),
170175
credential when not is_nil(credential) <-
171176
Credentials.get_credential(id),
172177
:ok <- validate_credential_ownership(credential, current_user),
173178
{:ok, _} <- Credentials.delete_credential(credential) do
174179
send_resp(conn, :no_content, "")
175180
else
176-
{:error, :invalid_uuid} ->
177-
{:error, :not_found}
181+
{:error, :bad_request} ->
182+
{:error, :bad_request}
178183

179184
nil ->
180185
{:error, :not_found}
@@ -187,13 +192,6 @@ defmodule LightningWeb.API.CredentialController do
187192
end
188193
end
189194

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-
197195
defp validate_credential_ownership(credential, current_user) do
198196
if credential.user_id == current_user.id do
199197
: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
@@ -54,6 +54,7 @@ defmodule LightningWeb.API.JobController do
5454
alias Lightning.Policies.Permissions
5555
alias Lightning.Policies.ProjectUsers
5656
alias Lightning.Workflows
57+
alias LightningWeb.API.Helpers
5758

5859
action_fallback LightningWeb.FallbackController
5960

@@ -94,7 +95,8 @@ defmodule LightningWeb.API.JobController do
9495
def index(conn, %{"project_id" => project_id} = params) do
9596
pagination_attrs = Map.take(params, ["page_size", "page"])
9697

97-
with project <- Lightning.Projects.get_project(project_id),
98+
with :ok <- Helpers.validate_uuid(project_id),
99+
project <- Lightning.Projects.get_project(project_id),
98100
:ok <-
99101
ProjectUsers
100102
|> Permissions.can(
@@ -145,7 +147,8 @@ defmodule LightningWeb.API.JobController do
145147
"""
146148
@spec show(Plug.Conn.t(), map()) :: Plug.Conn.t()
147149
def show(conn, %{"id" => id}) do
148-
with {:ok, job} <- Jobs.get_job(id, include: [workflow: :project]),
150+
with :ok <- Helpers.validate_uuid(id),
151+
{:ok, job} <- Jobs.get_job(id, include: [workflow: :project]),
149152
:ok <-
150153
ProjectUsers
151154
|> Permissions.can(

lib/lightning_web/controllers/api/project_controller.ex

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ defmodule LightningWeb.API.ProjectController do
3636
alias Lightning.Policies.Permissions
3737
alias Lightning.Policies.ProjectUsers
3838
alias Lightning.Projects
39+
alias LightningWeb.API.Helpers
3940

4041
action_fallback LightningWeb.FallbackController
4142

@@ -94,7 +95,8 @@ defmodule LightningWeb.API.ProjectController do
9495
"""
9596
@spec show(Plug.Conn.t(), map()) :: Plug.Conn.t()
9697
def show(conn, %{"id" => id}) do
97-
with %Lightning.Projects.Project{} = project <-
98+
with :ok <- Helpers.validate_uuid(id),
99+
%Lightning.Projects.Project{} = project <-
98100
Projects.get_project(id),
99101
:ok <-
100102
ProjectUsers

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
@@ -56,6 +56,7 @@ defmodule LightningWeb.API.RunController do
5656
alias Lightning.Policies.Permissions
5757
alias Lightning.Policies.ProjectUsers
5858
alias Lightning.Runs
59+
alias LightningWeb.API.Helpers
5960

6061
action_fallback LightningWeb.FallbackController
6162

@@ -104,7 +105,8 @@ defmodule LightningWeb.API.RunController do
104105
def index(conn, %{"project_id" => project_id} = params) do
105106
pagination_attrs = Map.take(params, ["page_size", "page"])
106107

107-
with :ok <-
108+
with :ok <- Helpers.validate_uuid(project_id),
109+
:ok <-
108110
Invocation.Query.validate_datetime_params(params, [
109111
"inserted_after",
110112
"inserted_before",
@@ -174,7 +176,8 @@ defmodule LightningWeb.API.RunController do
174176
"""
175177
@spec show(Plug.Conn.t(), map()) :: Plug.Conn.t()
176178
def show(conn, %{"id" => id}) do
177-
with %Lightning.Run{} = run <-
179+
with :ok <- Helpers.validate_uuid(id),
180+
%Lightning.Run{} = run <-
178181
Runs.get(id, include: [work_order: [workflow: :project]]),
179182
:ok <-
180183
ProjectUsers

lib/lightning_web/controllers/api/work_orders_controller.ex

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ defmodule LightningWeb.API.WorkOrdersController do
6363
alias Lightning.Policies.Permissions
6464
alias Lightning.Policies.ProjectUsers
6565
alias Lightning.WorkOrders
66+
alias LightningWeb.API.Helpers
6667

6768
action_fallback LightningWeb.FallbackController
6869

@@ -111,7 +112,8 @@ defmodule LightningWeb.API.WorkOrdersController do
111112
def index(conn, %{"project_id" => project_id} = params) do
112113
pagination_attrs = Map.take(params, ["page_size", "page"])
113114

114-
with :ok <-
115+
with :ok <- Helpers.validate_uuid(project_id),
116+
:ok <-
115117
Invocation.Query.validate_datetime_params(params, [
116118
"inserted_after",
117119
"inserted_before",
@@ -181,7 +183,8 @@ defmodule LightningWeb.API.WorkOrdersController do
181183
"""
182184
@spec show(Plug.Conn.t(), map()) :: Plug.Conn.t()
183185
def show(conn, %{"id" => id}) do
184-
with %Lightning.WorkOrder{} = work_order <-
186+
with :ok <- Helpers.validate_uuid(id),
187+
%Lightning.WorkOrder{} = work_order <-
185188
WorkOrders.get(id, include: [workflow: :project, runs: []]),
186189
:ok <-
187190
ProjectUsers

lib/lightning_web/controllers/api/workflows_controller.ex

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ defmodule LightningWeb.API.WorkflowsController do
9292
alias Lightning.Workflows.Edge
9393
alias Lightning.Workflows.Presence
9494
alias Lightning.Workflows.Workflow
95+
alias LightningWeb.API.Helpers
9596
alias LightningWeb.ChangesetJSON
9697

9798
action_fallback LightningWeb.FallbackController
@@ -499,10 +500,10 @@ defmodule LightningWeb.API.WorkflowsController do
499500
end
500501
end
501502

502-
defp validate_uuid(project_id) do
503-
case Ecto.UUID.dump(to_string(project_id)) do
504-
{:ok, _bin} -> :ok
505-
:error -> {:error, :invalid_id, project_id}
503+
defp validate_uuid(id) do
504+
case Helpers.validate_uuid(id) do
505+
:ok -> :ok
506+
{:error, :bad_request} -> {:error, :invalid_id, id}
506507
end
507508
end
508509

0 commit comments

Comments
 (0)