- Daily database backups: jarl-backup.timer runs 'jarld backup', which writes jarl-<UTC time>.db into backup.dir, checks it before naming it, and prunes by the time in the name while always keeping the newest keep_at_least. New backup: section in jarl.yaml; the dashboard and 'jarl status' show the newest backup and warn when it is stale or failed. The Debian package enables the timer. - 'jarld restore [FILE | latest]': checks the backup, shows it next to the current database, keeps the current one aside and records the restore. 'jarld serve' holds a database lock, so a restore can't run under a live jarld and two jarlds can't share a database. - Token expiry warnings from 14 days ahead: dashboard, token lists, a daily log line, and a CLI warning via the Jarl-Token-Expires header; 'jarl token list --expiring'. - 'jarl zone serials' and the zone page's Name servers tab: compare the serial on a primary zone's secondaries (NS records, also-notify) or a secondary zone's primaries with this server's. - 'jarl zone delegation': for zones on the internet, check the parent's delegation and glue against the zone; an undelegated zone gets a note. - Server: Jarl/<version> header; CLI exit code 2 for failed checks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
270 lines
9.0 KiB
Go
270 lines
9.0 KiB
Go
// Package api defines the JSON types of Jarl's HTTP API, shared by the
|
|
// server and the Go client used by the CLI.
|
|
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"time"
|
|
|
|
"jarl/internal/model"
|
|
"jarl/internal/nscheck"
|
|
)
|
|
|
|
// Version is the API path prefix.
|
|
const Prefix = "/api/v1"
|
|
|
|
// Zone is a zone as returned by the API.
|
|
type Zone struct {
|
|
model.Zone
|
|
Display string `json:"display_name"`
|
|
View string `json:"view,omitempty"`
|
|
Records int `json:"record_count"`
|
|
ZoneFile string `json:"zone_file,omitempty"` // the file named loads
|
|
RName string `json:"rname_email,omitempty"`
|
|
// Pending is set when listing with a changeset: add, change or delete.
|
|
Pending string `json:"pending,omitempty"`
|
|
}
|
|
|
|
// Record is a record as returned by the API.
|
|
type Record struct {
|
|
model.Record
|
|
Zone string `json:"zone"`
|
|
View string `json:"view,omitempty"`
|
|
RelativeName string `json:"relative_name"`
|
|
EffectiveTTL uint32 `json:"effective_ttl"`
|
|
// Text is the decoded value of TXT and SPF records.
|
|
Text *string `json:"text,omitempty"`
|
|
// Pending is set when listing with a changeset: add, change or delete.
|
|
Pending string `json:"pending,omitempty"`
|
|
}
|
|
|
|
// View is a view with its zone count.
|
|
type View struct {
|
|
model.View
|
|
Zones int `json:"zone_count"`
|
|
}
|
|
|
|
// ACL is an ACL with the objects that use it.
|
|
type ACL struct {
|
|
model.ACL
|
|
UsedBy []string `json:"used_by"`
|
|
}
|
|
|
|
// TSIGKey is a key; Secret is only filled when asked for and allowed.
|
|
type TSIGKey struct {
|
|
model.TSIGKey
|
|
UsedBy []string `json:"used_by"`
|
|
}
|
|
|
|
// Option is an option with display helpers.
|
|
type Option struct {
|
|
model.Option
|
|
Where string `json:"where"` // e.g. "view internal"
|
|
Description string `json:"description,omitempty"`
|
|
Status string `json:"status,omitempty"`
|
|
}
|
|
|
|
// User is a user with formatted grants.
|
|
type User struct {
|
|
model.User
|
|
GrantStrings []string `json:"grant_strings"`
|
|
}
|
|
|
|
// Token is an API token. Secret is only set when the token is created.
|
|
type Token struct {
|
|
model.Token
|
|
GrantStrings []string `json:"grant_strings,omitempty"`
|
|
Secret string `json:"token,omitempty"`
|
|
}
|
|
|
|
// Whoami describes the authenticated principal.
|
|
type Whoami struct {
|
|
User User `json:"user"`
|
|
Method string `json:"method"`
|
|
Token *Token `json:"token,omitempty"`
|
|
CSRFToken string `json:"csrf_token,omitempty"`
|
|
Permissions []string `json:"permissions"`
|
|
Admin bool `json:"admin"`
|
|
Version string `json:"version"`
|
|
}
|
|
|
|
// LoginRequest logs in.
|
|
type LoginRequest struct {
|
|
Username string `json:"username"`
|
|
Password string `json:"password"`
|
|
}
|
|
|
|
// ErrorBody is the error response.
|
|
type ErrorBody struct {
|
|
Error ErrorInfo `json:"error"`
|
|
}
|
|
|
|
// ErrorInfo describes an error.
|
|
type ErrorInfo struct {
|
|
Code string `json:"code"`
|
|
Message string `json:"message"`
|
|
Op int `json:"op,omitempty"`
|
|
Problems json.RawMessage `json:"problems,omitempty"`
|
|
Check json.RawMessage `json:"check,omitempty"`
|
|
Result json.RawMessage `json:"result,omitempty"`
|
|
}
|
|
|
|
// ChangeRequest is a request in a batch (POST /apply or changeset ops).
|
|
type ChangeRequest struct {
|
|
Op string `json:"op"`
|
|
ID int64 `json:"id,omitempty"`
|
|
BaseVersion int `json:"base_version,omitempty"`
|
|
Data json.RawMessage `json:"data,omitempty"`
|
|
}
|
|
|
|
// BatchRequest applies (or stages) several requests at once.
|
|
type BatchRequest struct {
|
|
Description string `json:"description,omitempty"`
|
|
Ops []ChangeRequest `json:"ops"`
|
|
DryRun bool `json:"dry_run,omitempty"`
|
|
Force bool `json:"force,omitempty"`
|
|
// Apply (for POST /changesets) applies the new changeset at once.
|
|
Apply bool `json:"apply,omitempty"`
|
|
}
|
|
|
|
// MutationResponse is returned by endpoints that change the configuration.
|
|
type MutationResponse struct {
|
|
Applied bool `json:"applied"`
|
|
Staged bool `json:"staged,omitempty"`
|
|
DryRun bool `json:"dry_run,omitempty"`
|
|
ChangesetID int64 `json:"changeset_id,omitempty"`
|
|
Result json.RawMessage `json:"result,omitempty"`
|
|
Changeset json.RawMessage `json:"changeset,omitempty"`
|
|
Added []model.Op `json:"added,omitempty"`
|
|
Warnings []string `json:"warnings,omitempty"`
|
|
Problems json.RawMessage `json:"problems,omitempty"`
|
|
Object json.RawMessage `json:"object,omitempty"`
|
|
}
|
|
|
|
// Changeset is a changeset as returned by the API.
|
|
type Changeset struct {
|
|
model.Changeset
|
|
Undoable bool `json:"undoable"`
|
|
}
|
|
|
|
// StatusResponse is GET /status.
|
|
type StatusResponse struct {
|
|
Version string `json:"version"`
|
|
Engine json.RawMessage `json:"engine"`
|
|
Time time.Time `json:"time"`
|
|
// ExpiringTokens are tokens that expire within TokenExpiryWarning or
|
|
// expired less than that long ago (not revoked): every user's for
|
|
// administrators, otherwise the caller's own.
|
|
ExpiringTokens []Token `json:"expiring_tokens"`
|
|
// Backup is the state of database backups (administrators only).
|
|
Backup *BackupStatus `json:"backup,omitempty"`
|
|
}
|
|
|
|
// TokenExpiresHeader is sent with responses to a token that expires
|
|
// soon (model.TokenExpiryWarning), holding its expiry time (RFC 3339). The
|
|
// CLI warns its user when it sees it.
|
|
const TokenExpiresHeader = "Jarl-Token-Expires"
|
|
|
|
// BackupStatus describes the newest database backup, as recorded by
|
|
// 'jarld backup'.
|
|
type BackupStatus struct {
|
|
LastAt *time.Time `json:"last_at,omitempty"`
|
|
LastFile string `json:"last_file,omitempty"`
|
|
LastSize int64 `json:"last_size,omitempty"`
|
|
LastError string `json:"last_error,omitempty"`
|
|
// LastErrorAt is when the newest failed backup ran (only reported
|
|
// when it is newer than the last success).
|
|
LastErrorAt *time.Time `json:"last_error_at,omitempty"`
|
|
Dir string `json:"dir"`
|
|
// WarnAfter (seconds) is how old the newest backup may get before
|
|
// Stale is set (0: never).
|
|
WarnAfter int64 `json:"warn_after"`
|
|
Stale bool `json:"stale"`
|
|
}
|
|
|
|
// SearchResult is one search hit.
|
|
type SearchResult struct {
|
|
Kind string `json:"kind"` // zone, record, acl, view, key
|
|
ID int64 `json:"id"`
|
|
ZoneID int64 `json:"zone_id,omitempty"`
|
|
Label string `json:"label"`
|
|
Detail string `json:"detail,omitempty"`
|
|
}
|
|
|
|
// PasswordChange changes a password.
|
|
type PasswordChange struct {
|
|
Current string `json:"current_password,omitempty"`
|
|
Password string `json:"password"`
|
|
}
|
|
|
|
// UserInput creates or updates a user.
|
|
type UserInput struct {
|
|
Username *string `json:"username,omitempty"`
|
|
Password *string `json:"password,omitempty"`
|
|
Admin *bool `json:"admin,omitempty"`
|
|
Grants *[]model.Grant `json:"grants,omitempty"`
|
|
// GrantStrings are grants in compact form ("records:write zones=x").
|
|
GrantStrings *[]string `json:"grant_strings,omitempty"`
|
|
Role *string `json:"role,omitempty"` // a role template name
|
|
Disabled *bool `json:"disabled,omitempty"`
|
|
Comment *string `json:"comment,omitempty"`
|
|
}
|
|
|
|
// TokenInput creates a token.
|
|
type TokenInput struct {
|
|
Name string `json:"name"`
|
|
Grants *[]model.Grant `json:"grants,omitempty"`
|
|
GrantStrings *[]string `json:"grant_strings,omitempty"`
|
|
Role string `json:"role,omitempty"`
|
|
ExpiresIn string `json:"expires_in,omitempty"` // e.g. "90d", "720h"
|
|
ExpiresAt *time.Time `json:"expires_at,omitempty"`
|
|
}
|
|
|
|
// TokenRotation gives a token a new secret (POST /tokens/{id}/rotate).
|
|
// Every field is optional.
|
|
type TokenRotation struct {
|
|
// Grace keeps the old secret working this long (e.g. "1h", "2d"; at
|
|
// most 7d), so clients can be updated without an outage. Default:
|
|
// the old secret stops at once.
|
|
Grace string `json:"grace,omitempty"`
|
|
// The new expiry. Default: a token that expires gets its lifetime
|
|
// again, counted from now; one that doesn't, still doesn't.
|
|
ExpiresIn string `json:"expires_in,omitempty"`
|
|
ExpiresAt *time.Time `json:"expires_at,omitempty"`
|
|
NoExpiry bool `json:"no_expiry,omitempty"`
|
|
}
|
|
|
|
// ZoneImport imports zone file text into a zone.
|
|
type ZoneImport struct {
|
|
Text string `json:"text"`
|
|
// Mode "merge" adds missing records; "replace" makes the zone equal
|
|
// to the file (deleting other records).
|
|
Mode string `json:"mode"`
|
|
DryRun bool `json:"dry_run,omitempty"`
|
|
// UseSOA also takes the SOA timers and default TTL from the file.
|
|
UseSOA bool `json:"use_soa,omitempty"`
|
|
}
|
|
|
|
// SerialsResponse is GET /zones/{zone}/serials.
|
|
type SerialsResponse struct {
|
|
Zone string `json:"zone"`
|
|
View string `json:"view,omitempty"`
|
|
Type string `json:"type"`
|
|
// Serial is what this server serves: named's loaded serial, or for a
|
|
// primary zone Jarl's when named can't be asked (SerialSource says
|
|
// which).
|
|
Serial *uint32 `json:"serial"`
|
|
SerialSource string `json:"serial_source,omitempty"`
|
|
Servers []nscheck.Result `json:"servers"`
|
|
Notes []string `json:"notes"`
|
|
CheckedAt time.Time `json:"checked_at"`
|
|
}
|
|
|
|
// DelegationResponse is GET /zones/{zone}/delegation.
|
|
type DelegationResponse struct {
|
|
*nscheck.DelegationReport
|
|
View string `json:"view,omitempty"`
|
|
Notes []string `json:"notes"`
|
|
CheckedAt time.Time `json:"checked_at"`
|
|
}
|