// 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"` // DisabledRecords is how many of the records are disabled. DisabledRecords int `json:"disabled_count,omitempty"` 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, delete, // disable or enable. 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"` // Formatted is the value laid out over several lines, as in // named.conf, when it has a block (for display and editing; Value is // the same on one line). Formatted string `json:"formatted,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"` // BlocklistErrors are blocklists whose last update failed (for those // who may see blocking). BlocklistErrors []BlocklistError `json:"blocklist_errors,omitempty"` // BlockingPausedUntil is set while blocking is paused for a time, // BlockingPausedIndefinitely while it is paused until resumed. BlockingPausedUntil *time.Time `json:"blocking_paused_until,omitempty"` BlockingPausedIndefinitely bool `json:"blocking_paused_indefinitely,omitempty"` } // BlocklistError is a blocklist whose last update failed. UpdatedAt is // the version named keeps using (nil: none yet). type BlocklistError struct { Name string `json:"name"` Error string `json:"error"` UpdatedAt *time.Time `json:"updated_at,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"` ParentZoneID int64 `json:"parent_zone_id,omitempty"` ParentView string `json:"parent_view,omitempty"` // ParentDS is the DS records the parent zone in Jarl has for the // zone (record data, e.g. "51126 13 2 FFBE..."). ParentDS []string `json:"parent_ds,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"` } // Blocklist is a blocklist as returned by the API (docs/blocking.md). type Blocklist struct { model.Blocklist // Views are the views it applies to; AutoViews is set when that is // "every view that offers recursion" (view_ids is empty). Views []string `json:"views"` AutoViews bool `json:"auto_views,omitempty"` // State is ok, waiting (not downloaded yet), error (the last update // failed; the previous version stays in use), disabled, or idle (it // applies to no view). State string `json:"state"` Status *model.BlocklistStatus `json:"status,omitempty"` // MemoryMB estimates what named needs for the list, in all its views. MemoryMB int `json:"memory_mb,omitempty"` Pending string `json:"pending,omitempty"` } // BlockRule is a block or allow rule as returned by the API. type BlockRule struct { model.BlockRule Views []string `json:"views"` AutoViews bool `json:"auto_views,omitempty"` Pending string `json:"pending,omitempty"` }