fix: M1 — prevent cross-org resource existence probing via scoped fetch

Use Repo.get_by(schema, id: id, organization_id: organization_id) instead of
Repo.get + pattern match so that resources from wrong orgs return :not_found
instead of :forbidden, preventing org membership discovery.
This commit is contained in:
Graham McIntire 2026-05-12 13:44:19 -05:00
parent c473f472c7
commit 095c5d3236
14 changed files with 42 additions and 192 deletions

11
bugs.md
View file

@ -59,17 +59,6 @@
## MEDIUM
### M1. Resource Scoping Reveals Org Membership
**File:** `lib/towerops_web/scoped_resource.ex:15-20`
**Severity:** MEDIUM — Cross-org resource existence probing
**Description:** Returns `:forbidden` for wrong-org resources vs `:not_found` for nonexistent. Organization A can probe if a specific resource ID belongs to Organization B.
**Fix:** Return `:not_found` in both cases — use `Repo.get_by(schema, id: id, organization_id: organization_id)`.
---
### M4. Stripe Webhook Signature Not Enforced at Plug Level

View file

@ -110,11 +110,6 @@ defmodule ToweropsWeb.Api.V1.ChecksController do
{:ok, check} ->
json(conn, format_check(check))
{:error, :forbidden} ->
conn
|> put_status(:forbidden)
|> json(%{error: "Access denied to this check"})
{:error, :not_found} ->
conn
|> put_status(:not_found)
@ -142,11 +137,6 @@ defmodule ToweropsWeb.Api.V1.ChecksController do
|> json(%{errors: translate_errors(changeset)})
end
{:error, :forbidden} ->
conn
|> put_status(:forbidden)
|> json(%{error: "Access denied to this check"})
{:error, :not_found} ->
conn
|> put_status(:not_found)
@ -180,11 +170,6 @@ defmodule ToweropsWeb.Api.V1.ChecksController do
|> json(%{errors: translate_errors(changeset)})
end
{:error, :forbidden} ->
conn
|> put_status(:forbidden)
|> json(%{error: "Access denied to this check"})
{:error, :not_found} ->
conn
|> put_status(:not_found)

View file

@ -106,7 +106,7 @@ defmodule ToweropsWeb.Api.V1.DevicesController do
{:error, reason} ->
conn
|> put_status(:forbidden)
|> put_status(:not_found)
|> json(%{error: reason})
end
end
@ -144,11 +144,6 @@ defmodule ToweropsWeb.Api.V1.DevicesController do
{:ok, device} ->
json(conn, format_device_details(device))
{:error, :forbidden} ->
conn
|> put_status(:forbidden)
|> json(%{error: "Access denied to this device"})
{:error, :not_found} ->
conn
|> put_status(:not_found)
@ -192,11 +187,6 @@ defmodule ToweropsWeb.Api.V1.DevicesController do
|> json(%{errors: translate_errors(changeset)})
end
{:error, :forbidden} ->
conn
|> put_status(:forbidden)
|> json(%{error: "Access denied to this device"})
{:error, :not_found} ->
conn
|> put_status(:not_found)
@ -235,11 +225,6 @@ defmodule ToweropsWeb.Api.V1.DevicesController do
|> json(%{errors: translate_errors(changeset)})
end
{:error, :forbidden} ->
conn
|> put_status(:forbidden)
|> json(%{error: "Access denied to this device"})
{:error, :not_found} ->
conn
|> put_status(:not_found)
@ -280,7 +265,6 @@ defmodule ToweropsWeb.Api.V1.DevicesController do
defp verify_site_access(site_id, organization_id) do
case ScopedResource.fetch(Site, site_id, organization_id) do
{:ok, _site} -> :ok
{:error, :forbidden} -> {:error, "Access denied to this site"}
{:error, :not_found} -> {:error, "Site not found"}
end
end

View file

@ -67,11 +67,6 @@ defmodule ToweropsWeb.Api.V1.EscalationPoliciesController do
{:ok, policy} ->
json(conn, format_policy_detail(policy))
{:error, :forbidden} ->
conn
|> put_status(:forbidden)
|> json(%{error: "Access denied to this escalation policy"})
{:error, :not_found} ->
conn
|> put_status(:not_found)
@ -95,11 +90,6 @@ defmodule ToweropsWeb.Api.V1.EscalationPoliciesController do
|> json(%{errors: translate_errors(changeset)})
end
{:error, :forbidden} ->
conn
|> put_status(:forbidden)
|> json(%{error: "Access denied to this escalation policy"})
{:error, :not_found} ->
conn
|> put_status(:not_found)
@ -129,11 +119,6 @@ defmodule ToweropsWeb.Api.V1.EscalationPoliciesController do
|> json(%{errors: translate_errors(changeset)})
end
{:error, :forbidden} ->
conn
|> put_status(:forbidden)
|> json(%{error: "Access denied to this escalation policy"})
{:error, :not_found} ->
conn
|> put_status(:not_found)
@ -163,11 +148,6 @@ defmodule ToweropsWeb.Api.V1.EscalationPoliciesController do
|> json(%{errors: translate_errors(changeset)})
end
{:error, :forbidden} ->
conn
|> put_status(:forbidden)
|> json(%{error: "Access denied to this escalation policy"})
{:error, :not_found} ->
conn
|> put_status(:not_found)
@ -191,9 +171,6 @@ defmodule ToweropsWeb.Api.V1.EscalationPoliciesController do
|> json(%{errors: translate_errors(changeset)})
end
else
{:error, :forbidden} ->
conn |> put_status(:forbidden) |> json(%{error: "Access denied to this escalation policy"})
{:error, :not_found} ->
conn |> put_status(:not_found) |> json(%{error: "Escalation policy not found"})
@ -211,9 +188,6 @@ defmodule ToweropsWeb.Api.V1.EscalationPoliciesController do
{:ok, _} = OnCall.delete_escalation_rule(rule)
json(conn, %{success: true})
else
{:error, :forbidden} ->
conn |> put_status(:forbidden) |> json(%{error: "Access denied to this escalation policy"})
{:error, :not_found} ->
conn |> put_status(:not_found) |> json(%{error: "Escalation policy not found"})
@ -244,9 +218,6 @@ defmodule ToweropsWeb.Api.V1.EscalationPoliciesController do
|> json(%{errors: translate_errors(changeset)})
end
else
{:error, :forbidden} ->
conn |> put_status(:forbidden) |> json(%{error: "Access denied to this escalation policy"})
{:error, :not_found} ->
conn |> put_status(:not_found) |> json(%{error: "Escalation policy not found"})
@ -273,9 +244,6 @@ defmodule ToweropsWeb.Api.V1.EscalationPoliciesController do
conn |> put_status(:internal_server_error) |> json(%{error: "Failed to delete escalation target"})
end
else
{:error, :forbidden} ->
conn |> put_status(:forbidden) |> json(%{error: "Access denied to this escalation policy"})
{:error, :not_found} ->
conn |> put_status(:not_found) |> json(%{error: "Escalation policy not found"})

View file

@ -71,11 +71,6 @@ defmodule ToweropsWeb.Api.V1.MaintenanceWindowsController do
{:ok, window} ->
json(conn, format_window(window))
{:error, :forbidden} ->
conn
|> put_status(:forbidden)
|> json(%{error: "Access denied to this maintenance window"})
{:error, :not_found} ->
conn
|> put_status(:not_found)
@ -99,11 +94,6 @@ defmodule ToweropsWeb.Api.V1.MaintenanceWindowsController do
|> json(%{errors: translate_errors(changeset)})
end
{:error, :forbidden} ->
conn
|> put_status(:forbidden)
|> json(%{error: "Access denied to this maintenance window"})
{:error, :not_found} ->
conn
|> put_status(:not_found)
@ -133,11 +123,6 @@ defmodule ToweropsWeb.Api.V1.MaintenanceWindowsController do
|> json(%{errors: translate_errors(changeset)})
end
{:error, :forbidden} ->
conn
|> put_status(:forbidden)
|> json(%{error: "Access denied to this maintenance window"})
{:error, :not_found} ->
conn
|> put_status(:not_found)

View file

@ -68,11 +68,6 @@ defmodule ToweropsWeb.Api.V1.SchedulesController do
{:ok, schedule} ->
json(conn, format_schedule_detail(schedule))
{:error, :forbidden} ->
conn
|> put_status(:forbidden)
|> json(%{error: "Access denied to this schedule"})
{:error, :not_found} ->
conn
|> put_status(:not_found)
@ -96,11 +91,6 @@ defmodule ToweropsWeb.Api.V1.SchedulesController do
|> json(%{errors: translate_errors(changeset)})
end
{:error, :forbidden} ->
conn
|> put_status(:forbidden)
|> json(%{error: "Access denied to this schedule"})
{:error, :not_found} ->
conn
|> put_status(:not_found)
@ -130,11 +120,6 @@ defmodule ToweropsWeb.Api.V1.SchedulesController do
|> json(%{errors: translate_errors(changeset)})
end
{:error, :forbidden} ->
conn
|> put_status(:forbidden)
|> json(%{error: "Access denied to this schedule"})
{:error, :not_found} ->
conn
|> put_status(:not_found)
@ -153,11 +138,6 @@ defmodule ToweropsWeb.Api.V1.SchedulesController do
user = OnCall.who_is_on_call(id)
json(conn, %{on_call: format_user(user)})
{:error, :forbidden} ->
conn
|> put_status(:forbidden)
|> json(%{error: "Access denied to this schedule"})
{:error, :not_found} ->
conn
|> put_status(:not_found)
@ -187,11 +167,6 @@ defmodule ToweropsWeb.Api.V1.SchedulesController do
|> json(%{errors: translate_errors(changeset)})
end
{:error, :forbidden} ->
conn
|> put_status(:forbidden)
|> json(%{error: "Access denied to this schedule"})
{:error, :not_found} ->
conn
|> put_status(:not_found)
@ -215,9 +190,6 @@ defmodule ToweropsWeb.Api.V1.SchedulesController do
|> json(%{errors: translate_errors(changeset)})
end
else
{:error, :forbidden} ->
conn |> put_status(:forbidden) |> json(%{error: "Access denied to this schedule"})
{:error, :not_found} ->
conn |> put_status(:not_found) |> json(%{error: "Schedule not found"})
@ -235,9 +207,6 @@ defmodule ToweropsWeb.Api.V1.SchedulesController do
{:ok, _} = OnCall.delete_layer(layer)
json(conn, %{success: true})
else
{:error, :forbidden} ->
conn |> put_status(:forbidden) |> json(%{error: "Access denied to this schedule"})
{:error, :not_found} ->
conn |> put_status(:not_found) |> json(%{error: "Schedule not found"})
@ -289,9 +258,6 @@ defmodule ToweropsWeb.Api.V1.SchedulesController do
{:ok, _} = OnCall.remove_layer_member(member)
json(conn, %{success: true})
else
{:error, :forbidden} ->
conn |> put_status(:forbidden) |> json(%{error: "Access denied to this schedule"})
{:error, :not_found} ->
conn |> put_status(:not_found) |> json(%{error: "Schedule not found"})
@ -325,9 +291,6 @@ defmodule ToweropsWeb.Api.V1.SchedulesController do
|> json(%{errors: translate_errors(changeset)})
end
{:error, :forbidden} ->
conn |> put_status(:forbidden) |> json(%{error: "Access denied to this schedule"})
{:error, :not_found} ->
conn |> put_status(:not_found) |> json(%{error: "Schedule not found"})
end
@ -348,9 +311,6 @@ defmodule ToweropsWeb.Api.V1.SchedulesController do
conn |> put_status(:internal_server_error) |> json(%{error: "Failed to delete override"})
end
else
{:error, :forbidden} ->
conn |> put_status(:forbidden) |> json(%{error: "Access denied to this schedule"})
{:error, :not_found} ->
conn |> put_status(:not_found) |> json(%{error: "Schedule not found"})

View file

@ -87,11 +87,6 @@ defmodule ToweropsWeb.Api.V1.SitesController do
{:ok, site} ->
json(conn, format_site(site))
{:error, :forbidden} ->
conn
|> put_status(:forbidden)
|> json(%{error: "Access denied to this site"})
{:error, :not_found} ->
conn
|> put_status(:not_found)
@ -119,11 +114,6 @@ defmodule ToweropsWeb.Api.V1.SitesController do
|> json(%{errors: translate_errors(changeset)})
end
{:error, :forbidden} ->
conn
|> put_status(:forbidden)
|> json(%{error: "Access denied to this site"})
{:error, :not_found} ->
conn
|> put_status(:not_found)
@ -157,11 +147,6 @@ defmodule ToweropsWeb.Api.V1.SitesController do
|> json(%{errors: translate_errors(changeset)})
end
{:error, :forbidden} ->
conn
|> put_status(:forbidden)
|> json(%{error: "Access denied to this site"})
{:error, :not_found} ->
conn
|> put_status(:not_found)

View file

@ -8,15 +8,9 @@ defmodule ToweropsWeb.ScopedResource do
@spec fetch(module(), Ecto.UUID.t(), Ecto.UUID.t()) ::
{:ok, struct()} | {:error, :not_found | :forbidden}
def fetch(schema, id, organization_id) do
case Repo.get(schema, id) do
nil ->
{:error, :not_found}
%{organization_id: ^organization_id} = resource ->
{:ok, resource}
%{organization_id: _other_org_id} ->
{:error, :forbidden}
case Repo.get_by(schema, id: id, organization_id: organization_id) do
nil -> {:error, :not_found}
resource -> {:ok, resource}
end
end

View file

@ -297,14 +297,14 @@ defmodule ToweropsWeb.Api.V1.ChecksControllerTest do
assert %{"error" => "Check not found"} = json_response(conn, 404)
end
test "returns 403 when check belongs to different organization", %{conn: conn} do
test "returns 404 when check belongs to different organization", %{conn: conn} do
other_user = user_fixture()
other_org = organization_fixture(other_user.id)
other_check = create_check(other_org.id)
conn = get(conn, ~p"/api/v1/checks/#{other_check.id}")
assert %{"error" => "Access denied to this check"} = json_response(conn, 403)
assert %{"error" => "Check not found"} = json_response(conn, 404)
end
end
@ -342,7 +342,7 @@ defmodule ToweropsWeb.Api.V1.ChecksControllerTest do
assert %{"error" => "Check not found"} = json_response(conn, 404)
end
test "returns 403 when check belongs to different organization", %{conn: conn} do
test "returns 404 when check belongs to different organization", %{conn: conn} do
other_user = user_fixture()
other_org = organization_fixture(other_user.id)
other_check = create_check(other_org.id)
@ -351,7 +351,7 @@ defmodule ToweropsWeb.Api.V1.ChecksControllerTest do
conn = patch(conn, ~p"/api/v1/checks/#{other_check.id}", params)
assert %{"error" => "Access denied to this check"} = json_response(conn, 403)
assert %{"error" => "Check not found"} = json_response(conn, 404)
end
test "returns 422 with invalid params", %{conn: conn, organization: organization} do
@ -391,14 +391,14 @@ defmodule ToweropsWeb.Api.V1.ChecksControllerTest do
assert %{"error" => "Check not found"} = json_response(conn, 404)
end
test "returns 403 when check belongs to different organization", %{conn: conn} do
test "returns 404 when check belongs to different organization", %{conn: conn} do
other_user = user_fixture()
other_org = organization_fixture(other_user.id)
other_check = create_check(other_org.id)
conn = delete(conn, ~p"/api/v1/checks/#{other_check.id}")
assert %{"error" => "Access denied to this check"} = json_response(conn, 403)
assert %{"error" => "Check not found"} = json_response(conn, 404)
end
end

View file

@ -182,7 +182,7 @@ defmodule ToweropsWeb.Api.V1.DevicesControllerTest do
assert org_id == organization.id
end
test "returns 403 when site belongs to different organization", %{conn: conn, organization: organization} do
test "returns 404 when site belongs to different organization", %{conn: conn, organization: organization} do
# Enable sites for the authenticated organization
{:ok, _organization} = Towerops.Organizations.update_organization(organization, %{use_sites: true})
@ -204,10 +204,10 @@ defmodule ToweropsWeb.Api.V1.DevicesControllerTest do
conn = post(conn, ~p"/api/v1/devices", device_params)
assert %{"error" => "Access denied to this site"} = json_response(conn, 403)
assert %{"error" => "Site not found"} = json_response(conn, 404)
end
test "returns 403 when site_id does not exist", %{conn: conn, organization: organization} do
test "returns 404 when site_id does not exist", %{conn: conn, organization: organization} do
# Enable sites for this organization
{:ok, _organization} = Towerops.Organizations.update_organization(organization, %{use_sites: true})
@ -221,7 +221,7 @@ defmodule ToweropsWeb.Api.V1.DevicesControllerTest do
conn = post(conn, ~p"/api/v1/devices", device_params)
assert %{"error" => "Site not found"} = json_response(conn, 403)
assert %{"error" => "Site not found"} = json_response(conn, 404)
end
test "returns 422 with invalid device params", %{conn: conn} do
@ -277,7 +277,7 @@ defmodule ToweropsWeb.Api.V1.DevicesControllerTest do
assert %{"error" => "Device not found"} = json_response(conn, 404)
end
test "returns 403 when device belongs to different organization", %{conn: conn} do
test "returns 404 when device belongs to different organization", %{conn: conn} do
# Create another organization with a device
other_user = user_fixture()
other_org = organization_fixture(other_user.id)
@ -285,7 +285,7 @@ defmodule ToweropsWeb.Api.V1.DevicesControllerTest do
conn = get(conn, ~p"/api/v1/devices/#{other_device.id}")
assert %{"error" => "Access denied to this device"} = json_response(conn, 403)
assert %{"error" => "Device not found"} = json_response(conn, 404)
end
end
@ -323,7 +323,7 @@ defmodule ToweropsWeb.Api.V1.DevicesControllerTest do
assert %{"error" => "Device not found"} = json_response(conn, 404)
end
test "returns 403 when device belongs to different organization", %{conn: conn} do
test "returns 404 when device belongs to different organization", %{conn: conn} do
# Create another organization with a device
other_user = user_fixture()
other_org = organization_fixture(other_user.id)
@ -337,7 +337,7 @@ defmodule ToweropsWeb.Api.V1.DevicesControllerTest do
conn = patch(conn, ~p"/api/v1/devices/#{other_device.id}", update_params)
assert %{"error" => "Access denied to this device"} = json_response(conn, 403)
assert %{"error" => "Device not found"} = json_response(conn, 404)
end
test "returns 422 with invalid update params", %{conn: conn, organization: organization} do
@ -388,7 +388,7 @@ defmodule ToweropsWeb.Api.V1.DevicesControllerTest do
assert %{"error" => "Device not found"} = json_response(conn, 404)
end
test "returns 403 when device belongs to different organization", %{conn: conn} do
test "returns 404 when device belongs to different organization", %{conn: conn} do
# Create another organization with a device
other_user = user_fixture()
other_org = organization_fixture(other_user.id)
@ -396,7 +396,7 @@ defmodule ToweropsWeb.Api.V1.DevicesControllerTest do
conn = delete(conn, ~p"/api/v1/devices/#{other_device.id}")
assert %{"error" => "Access denied to this device"} = json_response(conn, 403)
assert %{"error" => "Device not found"} = json_response(conn, 404)
end
end
end

View file

@ -132,14 +132,14 @@ defmodule ToweropsWeb.Api.V1.EscalationPoliciesControllerTest do
assert %{"error" => "Escalation policy not found"} = json_response(conn, 404)
end
test "returns 403 when policy belongs to different organization", %{conn: conn} do
test "returns 404 when policy belongs to different organization", %{conn: conn} do
other_user = user_fixture()
other_org = organization_fixture(other_user.id)
other_policy = escalation_policy_fixture(other_org.id)
conn = get(conn, ~p"/api/v1/escalation_policies/#{other_policy.id}")
assert %{"error" => "Access denied to this escalation policy"} = json_response(conn, 403)
assert %{"error" => "Escalation policy not found"} = json_response(conn, 404)
end
end
@ -160,7 +160,7 @@ defmodule ToweropsWeb.Api.V1.EscalationPoliciesControllerTest do
assert %{"error" => "Escalation policy not found"} = json_response(conn, 404)
end
test "returns 403 when policy belongs to different organization", %{conn: conn} do
test "returns 404 when policy belongs to different organization", %{conn: conn} do
other_user = user_fixture()
other_org = organization_fixture(other_user.id)
other_policy = escalation_policy_fixture(other_org.id)
@ -168,7 +168,7 @@ defmodule ToweropsWeb.Api.V1.EscalationPoliciesControllerTest do
params = %{"escalation_policy" => %{"name" => "Hacked"}}
conn = patch(conn, ~p"/api/v1/escalation_policies/#{other_policy.id}", params)
assert %{"error" => "Access denied to this escalation policy"} = json_response(conn, 403)
assert %{"error" => "Escalation policy not found"} = json_response(conn, 404)
end
test "returns 400 when escalation_policy parameter is missing", %{
@ -199,14 +199,14 @@ defmodule ToweropsWeb.Api.V1.EscalationPoliciesControllerTest do
assert %{"error" => "Escalation policy not found"} = json_response(conn, 404)
end
test "returns 403 when policy belongs to different organization", %{conn: conn} do
test "returns 404 when policy belongs to different organization", %{conn: conn} do
other_user = user_fixture()
other_org = organization_fixture(other_user.id)
other_policy = escalation_policy_fixture(other_org.id)
conn = delete(conn, ~p"/api/v1/escalation_policies/#{other_policy.id}")
assert %{"error" => "Access denied to this escalation policy"} = json_response(conn, 403)
assert %{"error" => "Escalation policy not found"} = json_response(conn, 404)
end
end

View file

@ -164,7 +164,7 @@ defmodule ToweropsWeb.Api.V1.MaintenanceWindowsControllerTest do
assert %{"error" => "Maintenance window not found"} = json_response(conn, 404)
end
test "returns 403 when window belongs to different organization", %{conn: conn} do
test "returns 404 when window belongs to different organization", %{conn: conn} do
other_user = user_fixture()
other_org = organization_fixture(other_user.id)
@ -176,7 +176,7 @@ defmodule ToweropsWeb.Api.V1.MaintenanceWindowsControllerTest do
conn = get(conn, ~p"/api/v1/maintenance_windows/#{other_window.id}")
assert %{"error" => "Access denied to this maintenance window"} = json_response(conn, 403)
assert %{"error" => "Maintenance window not found"} = json_response(conn, 404)
end
end
@ -242,7 +242,7 @@ defmodule ToweropsWeb.Api.V1.MaintenanceWindowsControllerTest do
assert %{"error" => "Maintenance window not found"} = json_response(conn, 404)
end
test "returns 403 when window belongs to different organization", %{conn: conn} do
test "returns 404 when window belongs to different organization", %{conn: conn} do
other_user = user_fixture()
other_org = organization_fixture(other_user.id)
@ -254,7 +254,7 @@ defmodule ToweropsWeb.Api.V1.MaintenanceWindowsControllerTest do
conn = delete(conn, ~p"/api/v1/maintenance_windows/#{other_window.id}")
assert %{"error" => "Access denied to this maintenance window"} = json_response(conn, 403)
assert %{"error" => "Maintenance window not found"} = json_response(conn, 404)
end
end

View file

@ -137,14 +137,14 @@ defmodule ToweropsWeb.Api.V1.SchedulesControllerTest do
assert %{"error" => "Schedule not found"} = json_response(conn, 404)
end
test "returns 403 when schedule belongs to different organization", %{conn: conn} do
test "returns 404 when schedule belongs to different organization", %{conn: conn} do
other_user = user_fixture()
other_org = organization_fixture(other_user.id)
other_schedule = schedule_fixture(other_org.id)
conn = get(conn, ~p"/api/v1/schedules/#{other_schedule.id}")
assert %{"error" => "Access denied to this schedule"} = json_response(conn, 403)
assert %{"error" => "Schedule not found"} = json_response(conn, 404)
end
end
@ -165,7 +165,7 @@ defmodule ToweropsWeb.Api.V1.SchedulesControllerTest do
assert %{"error" => "Schedule not found"} = json_response(conn, 404)
end
test "returns 403 when schedule belongs to different organization", %{conn: conn} do
test "returns 404 when schedule belongs to different organization", %{conn: conn} do
other_user = user_fixture()
other_org = organization_fixture(other_user.id)
other_schedule = schedule_fixture(other_org.id)
@ -173,7 +173,7 @@ defmodule ToweropsWeb.Api.V1.SchedulesControllerTest do
params = %{"schedule" => %{"name" => "Hacked"}}
conn = patch(conn, ~p"/api/v1/schedules/#{other_schedule.id}", params)
assert %{"error" => "Access denied to this schedule"} = json_response(conn, 403)
assert %{"error" => "Schedule not found"} = json_response(conn, 404)
end
test "returns 400 when schedule parameter is missing", %{
@ -204,14 +204,14 @@ defmodule ToweropsWeb.Api.V1.SchedulesControllerTest do
assert %{"error" => "Schedule not found"} = json_response(conn, 404)
end
test "returns 403 when schedule belongs to different organization", %{conn: conn} do
test "returns 404 when schedule belongs to different organization", %{conn: conn} do
other_user = user_fixture()
other_org = organization_fixture(other_user.id)
other_schedule = schedule_fixture(other_org.id)
conn = delete(conn, ~p"/api/v1/schedules/#{other_schedule.id}")
assert %{"error" => "Access denied to this schedule"} = json_response(conn, 403)
assert %{"error" => "Schedule not found"} = json_response(conn, 404)
end
end

View file

@ -173,7 +173,7 @@ defmodule ToweropsWeb.Api.V1.SitesControllerTest do
assert %{"error" => "Site not found"} = json_response(conn, 404)
end
test "returns 403 when site belongs to different organization", %{conn: conn} do
test "returns 404 when site belongs to different organization", %{conn: conn} do
other_user = user_fixture()
other_org = organization_fixture(other_user.id)
@ -182,7 +182,7 @@ defmodule ToweropsWeb.Api.V1.SitesControllerTest do
conn = get(conn, ~p"/api/v1/sites/#{other_site.id}")
assert %{"error" => "Access denied to this site"} = json_response(conn, 403)
assert %{"error" => "Site not found"} = json_response(conn, 404)
end
end
@ -225,7 +225,7 @@ defmodule ToweropsWeb.Api.V1.SitesControllerTest do
assert %{"error" => "Site not found"} = json_response(conn, 404)
end
test "returns 403 when site belongs to different organization", %{conn: conn} do
test "returns 404 when site belongs to different organization", %{conn: conn} do
other_user = user_fixture()
other_org = organization_fixture(other_user.id)
@ -240,7 +240,7 @@ defmodule ToweropsWeb.Api.V1.SitesControllerTest do
conn = patch(conn, ~p"/api/v1/sites/#{other_site.id}", update_params)
assert %{"error" => "Access denied to this site"} = json_response(conn, 403)
assert %{"error" => "Site not found"} = json_response(conn, 404)
end
test "returns 422 with invalid update params", %{conn: conn, organization: organization} do
@ -293,7 +293,7 @@ defmodule ToweropsWeb.Api.V1.SitesControllerTest do
assert %{"error" => "Site not found"} = json_response(conn, 404)
end
test "returns 403 when site belongs to different organization", %{conn: conn} do
test "returns 404 when site belongs to different organization", %{conn: conn} do
other_user = user_fixture()
other_org = organization_fixture(other_user.id)
@ -302,7 +302,7 @@ defmodule ToweropsWeb.Api.V1.SitesControllerTest do
conn = delete(conn, ~p"/api/v1/sites/#{other_site.id}")
assert %{"error" => "Access denied to this site"} = json_response(conn, 403)
assert %{"error" => "Site not found"} = json_response(conn, 404)
end
end