towerops/lib/towerops/on_call.ex
Graham McIntie 1590f78bdc fix: input validation, SSRF, API hardening, and cookie security
- LIKE wildcard injection: sanitize %, _ in search queries (devices, sites, gaiia)
- Jason.decode! → Jason.decode with error handling for untrusted input
- inspect() leak: replace with generic error messages, log details server-side
- SSRF protection: URL validator blocks private IPs, localhost, non-HTTP schemes
- SSRF validation added to HTTP monitoring executor and integration credentials
- GraphQL complexity limits: always applied, not just in prod
- GraphQL introspection: also check GET query params, not just body
- Stripe webhook: explicit nil/empty checks for signature and body
- Cookie security: secure flag for session (prod), http_only+secure for remember_me
- Honeybadger API key: read from env var with fallback
- String.to_integer → Integer.parse with fallback for URL params
- String.to_atom → whitelist map for HTTP methods
- Gaiia webhook: remove secret_len and expected signature from log
- Admin API: add rate limiting pipeline
- to_atom_keys: per-key fallback instead of all-or-nothing rescue
2026-03-14 14:48:59 -05:00

196 lines
4.3 KiB
Elixir

defmodule Towerops.OnCall do
@moduledoc """
Context for on-call schedules and escalation policies.
"""
import Ecto.Query
alias Towerops.OnCall.EscalationPolicy
alias Towerops.OnCall.EscalationRule
alias Towerops.OnCall.EscalationTarget
alias Towerops.OnCall.Layer
alias Towerops.OnCall.LayerMember
alias Towerops.OnCall.Override
alias Towerops.OnCall.Resolver
alias Towerops.OnCall.Schedule
alias Towerops.Repo
# --- Schedules ---
def list_schedules(organization_id) do
Schedule
|> where([s], s.organization_id == ^organization_id)
|> order_by([s], asc: s.name)
|> Repo.all()
end
def get_schedule!(id) do
Schedule
|> Repo.get!(id)
|> Repo.preload(overrides: :user, layers: [members: :user])
end
def get_schedule!(id, organization_id) do
Schedule
|> where([s], s.id == ^id and s.organization_id == ^organization_id)
|> Repo.one!()
|> Repo.preload(overrides: :user, layers: [members: :user])
end
def get_schedule(id) do
case Repo.get(Schedule, id) do
nil -> nil
schedule -> Repo.preload(schedule, overrides: :user, layers: [members: :user])
end
end
def create_schedule(attrs) do
%Schedule{}
|> Schedule.changeset(attrs)
|> Repo.insert()
end
def update_schedule(%Schedule{} = schedule, attrs) do
schedule
|> Schedule.changeset(attrs)
|> Repo.update()
end
def delete_schedule(%Schedule{} = schedule) do
Repo.delete(schedule)
end
def change_schedule(%Schedule{} = schedule, attrs \\ %{}) do
Schedule.changeset(schedule, attrs)
end
# --- Layers ---
def create_layer(attrs) do
%Layer{}
|> Layer.changeset(attrs)
|> Repo.insert()
end
def update_layer(%Layer{} = layer, attrs) do
layer
|> Layer.changeset(attrs)
|> Repo.update()
end
def delete_layer(%Layer{} = layer) do
Repo.delete(layer)
end
# --- Layer Members ---
def add_layer_member(attrs) do
%LayerMember{}
|> LayerMember.changeset(attrs)
|> Repo.insert()
end
def remove_layer_member(%LayerMember{} = member) do
Repo.delete(member)
end
# --- Overrides ---
def create_override(attrs) do
%Override{}
|> Override.changeset(attrs)
|> Repo.insert()
end
def delete_override(%Override{} = override) do
Repo.delete(override)
end
# --- Escalation Policies ---
def list_escalation_policies(organization_id) do
EscalationPolicy
|> where([p], p.organization_id == ^organization_id)
|> order_by([p], asc: p.name)
|> Repo.all()
end
def get_escalation_policy!(id) do
EscalationPolicy
|> Repo.get!(id)
|> Repo.preload(rules: :targets)
end
def get_escalation_policy!(id, organization_id) do
EscalationPolicy
|> where([p], p.id == ^id and p.organization_id == ^organization_id)
|> Repo.one!()
|> Repo.preload(rules: :targets)
end
def get_escalation_policy(id) do
case Repo.get(EscalationPolicy, id) do
nil -> nil
policy -> Repo.preload(policy, rules: :targets)
end
end
def create_escalation_policy(attrs) do
%EscalationPolicy{}
|> EscalationPolicy.changeset(attrs)
|> Repo.insert()
end
def update_escalation_policy(%EscalationPolicy{} = policy, attrs) do
policy
|> EscalationPolicy.changeset(attrs)
|> Repo.update()
end
def delete_escalation_policy(%EscalationPolicy{} = policy) do
Repo.delete(policy)
end
def change_escalation_policy(%EscalationPolicy{} = policy, attrs \\ %{}) do
EscalationPolicy.changeset(policy, attrs)
end
# --- Escalation Rules ---
def create_escalation_rule(attrs) do
%EscalationRule{}
|> EscalationRule.changeset(attrs)
|> Repo.insert()
end
def update_escalation_rule(%EscalationRule{} = rule, attrs) do
rule
|> EscalationRule.changeset(attrs)
|> Repo.update()
end
def delete_escalation_rule(%EscalationRule{} = rule) do
Repo.delete(rule)
end
# --- Escalation Targets ---
def create_escalation_target(attrs) do
%EscalationTarget{}
|> EscalationTarget.changeset(attrs)
|> Repo.insert()
end
def delete_escalation_target(%EscalationTarget{} = target) do
Repo.delete(target)
end
# --- On-Call Resolution ---
def who_is_on_call(schedule_id, datetime \\ DateTime.utc_now()) do
case get_schedule(schedule_id) do
nil -> nil
schedule -> Resolver.resolve(schedule, datetime)
end
end
end