diff --git a/CHANGELOG.md b/CHANGELOG.md index ead2782..d10562d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,9 +55,13 @@ follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - The peer form's role-specific checkbox group for the *other* role (the iBGP switches on an eBGP peer, and vice versa) was visible, greyed out, instead of hidden: the group's flex layout overrode the `hidden` attribute. -- A peer rename that failed validation re-rendered the form posting to the new - name, so the corrected resubmit hit "not found" and the edit was lost. The form - now posts back to the name the peer is stored under. +- **A rename that failed validation could overwrite another object.** The peer, + prefix set, AS set, community, RPKI server, BMP station and policy forms + re-rendered posting to the name the operator had typed. If that name was free, + the corrected resubmit hit "not found" and the edit was lost; if it was taken — + the usual reason a rename is refused — the resubmit saved over the object that + held it, replacing its contents, and left the one being edited as it was. Every + form now posts back to the name the object is stored under. - **Two objects with one BIRD name are caught before BIRD is.** BIRD keeps defines, filters, functions, templates and protocols in one namespace, so a peer and a prefix set (or community, AS set, RPKI server, BMP station) of the same diff --git a/internal/web/assets.go b/internal/web/assets.go index c6fad2e..1e39b0e 100644 --- a/internal/web/assets.go +++ b/internal/web/assets.go @@ -29,6 +29,9 @@ type asSetFormView struct { Errs map[string]string Preview string PreviewErr string + // StoredName is the name the set is saved under. A form re-rendered after + // a failed rename posts back to it, not to the name the operator typed. + StoredName string } func (s *Server) handleASSetsList(w http.ResponseWriter, r *http.Request) { @@ -146,7 +149,8 @@ func (s *Server) handleASSetSave(w http.ResponseWriter, r *http.Request) { return } s.renderASSetForm(w, asSetFormView{ - Active: "library", ReadOnly: s.readOnly, IsNew: isNew, Set: as, EntryText: entryText, Errs: errs, + Active: "library", ReadOnly: s.readOnly, IsNew: isNew, StoredName: r.PathValue("name"), + Set: as, EntryText: entryText, Errs: errs, }) } diff --git a/internal/web/bmp.go b/internal/web/bmp.go index cfd7563..79801e6 100644 --- a/internal/web/bmp.go +++ b/internal/web/bmp.go @@ -24,6 +24,9 @@ type bmpFormView struct { IsNew bool Station store.BMPStation Errs map[string]string + // StoredName is the name the station is saved under. A form re-rendered + // after a failed rename posts back to it, not to the name the operator typed. + StoredName string } func (s *Server) handleBMPPage(w http.ResponseWriter, r *http.Request) { @@ -110,7 +113,7 @@ func (s *Server) handleBMPSave(w http.ResponseWriter, r *http.Request) { s.flashRedirect(w, r, "/bmp", "Saved "+st.Name, false) return } - render(w, s.log, "bmp_form.html", bmpFormView{Active: "bmp", ReadOnly: s.readOnly, IsNew: isNew, Station: st, Errs: errs}) + render(w, s.log, "bmp_form.html", bmpFormView{Active: "bmp", ReadOnly: s.readOnly, IsNew: isNew, StoredName: r.PathValue("name"), Station: st, Errs: errs}) } func (s *Server) handleBMPDelete(w http.ResponseWriter, r *http.Request) { diff --git a/internal/web/communities_handler_test.go b/internal/web/communities_handler_test.go index aa7f05b..1924264 100644 --- a/internal/web/communities_handler_test.go +++ b/internal/web/communities_handler_test.go @@ -101,6 +101,22 @@ func TestCommunityReferenceGuards(t *testing.T) { } } +// The in-use check is what keeps a delete from leaving an undefined symbol +// behind; when the check cannot run, the delete must not go ahead blind. +func TestCommunityDeleteRefusesWhenTheUseCheckFails(t *testing.T) { + env := newTestEnv(t, false) + if rec := env.do(t, "POST", "/library/communities/new", url.Values{"name": {"CUST_TAG"}, "value": {"65000:7"}}); rec.Code != 303 { + t.Fatalf("create: %d %s", rec.Code, rec.Body.String()) + } + sabotageDB(t, env, `ALTER TABLE peer_templates RENAME TO peer_templates_gone`) + if rec := env.do(t, "POST", "/library/communities/CUST_TAG/delete", nil); rec.Code == 303 { + t.Errorf("the delete went ahead although the in-use check failed: %q", flashOf(rec)) + } + if _, err := env.store.GetCommunityDefByName("CUST_TAG"); err != nil { + t.Errorf("the community should still exist: %v", err) + } +} + // A reserved name and a malformed value are rejected without creating anything. func TestCommunityValidation(t *testing.T) { env := applyReady(t) diff --git a/internal/web/library.go b/internal/web/library.go index b598423..bd204fd 100644 --- a/internal/web/library.go +++ b/internal/web/library.go @@ -26,6 +26,9 @@ type prefixSetFormView struct { Preview string PreviewErr string Bgpq4 bool // whether IRR expansion is enabled + // StoredName is the name the set is saved under. A form re-rendered after + // a failed rename posts back to it, not to the name the operator typed. + StoredName string } func (s *Server) handlePrefixSetsList(w http.ResponseWriter, r *http.Request) { @@ -148,7 +151,7 @@ func (s *Server) handlePrefixSetSave(w http.ResponseWriter, r *http.Request) { s.flashRedirect(w, r, "/library/prefix-sets", "Saved "+ps.Name, false) return } - s.renderPrefixSetForm(w, prefixSetFormView{Active: "library", ReadOnly: s.readOnly, IsNew: isNew, Set: ps, Errs: errs}) + s.renderPrefixSetForm(w, prefixSetFormView{Active: "library", ReadOnly: s.readOnly, IsNew: isNew, StoredName: r.PathValue("name"), Set: ps, Errs: errs}) } func (s *Server) handlePrefixSetDelete(w http.ResponseWriter, r *http.Request) { diff --git a/internal/web/library_communities.go b/internal/web/library_communities.go index 2dfd2ab..63e5055 100644 --- a/internal/web/library_communities.go +++ b/internal/web/library_communities.go @@ -26,6 +26,9 @@ type communityFormView struct { Value string Errs map[string]string Preview string + // StoredName is the name the community is saved under. A form re-rendered + // after a failed rename posts back to it, not to the name the operator typed. + StoredName string } func (s *Server) handleCommunitiesList(w http.ResponseWriter, r *http.Request) { @@ -125,7 +128,7 @@ func (s *Server) handleCommunitySave(w http.ResponseWriter, r *http.Request) { return } s.renderCommunityForm(w, communityFormView{ - Active: "library", ReadOnly: s.readOnly, IsNew: isNew, Def: cd, + Active: "library", ReadOnly: s.readOnly, IsNew: isNew, StoredName: r.PathValue("name"), Def: cd, Value: r.FormValue("value"), Errs: errs, }) } diff --git a/internal/web/library_rename_test.go b/internal/web/library_rename_test.go new file mode 100644 index 0000000..f4daa0e --- /dev/null +++ b/internal/web/library_rename_test.go @@ -0,0 +1,99 @@ +package web + +import ( + "fmt" + "net/http" + "net/url" + "regexp" + "testing" +) + +// editorAction is where a re-rendered library form posts. +var editorAction = regexp.MustCompile(`