Files
jarl/internal/model/changeset.go
T
abrown38andClaude Opus 5.5 bd6b8778e4 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>
2026-10-07 15:03:13 +00:00

136 lines
5.1 KiB
Go

package model
import (
"encoding/json"
"time"
)
// Changeset statuses.
const (
ChangesetOpen = "open"
ChangesetApplied = "applied"
ChangesetDiscarded = "discarded"
)
// Op is one operation in a changeset, for example "record.create".
type Op struct {
Seq int `json:"seq"`
Kind string `json:"op"`
// ID is the object the operation acts on. For create operations it is
// allocated when the operation is added, so later operations in the
// same changeset can refer to the new object.
ID int64 `json:"id,omitempty"`
// BaseVersion is the version of the object when the operation was
// staged. Applying fails with a conflict if the object has changed
// since (0 skips the check).
BaseVersion int `json:"base_version,omitempty"`
Data json.RawMessage `json:"data,omitempty"`
Summary string `json:"summary,omitempty"`
AddedAt time.Time `json:"added_at"`
}
// Changeset is a group of operations applied together.
type Changeset struct {
ID int64 `json:"id"`
Description string `json:"description"`
Status string `json:"status"`
UserID int64 `json:"user_id"`
Username string `json:"username"`
Via string `json:"via"` // web, api, cli, import, system
Ops []Op `json:"ops"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
AppliedAt *time.Time `json:"applied_at,omitempty"`
AppliedBy string `json:"applied_by,omitempty"`
Result json.RawMessage `json:"result,omitempty"`
Diff string `json:"diff,omitempty"`
}
// Grant gives access to a kind of resource. Filters narrow it: empty
// filters match everything.
type Grant struct {
Resource string `json:"resource"` // zones, records, views, acls, tsig, options, audit, *
Access string `json:"access"` // read or write (write implies read)
Zones []string `json:"zones,omitempty"`
Views []string `json:"views,omitempty"`
Names []string `json:"names,omitempty"`
Types []string `json:"types,omitempty"`
}
// User is a person or service account.
type User struct {
ID int64 `json:"id"`
Username string `json:"username"`
PasswordHash string `json:"-"`
HasPassword bool `json:"has_password"`
Admin bool `json:"admin"`
Grants []Grant `json:"grants"`
Disabled bool `json:"disabled"`
Comment string `json:"comment"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
LastLoginAt *time.Time `json:"last_login_at,omitempty"`
}
// Token is an API key. The secret is shown once at creation and only its
// hash is stored.
type Token struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
Username string `json:"username,omitempty"`
Name string `json:"name"`
PublicID string `json:"public_id"`
SecretHash string `json:"-"`
// Grants restrict the token to a subset of its user's permissions;
// nil means the token has all of the user's permissions.
Grants []Grant `json:"grants"`
ExpiresAt *time.Time `json:"expires_at,omitempty"`
CreatedAt time.Time `json:"created_at"`
LastUsedAt *time.Time `json:"last_used_at,omitempty"`
LastUsedIP string `json:"last_used_ip,omitempty"`
RevokedAt *time.Time `json:"revoked_at,omitempty"`
}
// Session is a logged-in web session.
type Session struct {
IDHash string `json:"-"`
UserID int64 `json:"user_id"`
CSRFToken string `json:"-"`
CreatedAt time.Time `json:"created_at"`
ExpiresAt time.Time `json:"expires_at"`
LastSeenAt time.Time `json:"last_seen_at"`
IP string `json:"ip"`
UserAgent string `json:"user_agent"`
}
// AuditEvent records an action.
type AuditEvent struct {
ID int64 `json:"id"`
Time time.Time `json:"time"`
UserID int64 `json:"user_id,omitempty"`
Username string `json:"username,omitempty"`
Auth string `json:"auth,omitempty"` // session, token, local
TokenID int64 `json:"token_id,omitempty"`
TokenName string `json:"token_name,omitempty"`
IP string `json:"ip,omitempty"`
UserAgent string `json:"user_agent,omitempty"`
Action string `json:"action"`
ObjectType string `json:"object_type,omitempty"`
ObjectID int64 `json:"object_id,omitempty"`
ObjectName string `json:"object_name,omitempty"`
ChangesetID int64 `json:"changeset_id,omitempty"`
Outcome string `json:"outcome"` // success, failure, denied
Message string `json:"message,omitempty"`
Details json.RawMessage `json:"details,omitempty"`
}
// DeployedFile records a file Jarl wrote, for drift detection.
type DeployedFile struct {
Path string `json:"path"`
SHA256 string `json:"sha256"`
Size int64 `json:"size"`
Kind string `json:"kind"`
WrittenAt time.Time `json:"written_at"`
ChangesetID int64 `json:"changeset_id"`
}