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, "") } 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) }