Jarl keeps a BIND 9.20 server's whole configuration (views, ACLs, TSIG keys, zones of every type, records, named.conf options) in SQLite and renders named.conf, keys.conf and zone files from it. Every change goes through one changeset pipeline: whole-configuration validation, named-checkconf -z and named-checkzone in a staging directory, atomic install, rndc reconfig/reload, serial verification with rndc zonestatus, and rollback if named does not load the change. Changes apply one at a time by default or are bundled and applied together. Applied changesets can be reverted. - jarld: the daemon with the REST API and an embedded web UI, plus import, adopt, check, resync, backup and local user recovery commands - jarl: the CLI, including ACME DNS-01 hooks for certbot and acme.sh - users with argon2id passwords and CSRF-protected sessions; hashed API tokens with granular grants and roles; an audit log - systemd unit, sysusers.d/tmpfiles.d files, Makefile - unit tests, integration tests against a real named, scripts/e2e.sh - docs/ guides, README and CHANGELOG Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
204 lines
6.4 KiB
Go
204 lines
6.4 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"
|
|
)
|
|
|
|
// 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"`
|
|
}
|
|
|
|
// 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"`
|
|
}
|
|
|
|
// 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"`
|
|
}
|