ee25552a53
The sign-in screen asks the server how it can be signed in to (GET /api/auth/config) and offers what it finds: the password form, and "Sign in with <provider>" when the server can do a device login. The TUI shows a link and a short code, the person approves it in any browser, and the next poll hands over the ordinary session, so it works over SSH where no browser can be opened. The terminal never talks to the identity provider. The password form is hidden when the server has turned password login off. `auth: sso` in config.yaml starts the SSO login straight away, but not right after signing out, where that would sign the person straight back in; any other value is refused when the config is read. Polling honours the server's interval, backs off on slow_down, and gives up after repeated failures rather than retrying forever. A server without /api/auth/config answers 404 and is treated as passwords only, so the sign-in screen is the one it had. Needs terdut-server v0.29.0 for SSO.
297 lines
11 KiB
Go
297 lines
11 KiB
Go
package api
|
|
|
|
import "time"
|
|
|
|
// Alert is the server's record of what Alertmanager said. It is read-only:
|
|
// acknowledging, assigning, noting and resolving all happen on the Incident an
|
|
// alert belongs to.
|
|
type Alert struct {
|
|
ID int64 `json:"id"`
|
|
TeamID int64 `json:"team_id"`
|
|
TeamName string `json:"team_name,omitempty"`
|
|
Fingerprint string `json:"fingerprint"`
|
|
Name string `json:"name"`
|
|
Status string `json:"status"`
|
|
Labels map[string]string `json:"labels"`
|
|
Annotations map[string]string `json:"annotations"`
|
|
StartsAt time.Time `json:"starts_at"`
|
|
EndsAt *time.Time `json:"ends_at"`
|
|
GeneratorURL string `json:"generator_url"`
|
|
ReceivedAt time.Time `json:"received_at"`
|
|
ArchivedAt *time.Time `json:"archived_at,omitempty"`
|
|
|
|
// IncidentID is the most recent incident this alert belongs to. An alert row
|
|
// is reused across occurrences of the same fingerprint, so it belongs to a
|
|
// series of incidents over its life and this is only the newest.
|
|
IncidentID *int64 `json:"incident_id,omitempty"`
|
|
|
|
// ResolutionSource records why a resolved alert left the firing state:
|
|
// "alertmanager" for a real resolved webhook, "expiry" when the server
|
|
// inferred it after the alert stopped being refreshed, "deadman" for a
|
|
// dead man's switch that came back. Treat the value set as open.
|
|
ResolutionSource *string `json:"resolution_source,omitempty"`
|
|
}
|
|
|
|
// AuthConfig is how the server can be signed in to, from the unauthenticated
|
|
// GET /api/auth/config. A server too old to have the endpoint answers 404, which
|
|
// callers treat as "passwords only".
|
|
type AuthConfig struct {
|
|
PasswordLogin bool `json:"password_login"`
|
|
OIDC struct {
|
|
Enabled bool `json:"enabled"`
|
|
Name string `json:"name"`
|
|
} `json:"oidc"`
|
|
|
|
// DeviceLogin is whether the server can sign in a client that has no browser,
|
|
// by showing a code (see StartDeviceLogin).
|
|
DeviceLogin bool `json:"device_login"`
|
|
}
|
|
|
|
// DeviceLogin is a sign-in the server has started for this client: the person
|
|
// opens VerificationURL, checks UserCode, and approves; the client polls with
|
|
// DeviceCode until the server hands over a session.
|
|
type DeviceLogin struct {
|
|
DeviceCode string `json:"device_code"`
|
|
UserCode string `json:"user_code"`
|
|
VerificationURL string `json:"verification_url"`
|
|
|
|
// Interval is how many seconds to wait between polls, and ExpiresIn how many
|
|
// the person has to approve.
|
|
Interval int `json:"interval"`
|
|
ExpiresIn int `json:"expires_in"`
|
|
}
|
|
|
|
// Incident statuses.
|
|
const (
|
|
StatusTriggered = "triggered"
|
|
StatusAcknowledged = "acknowledged"
|
|
StatusResolved = "resolved"
|
|
)
|
|
|
|
// Incident is the work item: what a person acknowledges, assigns, snoozes,
|
|
// discusses and resolves. Many alerts map to one incident, correlated by the
|
|
// groupKey Alertmanager computed from the operator's group_by configuration.
|
|
type Incident struct {
|
|
ID int64 `json:"id"`
|
|
TeamID int64 `json:"team_id"`
|
|
TeamName string `json:"team_name,omitempty"`
|
|
GroupKey string `json:"group_key"`
|
|
Title string `json:"title"`
|
|
GroupLabels map[string]string `json:"group_labels"`
|
|
Status string `json:"status"`
|
|
|
|
// Severity is a high-water mark across the incident's alerts, never lowered,
|
|
// so a resolved incident still says how bad it got.
|
|
Severity string `json:"severity,omitempty"`
|
|
|
|
TriggeredAt time.Time `json:"triggered_at"`
|
|
|
|
AcknowledgedByID *int64 `json:"acknowledged_by_id,omitempty"`
|
|
AcknowledgedBy string `json:"acknowledged_by,omitempty"`
|
|
AcknowledgedAt *time.Time `json:"acknowledged_at,omitempty"`
|
|
|
|
AssignedToID *int64 `json:"assigned_to_id,omitempty"`
|
|
AssignedTo string `json:"assigned_to,omitempty"`
|
|
|
|
SnoozedUntil *time.Time `json:"snoozed_until,omitempty"`
|
|
|
|
ResolvedAt *time.Time `json:"resolved_at,omitempty"`
|
|
|
|
// ResolutionSource is "alerts" when every alert stopped firing, "manual"
|
|
// when a person closed it, or "recovered" when a dead man's switch came back.
|
|
// Treat the value set as open.
|
|
ResolutionSource *string `json:"resolution_source,omitempty"`
|
|
|
|
// EscalationLevel is how far up the team's escalation ladder the incident has
|
|
// climbed (0 = not escalated). EscalationDueAt is when the next step fires,
|
|
// and is nil once the ladder is exhausted or the incident is acknowledged.
|
|
EscalationLevel int `json:"escalation_level"`
|
|
EscalationDueAt *time.Time `json:"escalation_due_at,omitempty"`
|
|
|
|
ArchivedAt *time.Time `json:"archived_at,omitempty"`
|
|
|
|
// Alerts is populated by GET /api/incidents/{id} only.
|
|
Alerts []Alert `json:"alerts,omitempty"`
|
|
}
|
|
|
|
// IsSnoozed reports whether the incident is currently quietened. A snooze
|
|
// expires by falling into the past; nothing on the server sweeps it.
|
|
func (i Incident) IsSnoozed() bool {
|
|
return i.SnoozedUntil != nil && i.SnoozedUntil.After(time.Now())
|
|
}
|
|
|
|
// IsOpen reports whether the incident is still work in progress.
|
|
func (i Incident) IsOpen() bool { return i.ResolvedAt == nil }
|
|
|
|
// Incident timeline event types written by the server. New ones may be added,
|
|
// so render unrecognised types generically rather than dropping them.
|
|
const (
|
|
EventTriggered = "triggered"
|
|
EventAlertAdded = "alert_added"
|
|
EventAlertResolved = "alert_resolved"
|
|
EventAcknowledged = "acknowledged"
|
|
EventUnacknowledged = "unacknowledged"
|
|
EventAssigned = "assigned"
|
|
EventSnoozed = "snoozed"
|
|
EventUnsnoozed = "unsnoozed"
|
|
EventResolved = "resolved"
|
|
EventNote = "note"
|
|
|
|
// A note marked as what fixed the incident. The server leads similar
|
|
// incidents with these.
|
|
EventResolutionNote = "resolution_note"
|
|
|
|
// Written when a team's dead man's switch stops reporting.
|
|
EventDeadmanSilent = "deadman_silent"
|
|
|
|
// Written by the server's notifier from the delivery result, not at enqueue.
|
|
// Detail carries the notification kind ("triggered", "reminder", "resolved"),
|
|
// and on a failure the reason after it. An absent user means the page went to
|
|
// the shared fallback topic rather than to a person.
|
|
EventNotified = "notified"
|
|
EventNotifyFailed = "notify_failed"
|
|
)
|
|
|
|
// IncidentEvent is one entry in an incident's timeline. An empty Username means
|
|
// the server acted rather than a person. On an "assigned" event the user is the
|
|
// assignee, not the actor.
|
|
type IncidentEvent struct {
|
|
ID int64 `json:"id"`
|
|
IncidentID int64 `json:"incident_id"`
|
|
Type string `json:"type"`
|
|
UserID *int64 `json:"user_id,omitempty"`
|
|
Username string `json:"username,omitempty"`
|
|
AlertID *int64 `json:"alert_id,omitempty"`
|
|
Detail string `json:"detail,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
}
|
|
|
|
type AlertStats struct {
|
|
Total int `json:"total"`
|
|
Firing int `json:"firing"`
|
|
Resolved int `json:"resolved"`
|
|
}
|
|
|
|
// IncidentStats carries the queue counts plus mean time to acknowledge and to
|
|
// resolve. Both averages are nil until something has actually been acknowledged
|
|
// or resolved — that is "no data", not zero.
|
|
type IncidentStats struct {
|
|
Total int `json:"total"`
|
|
Triggered int `json:"triggered"`
|
|
Acknowledged int `json:"acknowledged"`
|
|
Resolved int `json:"resolved"`
|
|
MTTASeconds *float64 `json:"mtta_seconds"`
|
|
MTTRSeconds *float64 `json:"mttr_seconds"`
|
|
}
|
|
|
|
type TopAlert struct {
|
|
Name string `json:"name"`
|
|
Count int `json:"count"`
|
|
}
|
|
|
|
type HourStat struct {
|
|
Hour int `json:"hour"`
|
|
Count int `json:"count"`
|
|
}
|
|
|
|
type DayStat struct {
|
|
Day int `json:"day"`
|
|
DayName string `json:"day_name"`
|
|
Count int `json:"count"`
|
|
}
|
|
|
|
type ScheduleEntry struct {
|
|
ID int64 `json:"id"`
|
|
TeamID int64 `json:"team_id"`
|
|
TeamName string `json:"team_name,omitempty"`
|
|
UserID int64 `json:"user_id"`
|
|
Username string `json:"username"`
|
|
Date string `json:"date"` // YYYY-MM-DD
|
|
CreatedAt time.Time `json:"created_at"`
|
|
}
|
|
|
|
type User struct {
|
|
ID int64 `json:"id"`
|
|
Username string `json:"username"`
|
|
Email string `json:"email"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
|
|
// IsAdmin marks a system administrator: the only kind of user who can create
|
|
// or delete users and act on other people's passwords and keys.
|
|
IsAdmin bool `json:"is_admin"`
|
|
|
|
// DisabledAt is set when an administrator has disabled the account. A
|
|
// disabled user cannot sign in or use their keys.
|
|
DisabledAt *time.Time `json:"disabled_at,omitempty"`
|
|
|
|
// NtfyTopic is where this user's push notifications go. Nil and empty mean
|
|
// the same thing — no topic of their own — because the server stores a blank
|
|
// string as NULL. Their incidents fall back to the server's shared fallback
|
|
// topic, which carries no Acknowledge button.
|
|
NtfyTopic *string `json:"ntfy_topic,omitempty"`
|
|
}
|
|
|
|
// Topic reads the user's ntfy topic, flattening the nil and empty cases the
|
|
// server treats alike.
|
|
func (u User) Topic() string {
|
|
if u.NtfyTopic == nil {
|
|
return ""
|
|
}
|
|
return *u.NtfyTopic
|
|
}
|
|
|
|
// IsDisabled reports whether the account has been disabled.
|
|
func (u User) IsDisabled() bool { return u.DisabledAt != nil }
|
|
|
|
// Me is GET /api/me: the caller, and whether they can sign in to the web UI.
|
|
type Me struct {
|
|
User User `json:"user"`
|
|
HasPassword bool `json:"has_password"`
|
|
}
|
|
|
|
// Team roles.
|
|
const (
|
|
RoleOwner = "owner"
|
|
RoleMember = "member"
|
|
)
|
|
|
|
// Team is a group that owns integrations, incidents, a schedule and an
|
|
// escalation ladder. Role is the caller's role in it, and is only present on
|
|
// the caller's own team lists (GET /api/teams).
|
|
type Team struct {
|
|
ID int64 `json:"id"`
|
|
Name string `json:"name"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
Role string `json:"role,omitempty"`
|
|
}
|
|
|
|
// TeamMember is one person's membership of a team.
|
|
type TeamMember struct {
|
|
TeamID int64 `json:"team_id"`
|
|
UserID int64 `json:"user_id"`
|
|
Username string `json:"username"`
|
|
Role string `json:"role"`
|
|
JoinedAt time.Time `json:"joined_at"`
|
|
}
|
|
|
|
type APIKey struct {
|
|
ID int64 `json:"id"`
|
|
UserID int64 `json:"user_id"`
|
|
Name string `json:"name"`
|
|
Key string `json:"key,omitempty"` // only present in create response
|
|
CreatedAt time.Time `json:"created_at"`
|
|
LastUsedAt *time.Time `json:"last_used_at"`
|
|
}
|
|
|
|
// SimilarIncident is an earlier, resolved incident with the same signature
|
|
// (alert name plus stable group labels) as the one being viewed. ResolutionNotes
|
|
// are its "what fixed it" notes; NoteCount counts its plain notes.
|
|
type SimilarIncident struct {
|
|
ID int64 `json:"id"`
|
|
Title string `json:"title"`
|
|
TriggeredAt time.Time `json:"triggered_at"`
|
|
ResolvedAt time.Time `json:"resolved_at"`
|
|
NoteCount int `json:"note_count"`
|
|
ResolutionNotes []IncidentEvent `json:"resolution_notes"`
|
|
}
|