- 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
196 lines
4.3 KiB
Elixir
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
|