Client groups put devices (addresses, networks, or ACLs of them) at a blocking step: off (only your rules), normal (the default), or steps added above it, each including the lists of the steps below. Lists start at a step. In each view, a gate policy zone in front of a step's lists lets the devices below that step through (rpz-client-ip triggers with rpz-passthru), so every list is still loaded once. The IPv6 part of a gate leaves the IPv4-mapped range out: named matches IPv4 clients as ::ffff:a.b.c.d. Web UI: a Devices panel on the Blocking page and a step on each list. CLI: jarl block step, jarl block group, jarl blocklist --step. API: /blocking/steps, /blocking/groups. Database schema 8. Also: copying the state kept empty lists as empty (not nil), so reverting a changeset no longer adds restore operations for unchanged blocklists, rules and views. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
539 lines
19 KiB
Go
539 lines
19 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"`
|
|
// 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"`
|
|
// Step is the name of the step the list starts at ("normal" or one
|
|
// above it).
|
|
Step string `json:"step"`
|
|
}
|
|
|
|
// BlockStep is a blocking step above normal as returned by the API, with
|
|
// what uses it.
|
|
type BlockStep struct {
|
|
model.BlockStep
|
|
Lists []string `json:"lists"`
|
|
Groups []string `json:"groups"`
|
|
Pending string `json:"pending,omitempty"`
|
|
}
|
|
|
|
// ClientGroup is a client group as returned by the API.
|
|
type ClientGroup struct {
|
|
model.ClientGroup
|
|
Step string `json:"step"`
|
|
Views []string `json:"views"`
|
|
AutoViews bool `json:"auto_views,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"`
|
|
}
|