A token for one view of a linked zone (an ACME token for external) couldn't add records: a new record was shared and reached the other view. A new name and type is now local when the user may not change it in the other views. A zone name in several views resolves to the only one the user can see, and the error no longer names views they can't. GET /views lists the views a user sees zones in (name, colour, count; limited) without views:read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
450 lines
22 KiB
Go
450 lines
22 KiB
Go
package engine
|
|
|
|
import (
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
|
|
"jarl/internal/auth"
|
|
"jarl/internal/model"
|
|
"jarl/internal/validate"
|
|
)
|
|
|
|
// linkedEnv has views internal and external and zone example.com in
|
|
// internal with a few records.
|
|
func linkedEnv(t *testing.T) *testEnv {
|
|
env := newEnv(t)
|
|
env.submit(env.admin, NewRequest(OpSettingsUpdate, 0, SettingsInput{Nameservers: &[]string{"ns1.example.net."}}))
|
|
env.submit(env.admin,
|
|
NewRequest(OpViewCreate, 0, ViewInput{Name: model.Ptr("internal"), MatchClients: &[]string{"10.0.0.0/8"}}),
|
|
NewRequest(OpViewCreate, 0, ViewInput{Name: model.Ptr("external"), MatchClients: &[]string{"any"}}))
|
|
env.submit(env.admin, NewRequest(OpZoneCreate, 0, ZoneInput{Name: model.Ptr("example.com"), View: model.Ptr("internal")}))
|
|
env.submit(env.admin,
|
|
recIn("internal", "www", "A", "192.0.2.10"),
|
|
recIn("internal", "mail", "A", "192.0.2.25"),
|
|
recIn("internal", "@", "MX", "10 mail"))
|
|
return env
|
|
}
|
|
|
|
func recIn(view, name, typ, data string) Request {
|
|
return NewRequest(OpRecordCreate, 0, RecordInput{Zone: "example.com", View: view, Name: &name, Type: &typ, Data: &data})
|
|
}
|
|
|
|
// view returns a view's records of example.com as "name type data S|L",
|
|
// S for shared, L for local.
|
|
func (env *testEnv) view(name string) []string {
|
|
env.t.Helper()
|
|
s := env.state()
|
|
v := s.ViewByName(name)
|
|
z := s.FindZone("example.com.", v.ID)
|
|
var out []string
|
|
for _, r := range s.ZoneRecords(z.ID) {
|
|
if r.Type == "NS" {
|
|
continue
|
|
}
|
|
mark := "L"
|
|
if r.SharedID != 0 {
|
|
mark = "S"
|
|
}
|
|
if r.Disabled {
|
|
mark += " disabled"
|
|
}
|
|
out = append(out, strings.TrimSuffix(r.Name, ".example.com.")+" "+r.Type+" "+r.Data+" "+mark)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (env *testEnv) recordID(view, name, typ string) int64 {
|
|
env.t.Helper()
|
|
s := env.state()
|
|
z := s.FindZone("example.com.", s.ViewByName(view).ID)
|
|
for _, r := range s.ZoneRecords(z.ID) {
|
|
if r.Name == name+".example.com." && r.Type == typ {
|
|
return r.ID
|
|
}
|
|
}
|
|
env.t.Fatalf("no %s %s in %s", name, typ, view)
|
|
return 0
|
|
}
|
|
|
|
func want(t *testing.T, what string, got []string, w ...string) {
|
|
t.Helper()
|
|
if !slices.Equal(got, w) {
|
|
t.Errorf("%s:\n got %q\n want %q", what, got, w)
|
|
}
|
|
}
|
|
|
|
func TestLinkedZones(t *testing.T) {
|
|
env := linkedEnv(t)
|
|
// A zone created linked copies everything; both are linked and shared.
|
|
env.submit(env.admin, NewRequest(OpZoneCreate, 0, ZoneInput{Name: model.Ptr("example.com"), View: model.Ptr("external"), LinkWith: "internal"}))
|
|
all := []string{"mail A 192.0.2.25 S", "www A 192.0.2.10 S", "example.com. MX 10 mail.example.com. S"}
|
|
slices.Sort(all)
|
|
got := env.view("external")
|
|
slices.Sort(got)
|
|
want(t, "linked copy", got, all...)
|
|
s := env.state()
|
|
if zs := s.ZonesNamed("example.com."); zs[0].LinkGroup == 0 || zs[0].LinkGroup != zs[1].LinkGroup {
|
|
t.Fatalf("not linked: %+v %+v", zs[0], zs[1])
|
|
}
|
|
|
|
// Adding, changing, disabling and deleting a shared record reaches the other view.
|
|
env.submit(env.admin, recIn("external", "ftp", "CNAME", "www"))
|
|
if !slices.Contains(env.view("internal"), "ftp CNAME www.example.com. S") {
|
|
t.Errorf("shared add didn't reach internal: %q", env.view("internal"))
|
|
}
|
|
env.submit(env.admin, NewRequest(OpRecordUpdate, env.recordID("internal", "mail", "A"), RecordInput{Data: model.Ptr("192.0.2.26")}))
|
|
if !slices.Contains(env.view("external"), "mail A 192.0.2.26 S") {
|
|
t.Errorf("shared change didn't reach external: %q", env.view("external"))
|
|
}
|
|
env.submit(env.admin, NewRequest(OpRecordDisable, 0, RecordInput{Zone: "example.com", View: "external", Name: model.Ptr("ftp"), Type: model.Ptr("CNAME")}))
|
|
if !slices.Contains(env.view("internal"), "ftp CNAME www.example.com. S disabled") {
|
|
t.Errorf("disable didn't reach internal: %q", env.view("internal"))
|
|
}
|
|
env.submit(env.admin, NewRequest(OpRecordDelete, 0, RecordInput{Zone: "example.com", View: "internal", Name: model.Ptr("ftp"), Type: model.Ptr("CNAME")}))
|
|
for _, v := range []string{"internal", "external"} {
|
|
for _, r := range env.view(v) {
|
|
if strings.HasPrefix(r, "ftp ") {
|
|
t.Errorf("delete by match left %s in %s", r, v)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A local override: www in internal points elsewhere; external keeps
|
|
// the shared one.
|
|
res := env.submit(env.admin, NewRequest(OpRecordCreate, 0, RecordInput{Zone: "example.com", View: "internal", Name: model.Ptr("www"), Type: model.Ptr("A"), Data: model.Ptr("10.0.0.10"), Local: model.Ptr(true)}))
|
|
if !slices.Contains(env.view("internal"), "www A 10.0.0.10 L") || slices.Contains(env.view("internal"), "www A 192.0.2.10 S") {
|
|
t.Errorf("override: internal %q", env.view("internal"))
|
|
}
|
|
if !slices.Contains(env.view("external"), "www A 192.0.2.10 S") {
|
|
t.Errorf("override changed external: %q", env.view("external"))
|
|
}
|
|
_ = res
|
|
// A new record of an overridden set joins it as local.
|
|
env.submit(env.admin, recIn("internal", "www", "A", "10.0.0.11"))
|
|
if !slices.Contains(env.view("internal"), "www A 10.0.0.11 L") || len(env.view("external")) != 3 {
|
|
t.Errorf("join override: internal %q external %q", env.view("internal"), env.view("external"))
|
|
}
|
|
// Changing the shared www in external doesn't touch internal's override.
|
|
env.submit(env.admin, NewRequest(OpRRSetReplace, 0, RRSetInput{Zone: "example.com", View: "external", Name: "www", Type: "A", Data: []string{"192.0.2.11"}}))
|
|
if !slices.Contains(env.view("external"), "www A 192.0.2.11 S") || slices.Contains(env.view("internal"), "www A 192.0.2.11 S") {
|
|
t.Errorf("shared set with an override elsewhere: internal %q external %q", env.view("internal"), env.view("external"))
|
|
}
|
|
// Removing the override removes www A from internal only: external
|
|
// keeps its record, local now. Sharing it brings it back to internal.
|
|
env.submit(env.admin, NewRequest(OpRRSetReplace, 0, RRSetInput{Zone: "example.com", View: "internal", Name: "www", Type: "A", Data: []string{}}))
|
|
if slices.ContainsFunc(env.view("internal"), func(s string) bool { return strings.HasPrefix(s, "www A ") }) || !slices.Contains(env.view("external"), "www A 192.0.2.11 L") {
|
|
t.Errorf("override removed: internal %q external %q", env.view("internal"), env.view("external"))
|
|
}
|
|
env.submit(env.admin, NewRequest(OpRecordUpdate, env.recordID("external", "www", "A"), RecordInput{Local: model.Ptr(false)}))
|
|
if !slices.Contains(env.view("internal"), "www A 192.0.2.11 S") || !slices.Contains(env.view("external"), "www A 192.0.2.11 S") {
|
|
t.Errorf("shared again: internal %q external %q", env.view("internal"), env.view("external"))
|
|
}
|
|
|
|
// Make a shared set local, then share another view's version.
|
|
env.submit(env.admin, NewRequest(OpRecordUpdate, env.recordID("external", "mail", "A"), RecordInput{Local: model.Ptr(true)}))
|
|
env.submit(env.admin, NewRequest(OpRecordUpdate, env.recordID("external", "mail", "A"), RecordInput{Data: model.Ptr("198.51.100.25")}))
|
|
if !slices.Contains(env.view("external"), "mail A 198.51.100.25 L") || !slices.Contains(env.view("internal"), "mail A 192.0.2.26 S") {
|
|
t.Errorf("local edit: internal %q external %q", env.view("internal"), env.view("external"))
|
|
}
|
|
env.submit(env.admin, NewRequest(OpRecordUpdate, env.recordID("external", "mail", "A"), RecordInput{Local: model.Ptr(false)}))
|
|
if !slices.Contains(env.view("internal"), "mail A 198.51.100.25 S") || slices.Contains(env.view("internal"), "mail A 192.0.2.26 S") {
|
|
t.Errorf("share external's version: internal %q", env.view("internal"))
|
|
}
|
|
if probs := env.e.Validate(env.state()); hasCode(probs, "linked_out_of_sync") || hasCode(probs, "linked_mixed") {
|
|
t.Errorf("validation: %+v", probs)
|
|
}
|
|
|
|
// Undoing a changeset undoes it in both views.
|
|
r := env.submit(env.admin, NewRequest(OpRecordUpdate, env.recordID("internal", "mail", "A"), RecordInput{Data: model.Ptr("198.51.100.99")}))
|
|
cs, err := env.e.Revert(env.ctx, r.ChangesetID, env.admin)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := env.e.Apply(env.ctx, cs.ID, env.admin, ApplyOptions{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, v := range []string{"internal", "external"} {
|
|
if !slices.Contains(env.view(v), "mail A 198.51.100.25 S") {
|
|
t.Errorf("revert in %s: %q", v, env.view(v))
|
|
}
|
|
}
|
|
|
|
// Unlinking keeps every record, all local.
|
|
env.submit(env.admin, NewRequest(OpZoneUnlink, 0, LinkInput{Zone: "example.com", View: "external"}))
|
|
for _, z := range env.state().ZonesNamed("example.com.") {
|
|
if z.LinkGroup != 0 {
|
|
t.Errorf("still linked: %s", zoneDesc(env.state(), z))
|
|
}
|
|
}
|
|
for _, v := range []string{"internal", "external"} {
|
|
for _, r := range env.view(v) {
|
|
if strings.HasSuffix(r, " S") {
|
|
t.Errorf("still shared after unlink in %s: %s", v, r)
|
|
}
|
|
}
|
|
}
|
|
env.submit(env.admin, recIn("internal", "solo", "A", "10.0.0.5"))
|
|
if slices.Contains(env.view("external"), "solo A 10.0.0.5 L") {
|
|
t.Error("an unlinked zone was changed")
|
|
}
|
|
|
|
// Linking again works out what is shared: same in both, or not.
|
|
env.submit(env.admin, NewRequest(OpZoneLink, 0, LinkInput{Zone: "example.com", View: "internal"}))
|
|
in, ex := env.view("internal"), env.view("external")
|
|
if !slices.Contains(in, "mail A 198.51.100.25 S") || !slices.Contains(ex, "mail A 198.51.100.25 S") {
|
|
t.Errorf("relink: same records not shared: internal %q external %q", in, ex)
|
|
}
|
|
if !slices.Contains(in, "solo A 10.0.0.5 L") {
|
|
t.Errorf("relink: internal-only record should be local: %q", in)
|
|
}
|
|
if probs := env.e.Validate(env.state()); hasCode(probs, "linked_out_of_sync") || hasCode(probs, "linked_mixed") {
|
|
t.Errorf("validation after relink: %+v", probs)
|
|
}
|
|
|
|
// Deleting one zone of two dissolves the link.
|
|
z := env.state().FindZone("example.com.", env.state().ViewByName("external").ID)
|
|
env.submit(env.admin, NewRequest(OpZoneDelete, z.ID, nil))
|
|
if zs := env.state().ZonesNamed("example.com."); len(zs) != 1 || zs[0].LinkGroup != 0 {
|
|
t.Errorf("after deleting external: %+v", zs)
|
|
}
|
|
for _, r := range env.view("internal") {
|
|
if strings.HasSuffix(r, " S") {
|
|
t.Errorf("shared record left in an unlinked zone: %s", r)
|
|
}
|
|
}
|
|
}
|
|
|
|
func hasCode(probs []validate.Problem, code string) bool {
|
|
for _, p := range probs {
|
|
if p.Code == code {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func TestLinkedPermissions(t *testing.T) {
|
|
env := linkedEnv(t)
|
|
env.submit(env.admin, NewRequest(OpZoneCreate, 0, ZoneInput{Name: model.Ptr("example.com"), View: model.Ptr("external"), LinkWith: "internal"}))
|
|
u := &model.User{Username: "lan", Grants: []model.Grant{{Resource: auth.ResRecords, Access: "write", Views: []string{"internal"}}, {Resource: auth.ResZones, Access: "read"}}}
|
|
if err := env.st.CreateUser(env.ctx, u); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
lan := &auth.Principal{User: u, Method: "token"}
|
|
// A shared change would reach external, which this user may not change.
|
|
e := env.submitErr(lan, NewRequest(OpRecordUpdate, env.recordID("internal", "www", "A"), RecordInput{Data: model.Ptr("10.9.9.9")}))
|
|
if e.Kind != KindForbidden || !strings.Contains(e.Message, "shared with example.com [external]") || !strings.Contains(e.Message, "local") {
|
|
t.Errorf("shared change: %v", e)
|
|
}
|
|
// A local override is theirs to make.
|
|
env.submit(lan, NewRequest(OpRecordCreate, 0, RecordInput{Zone: "example.com", View: "internal", Name: model.Ptr("www"), Type: model.Ptr("A"), Data: model.Ptr("10.9.9.9"), Local: model.Ptr(true)}))
|
|
if !slices.Contains(env.view("internal"), "www A 10.9.9.9 L") || !slices.Contains(env.view("external"), "www A 192.0.2.10 S") {
|
|
t.Errorf("override by a view-restricted user: internal %q external %q", env.view("internal"), env.view("external"))
|
|
}
|
|
// Records of a zone that isn't linked can't be "local".
|
|
e = env.submitErr(env.admin, NewRequest(OpZoneCreate, 0, ZoneInput{Name: model.Ptr("example.org"), View: model.Ptr("internal"), LinkWith: "external"}))
|
|
if e.Kind != KindNotFound {
|
|
t.Errorf("link_with a view without the zone: %v", e)
|
|
}
|
|
}
|
|
|
|
func TestLinkExisting(t *testing.T) {
|
|
env := linkedEnv(t)
|
|
// external was made by hand: the same apart from www.
|
|
env.submit(env.admin, NewRequest(OpZoneCreate, 0, ZoneInput{Name: model.Ptr("example.com"), View: model.Ptr("external")}))
|
|
env.submit(env.admin,
|
|
recIn("external", "www", "A", "203.0.113.10"),
|
|
recIn("external", "mail", "A", "192.0.2.25"),
|
|
recIn("external", "@", "MX", "10 mail"))
|
|
before := []string{}
|
|
for _, v := range []string{"internal", "external"} {
|
|
before = append(before, strings.Join(env.view(v), ";"))
|
|
}
|
|
res := env.submit(env.admin, NewRequest(OpZoneLink, 0, LinkInput{Zone: "example.com", View: "external", With: []string{"internal"}}))
|
|
in, ex := env.view("internal"), env.view("external")
|
|
if !slices.Contains(in, "www A 192.0.2.10 L") || !slices.Contains(ex, "www A 203.0.113.10 L") {
|
|
t.Errorf("different www should stay local: internal %q external %q", in, ex)
|
|
}
|
|
if !slices.Contains(in, "mail A 192.0.2.25 S") || !slices.Contains(ex, "mail A 192.0.2.25 S") {
|
|
t.Errorf("same mail should be shared: internal %q external %q", in, ex)
|
|
}
|
|
if !strings.Contains(strings.Join(res.Warnings, " "), "www.example.com A (internal, external)") {
|
|
t.Errorf("warnings: %q", res.Warnings)
|
|
}
|
|
// Linking never changes what named answers: the same data as before.
|
|
for i, v := range []string{"internal", "external"} {
|
|
var data []string
|
|
for _, r := range env.view(v) {
|
|
data = append(data, strings.TrimSuffix(strings.TrimSuffix(r, " S"), " L"))
|
|
}
|
|
var was []string
|
|
for _, r := range strings.Split(before[i], ";") {
|
|
was = append(was, strings.TrimSuffix(strings.TrimSuffix(r, " S"), " L"))
|
|
}
|
|
want(t, "answers in "+v, data, was...)
|
|
}
|
|
// Sharing internal's www replaces external's.
|
|
env.submit(env.admin, NewRequest(OpRecordUpdate, env.recordID("internal", "www", "A"), RecordInput{Local: model.Ptr(false)}))
|
|
if !slices.Contains(env.view("external"), "www A 192.0.2.10 S") {
|
|
t.Errorf("shared from internal: external %q", env.view("external"))
|
|
}
|
|
}
|
|
|
|
// --local on a zone that isn't linked is fine (its records are local
|
|
// anyway); --shared there is refused.
|
|
func TestLocalOnUnlinkedZone(t *testing.T) {
|
|
env := linkedEnv(t)
|
|
local, shared := true, false
|
|
env.submit(env.admin, NewRequest(OpRecordCreate, 0, RecordInput{Zone: "example.com", View: "internal", Name: model.Ptr("a"), Type: model.Ptr("A"), Data: model.Ptr("10.0.0.1"), Local: &local}))
|
|
env.submit(env.admin, NewRequest(OpRRSetReplace, 0, RRSetInput{Zone: "example.com", View: "internal", Name: "b", Type: "A", Data: []string{"10.0.0.2"}, Local: &local}))
|
|
env.submit(env.admin, NewRequest(OpRecordUpdate, env.recordID("internal", "a", "A"), RecordInput{Data: model.Ptr("10.0.0.3"), Local: &local}))
|
|
if v := env.view("internal"); !slices.Contains(v, "a A 10.0.0.3 L") || !slices.Contains(v, "b A 10.0.0.2 L") {
|
|
t.Errorf("records: %q", v)
|
|
}
|
|
if e := env.submitErr(env.admin, NewRequest(OpRecordCreate, 0, RecordInput{Zone: "example.com", View: "internal", Name: model.Ptr("c"), Type: model.Ptr("A"), Data: model.Ptr("10.0.0.4"), Local: &shared})); !strings.Contains(e.Message, "nothing to share") {
|
|
t.Errorf("--shared on an unlinked zone: %v", e)
|
|
}
|
|
}
|
|
|
|
func TestCompareZones(t *testing.T) {
|
|
env := linkedEnv(t)
|
|
env.submit(env.admin, NewRequest(OpZoneCreate, 0, ZoneInput{Name: model.Ptr("example.com"), View: model.Ptr("external"), LinkWith: "internal"}))
|
|
env.submit(env.admin, NewRequest(OpRRSetReplace, 0, RRSetInput{Zone: "example.com", View: "external", Name: "www", Type: "A", Data: []string{"203.0.113.10"}, Local: model.Ptr(true)}))
|
|
env.submit(env.admin, NewRequest(OpRecordCreate, 0, RecordInput{Zone: "example.com", View: "internal", Name: model.Ptr("nas"), Type: model.Ptr("A"), Data: model.Ptr("10.0.0.20"), Local: model.Ptr(true)}))
|
|
st := env.state()
|
|
zones := st.ZonesNamed("example.com.")
|
|
st.SortByView(zones)
|
|
diff := map[string][]string{}
|
|
same := 0
|
|
for _, row := range CompareZones(st, zones) {
|
|
if row.Same {
|
|
same++
|
|
continue
|
|
}
|
|
var cells []string
|
|
for _, vs := range row.Views {
|
|
cells = append(cells, vs.View+"="+strings.Join(vs.Records, ",")+"/"+vs.Linked)
|
|
}
|
|
diff[row.Name+" "+row.Type] = cells
|
|
}
|
|
want := map[string][]string{
|
|
"www A": {"internal=192.0.2.10/shared", "external=203.0.113.10/local"},
|
|
"nas A": {"internal=10.0.0.20/local", "external=/"},
|
|
}
|
|
for k, v := range want {
|
|
if !slices.Equal(diff[k], v) {
|
|
t.Errorf("%s: %q, want %q", k, diff[k], v)
|
|
}
|
|
}
|
|
if len(diff) != 2 || same < 3 {
|
|
t.Errorf("differ %v, same %d", diff, same)
|
|
}
|
|
// "Use this version" on the shared one replaces external's override.
|
|
env.submit(env.admin, NewRequest(OpRecordUpdate, env.recordID("internal", "www", "A"), RecordInput{Local: model.Ptr(false)}))
|
|
if v := env.view("external"); !slices.Contains(v, "www A 192.0.2.10 S") || slices.Contains(v, "www A 203.0.113.10 L") {
|
|
t.Errorf("sharing the shared version: external %q", v)
|
|
}
|
|
}
|
|
|
|
func TestCompareReport(t *testing.T) {
|
|
env := linkedEnv(t)
|
|
env.submit(env.admin, NewRequest(OpZoneCreate, 0, ZoneInput{Name: model.Ptr("example.com"), View: model.Ptr("external"), LinkWith: "internal"}))
|
|
env.submit(env.admin,
|
|
NewRequest(OpRRSetReplace, 0, RRSetInput{Zone: "example.com", View: "external", Name: "www", Type: "A", Data: []string{"203.0.113.10"}, Local: model.Ptr(true)}),
|
|
NewRequest(OpRecordCreate, 0, RecordInput{Zone: "example.com", View: "internal", Name: model.Ptr("nas"), Type: model.Ptr("A"), Data: model.Ptr("10.0.0.20"), Local: model.Ptr(true)}),
|
|
NewRequest(OpRecordCreate, 0, RecordInput{Zone: "example.com", View: "internal", Name: model.Ptr("printer"), Type: model.Ptr("A"), Data: model.Ptr("10.0.0.30"), Local: model.Ptr(true)}),
|
|
NewRequest(OpRecordCreate, 0, RecordInput{Zone: "example.com", View: "external", Name: model.Ptr("shop"), Type: model.Ptr("CNAME"), Data: model.Ptr("shops.example.net."), Local: model.Ptr(true)}))
|
|
st := env.state()
|
|
zones := []*model.Zone{st.FindZone("example.com.", st.ViewByName("internal").ID), st.FindZone("example.com.", st.ViewByName("external").ID)}
|
|
|
|
rep := Compare(st, zones, CompareFilter{})
|
|
if rep.Base != "internal" || !rep.Linked || !slices.Equal(rep.Views, []string{"internal", "external"}) {
|
|
t.Errorf("report %+v", rep)
|
|
}
|
|
if s := rep.Summary[0]; s.View != "external" || s.Different != 1 || s.OnlyBase != 2 || s.OnlyOther != 1 || s.Same != 2 {
|
|
t.Errorf("summary %+v", s)
|
|
}
|
|
if len(rep.Different) != 1 || rep.Different[0].Name != "www" {
|
|
t.Errorf("different %+v", rep.Different)
|
|
}
|
|
var only []string
|
|
for _, o := range rep.Only {
|
|
var names []string
|
|
for _, r := range o.Rows {
|
|
names = append(names, r.Name)
|
|
}
|
|
only = append(only, strings.Join(o.Views, "+")+": "+strings.Join(names, ","))
|
|
}
|
|
want(t, "only", only, "internal: nas,printer", "external: shop")
|
|
if rep.Same != 2 || rep.SameRows != nil {
|
|
t.Errorf("same %d, rows %v", rep.Same, rep.SameRows)
|
|
}
|
|
|
|
// One name: just its rows, the same ones included.
|
|
rep = Compare(st, zones, CompareFilter{Name: "www"})
|
|
if len(rep.Different) != 1 || len(rep.Only) != 0 || len(rep.SameRows) != 0 {
|
|
t.Errorf("www: %+v", rep)
|
|
}
|
|
rep = Compare(st, zones, CompareFilter{Name: "mail.example.com.", Type: "A"})
|
|
if len(rep.SameRows) != 1 || rep.SameRows[0].Name != "mail" || len(rep.Different)+len(rep.Only) != 0 {
|
|
t.Errorf("mail A: %+v", rep)
|
|
}
|
|
}
|
|
|
|
// A shared record made local in one view, then deleted there, is gone from
|
|
// that view only; the other view keeps it, local now.
|
|
func TestDeleteLocalOverride(t *testing.T) {
|
|
env := linkedEnv(t)
|
|
env.submit(env.admin, NewRequest(OpZoneCreate, 0, ZoneInput{Name: model.Ptr("example.com"), View: model.Ptr("external"), LinkWith: "internal"}))
|
|
env.submit(env.admin, recIn("internal", "a", "A", "192.0.2.50"))
|
|
if !slices.Contains(env.view("external"), "a A 192.0.2.50 S") {
|
|
t.Fatalf("not shared: %q", env.view("external"))
|
|
}
|
|
env.submit(env.admin, NewRequest(OpRecordUpdate, env.recordID("internal", "a", "A"), RecordInput{Local: model.Ptr(true)}))
|
|
env.submit(env.admin, NewRequest(OpRecordDelete, env.recordID("internal", "a", "A"), nil))
|
|
if slices.ContainsFunc(env.view("internal"), func(s string) bool { return strings.HasPrefix(s, "a A ") }) {
|
|
t.Errorf("internal still has a: %q", env.view("internal"))
|
|
}
|
|
if !slices.Contains(env.view("external"), "a A 192.0.2.50 L") {
|
|
t.Errorf("external: %q", env.view("external"))
|
|
}
|
|
if probs := env.e.Validate(env.state()); hasCode(probs, "linked_out_of_sync") || hasCode(probs, "linked_mixed") {
|
|
t.Errorf("validation: %+v", probs)
|
|
}
|
|
}
|
|
|
|
// An ACME token for one view of a linked zone: its challenge records are
|
|
// local to that view, the zone is found without naming the view, and they
|
|
// go again without touching the other view.
|
|
func TestLinkedACMEToken(t *testing.T) {
|
|
env := linkedEnv(t)
|
|
env.submit(env.admin, NewRequest(OpZoneCreate, 0, ZoneInput{Name: model.Ptr("example.com"), View: model.Ptr("external"), LinkWith: "internal"}))
|
|
u := &model.User{Username: "certbot", Grants: []model.Grant{{Resource: auth.ResRecords, Access: "write", Zones: []string{"example.com"}, Views: []string{"external"}, Names: []string{"_acme-challenge.*"}, Types: []string{"TXT"}}}}
|
|
if err := env.st.CreateUser(env.ctx, u); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
bot := &auth.Principal{User: u, Method: "token"}
|
|
name, typ, data := "_acme-challenge", "TXT", "token-value"
|
|
res := env.submit(bot, NewRequest(OpRecordCreate, 0, RecordInput{Zone: "example.com", Name: &name, Type: &typ, Data: &data}))
|
|
if !slices.Contains(env.view("external"), "_acme-challenge TXT \"token-value\" L") || slices.ContainsFunc(env.view("internal"), func(s string) bool { return strings.HasPrefix(s, "_acme-challenge") }) {
|
|
t.Errorf("challenge: internal %q external %q", env.view("internal"), env.view("external"))
|
|
}
|
|
if !slices.ContainsFunc(res.Warnings, func(w string) bool { return strings.Contains(w, "local to view external") }) {
|
|
t.Errorf("warnings %q", res.Warnings)
|
|
}
|
|
env.submit(bot, NewRequest(OpRecordDelete, 0, RecordInput{Zone: "example.com", Name: &name, Type: &typ, Data: &data}))
|
|
if slices.ContainsFunc(env.view("external"), func(s string) bool { return strings.HasPrefix(s, "_acme-challenge") }) {
|
|
t.Errorf("not deleted: %q", env.view("external"))
|
|
}
|
|
// The admin, who may change both, still gets a shared record.
|
|
env.submit(env.admin, NewRequest(OpRecordCreate, 0, RecordInput{Zone: "example.com", View: "internal", Name: model.Ptr("both"), Type: model.Ptr("A"), Data: model.Ptr("192.0.2.7")}))
|
|
if !slices.Contains(env.view("external"), "both A 192.0.2.7 S") {
|
|
t.Errorf("admin: %q", env.view("external"))
|
|
}
|
|
// Without a view, the admin is still asked which.
|
|
if e := env.submitErr(env.admin, NewRequest(OpRecordCreate, 0, RecordInput{Zone: "example.com", Name: model.Ptr("x"), Type: model.Ptr("A"), Data: model.Ptr("192.0.2.8")})); !strings.Contains(e.Message, "several views (internal, external)") {
|
|
t.Errorf("admin without a view: %v", e)
|
|
}
|
|
}
|