Files
jarl/internal/api/types.go
T
2026-10-09 15:17:49 +00:00

466 lines
16 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/dnssec"
"jarl/internal/model"
"jarl/internal/nscheck"
"jarl/internal/porkbun"
"jarl/internal/version"
)
// 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"`
// Registrar names the registrar Jarl can manage the domain at
// ("porkbun"), when one is configured for it.
Registrar string `json:"registrar,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"`
Color string `json:"color,omitempty"` // web UI colour, see model.ViewColors
}
// ViewColor sets the colour the web UI shows a view in.
type ViewColor struct {
Color string `json:"color"`
}
// 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"`
// TOTPRequired says that totp.require in jarl.yaml makes two-factor
// login compulsory for the user.
TOTPRequired bool `json:"totp_required,omitempty"`
}
// 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"`
// TOTP is the user's two-factor login (sessions only).
TOTP *TOTPStatus `json:"totp,omitempty"`
// TOTPSetupRequired: the user must set up two-factor login before
// doing anything else (totp.require).
TOTPSetupRequired bool `json:"totp_setup_required,omitempty"`
// Build is what the server was built from (release or commit).
Build version.Build `json:"build"`
}
// LoginChallenge answers a correct password of a user with two-factor
// login: send a code with the challenge to POST /auth/login/totp.
type LoginChallenge struct {
TOTPRequired bool `json:"totp_required"`
Challenge string `json:"challenge"`
}
// LoginTOTP is the second login step.
type LoginTOTP struct {
Challenge string `json:"challenge"`
// Code is the current code from the authenticator app, or a recovery
// code.
Code string `json:"code"`
}
// TOTPStatus is a user's two-factor login (GET /me/totp).
type TOTPStatus struct {
Enabled bool `json:"enabled"`
EnabledAt *time.Time `json:"enabled_at,omitempty"`
Required bool `json:"required"`
RecoveryCodesLeft int `json:"recovery_codes_left"`
// Policy is totp.require: none, admins or all.
Policy string `json:"policy"`
}
// TOTPSetupRequest starts setting up two-factor login.
type TOTPSetupRequest struct {
Password string `json:"password"`
}
// TOTPSetup is a new secret to put into an authenticator app: QR is the
// QR code of URI, as an image data: URI.
type TOTPSetup struct {
Secret string `json:"secret"`
URI string `json:"uri"`
QR string `json:"qr"`
Issuer string `json:"issuer"`
Account string `json:"account"`
}
// TOTPCode confirms a setup.
type TOTPCode struct {
Code string `json:"code"`
}
// TOTPDisable turns two-factor login off.
type TOTPDisable struct {
Password string `json:"password"`
Code string `json:"code"`
}
// RecoveryCodes are new recovery codes, shown once.
type RecoveryCodes struct {
Codes []string `json:"recovery_codes"`
}
// 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"`
Build version.Build `json:"build"`
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"`
// TOTP is about two-factor login across users (administrators only).
TOTP *TOTPOverview `json:"totp,omitempty"`
}
// TOTPOverview is what the dashboard reports about two-factor login.
type TOTPOverview struct {
// Policy is totp.require: none, admins or all.
Policy string `json:"policy"`
// Missing are enabled users who must use two-factor login and
// haven't set it up (they can do nothing else until they do).
Missing []string `json:"missing"`
// SharedAdmin is set when an enabled administrator called "admin"
// (the usual first account) exists next to other enabled
// administrators: it is probably shared, and worth disabling.
SharedAdmin string `json:"shared_admin,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"`
}
// DNSSECResponse is GET /zones/{zone}/dnssec.
type DNSSECResponse struct {
Zone string `json:"zone"`
View string `json:"view,omitempty"`
Type string `json:"type"`
*dnssec.Status
// ParentZone is set when the parent zone is a primary zone in Jarl
// (in the same view): the DS record then goes there, as a record.
ParentZone string `json:"parent_zone,omitempty"`
// Registrar names the registrar Jarl can publish the DS at
// ("porkbun"), when one is configured in jarl.yaml.
Registrar string `json:"registrar,omitempty"`
}
// CheckDSResponse is POST /zones/{zone}/dnssec/check-ds: what the parent
// zone has, and what named was told.
type CheckDSResponse struct {
Parent *nscheck.ParentDSReport `json:"parent"`
Marked []string `json:"marked"`
Notes []string `json:"notes"`
Status *dnssec.Status `json:"status"`
}
// DNSSECWipe is POST /zones/{zone}/dnssec/wipe.
type DNSSECWipe struct {
// Force wipes although the parent zone's DS records couldn't be
// checked, or named still counts a removed DS as cached.
Force bool `json:"force,omitempty"`
DryRun bool `json:"dry_run,omitempty"`
}
// DNSSECWipeResponse is the result of a wipe.
type DNSSECWipeResponse struct {
Removed []string `json:"removed"`
DryRun bool `json:"dry_run,omitempty"`
ChangesetID int64 `json:"changeset_id,omitempty"`
Parent *nscheck.ParentDSReport `json:"parent,omitempty"`
}
// RegistrarResponse is GET /zones/{zone}/registrar: the domain's
// delegation at the registrar, next to what the zone says.
type RegistrarResponse struct {
Registrar string `json:"registrar"` // "porkbun"
Domain string `json:"domain"`
// NS are the name servers at the registry, JarlNS those in the
// zone's NS records (both without trailing dots).
NS []string `json:"ns"`
JarlNS []string `json:"jarl_ns"`
NSMatch bool `json:"ns_match"`
// Glue covers every glue record at the registry and every name server
// inside the zone that needs one.
Glue []GlueStatus `json:"glue"`
// DS are the DS records at the registry.
DS []RegistrarDS `json:"ds"`
Findings []nscheck.Finding `json:"findings"`
// Error is set when the registrar couldn't be asked; the rest is
// then empty.
Error string `json:"error,omitempty"`
}
// GlueStatus compares a glue record at the registry with the zone.
type GlueStatus struct {
Host string `json:"host"`
Registry []string `json:"registry"` // addresses at the registry (none: no glue record)
Jarl []string `json:"jarl"` // the zone's A and AAAA records for the host
// Needed: a name server in the zone's NS records that is inside the
// domain, so resolvers need its glue. InUse: in the registry's NS.
Needed bool `json:"needed"`
InUse bool `json:"in_use"`
// Action is what syncing the glue does: "create", "update" or "".
Action string `json:"action,omitempty"`
}
// RegistrarDS is a DS record at the registry.
type RegistrarDS struct {
porkbun.DS
// Matches: it points at a key named publishes for the zone.
Matches bool `json:"matches"`
}
// RegistrarChange asks for a change at the registrar.
type RegistrarChange struct {
// NS (set-ns) are the name servers to set (default: the zone's NS
// records).
NS []string `json:"ns,omitempty"`
// KeyTag (publish DS) picks the key (default: every key that needs
// one).
KeyTag uint16 `json:"key_tag,omitempty"`
// Force skips the checks that the change won't break the domain.
Force bool `json:"force,omitempty"`
DryRun bool `json:"dry_run,omitempty"`
}
// RegistrarChangeResponse is the result of a change at the registrar.
type RegistrarChangeResponse struct {
// Done lists what was changed (or would be, in a dry run).
Done []string `json:"done"`
DryRun bool `json:"dry_run,omitempty"`
Checks []nscheck.Finding `json:"checks"`
Warnings []string `json:"warnings"`
}