Files
jarl/internal/engine/refs.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

214 lines
5.6 KiB
Go

package engine
import (
"fmt"
"sort"
"strings"
"jarl/internal/catalog"
"jarl/internal/dnsname"
"jarl/internal/model"
"jarl/internal/namedconf"
)
type refKind int
const (
refACL refKind = iota
refKey
)
func sameRef(kind refKind, a, b string) bool {
if kind == refKey {
return model.NormalizeKeyName(a) == model.NormalizeKeyName(b)
}
return strings.EqualFold(a, b)
}
// rewriteElements renames references in ACL-style element strings. It
// returns the new list and whether anything matched.
func rewriteElements(list []string, kind refKind, old, new string) ([]string, bool) {
changed := false
var fix func(es []namedconf.AMElement) []namedconf.AMElement
fix = func(es []namedconf.AMElement) []namedconf.AMElement {
for i := range es {
e := &es[i]
switch {
case kind == refACL && e.Kind == namedconf.AMName && !e.IsBuiltin() && sameRef(kind, e.Name, old):
e.Name, changed = new, true
case kind == refKey && e.Kind == namedconf.AMKey && sameRef(kind, e.Name, old):
e.Name, changed = new, true
case e.Kind == namedconf.AMNested:
e.Nested = fix(e.Nested)
}
}
return es
}
out := make([]string, len(list))
for i, s := range list {
e, err := namedconf.ParseAMElement(s)
if err != nil {
out[i] = s
continue
}
res := fix([]namedconf.AMElement{e})
out[i] = res[0].String()
}
return out, changed
}
// walkRefs calls fn for every reference of the given kind in items. fn may
// change the value's text.
func walkRefs(items []namedconf.Item, kind refKind, aml bool, fn func(v *namedconf.Value)) {
for i := 0; i < len(items); i++ {
it := items[i]
if it.Value != nil && !it.Value.Quoted && i+1 < len(items) {
switch strings.ToLower(it.Value.Text) {
case "key":
if kind == refKey && items[i+1].Value != nil {
fn(items[i+1].Value)
}
i++
continue
case "keys":
next := items[i+1]
if kind == refKey {
if next.Value != nil {
fn(next.Value)
} else if next.Block != nil {
for _, s := range next.Block.Stmts {
if len(s.Items) == 1 && s.Items[0].Value != nil {
fn(s.Items[0].Value)
}
}
}
}
i++
continue
}
}
if it.Block == nil {
continue
}
for _, s := range it.Block.Stmts {
if kind == refACL && aml {
var v *namedconf.Value
switch {
case len(s.Items) == 1 && s.Items[0].Value != nil:
v = s.Items[0].Value
case len(s.Items) == 2 && s.Items[0].Bang && s.Items[1].Value != nil:
v = s.Items[1].Value
}
if v != nil && !namedconf.BuiltinACLs[strings.ToLower(v.Text)] {
fn(v)
continue
}
}
walkRefs(s.Items, kind, aml, fn)
}
}
}
// optionHasAML reports whether the option's grammar contains address match
// lists (so single words in its blocks are ACL names).
func optionHasAML(st *model.State, o *model.Option) bool {
cs := catalogScope(st, o.Scope, o.ScopeID)
stmt, ok := catalog.Default().Lookup(cs, o.Name)
return ok && strings.Contains(stmt.Grammar, "<address_match_element>")
}
func optionLabel(st *model.State, o *model.Option) string {
switch o.Scope {
case model.ScopeView:
if v := st.Views[o.ScopeID]; v != nil {
return fmt.Sprintf("view %s option %s", v.Name, o.Name)
}
case model.ScopeZone:
if z := st.Zones[o.ScopeID]; z != nil {
label := "zone " + dnsname.Display(z.Name)
if v := st.ViewName(z); v != "" {
label += " [view " + v + "]"
}
return label + " option " + o.Name
}
case model.ScopeTopLevel:
return "top-level statement " + o.Name
}
return "server option " + o.Name
}
// FindRefs lists the objects that refer to an ACL or key by name.
func FindRefs(st *model.State, kind refKind, name string) []string {
var out []string
for _, a := range st.SortedACLs() {
if _, hit := rewriteElements(a.Elements, kind, name, name); hit {
out = append(out, "ACL "+a.Name)
}
}
for _, v := range st.OrderedViews() {
_, h1 := rewriteElements(v.MatchClients, kind, name, name)
_, h2 := rewriteElements(v.MatchDestinations, kind, name, name)
if h1 || h2 {
out = append(out, "view "+v.Name)
}
}
var opts []string
for _, o := range st.Options {
stmt, err := namedconf.ParseOptionValue(o.Name, o.Value)
if err != nil {
continue
}
hit := false
walkRefs(stmt.Items, kind, optionHasAML(st, o), func(v *namedconf.Value) {
if sameRef(kind, v.Text, name) {
hit = true
}
})
if hit {
opts = append(opts, optionLabel(st, o))
}
}
sort.Strings(opts)
return append(out, opts...)
}
// renameRefs rewrites references to an ACL or key after a rename.
func renameRefs(st *model.State, kind refKind, old, new string, c *applyCtx) {
for _, a := range st.ACLs {
if l, hit := rewriteElements(a.Elements, kind, old, new); hit {
a.Elements = l
a.Touch(c.user(), c.now)
}
}
for _, v := range st.Views {
l1, h1 := rewriteElements(v.MatchClients, kind, old, new)
l2, h2 := rewriteElements(v.MatchDestinations, kind, old, new)
if h1 || h2 {
v.MatchClients, v.MatchDestinations = l1, l2
v.Touch(c.user(), c.now)
}
}
for _, o := range st.Options {
stmt, err := namedconf.ParseOptionValue(o.Name, o.Value)
if err != nil {
continue
}
hit := false
walkRefs(stmt.Items, kind, optionHasAML(st, o), func(v *namedconf.Value) {
if sameRef(kind, v.Text, old) {
v.Text, hit = new, true
}
})
if hit {
o.Value = namedconf.Compact(stmt.Items[1:])
o.Touch(c.user(), c.now)
}
}
}
// FindACLRefs lists the objects that use the ACL called name.
func FindACLRefs(st *model.State, name string) []string { return FindRefs(st, refACL, name) }
// FindKeyRefs lists the objects that use the TSIG key called name.
func FindKeyRefs(st *model.State, name string) []string { return FindRefs(st, refKey, name) }