Jarl 0.1.0: manage BIND through a web UI, REST API and CLI

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>
This commit is contained in:
abrown38andClaude Opus 5.5 committed 2026-10-07 15:03:13 +00:00
commit bd6b8778e4
122 files changed
+29217

No files matched your search

+203
View File
@@ -0,0 +1,203 @@
// 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"`
}