One-view tokens: local records in linked zones, zone lookup, view list
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>
This commit is contained in:
1 parent
4cc2ab2287
commit
57012fece8
13 files changed
+159
-12
No files matched your search
@@ -49,3 +49,34 @@ func TestZoneInViews(t *testing.T) {
|
||||
t.Errorf("in_views = %v", z["in_views"])
|
||||
}
|
||||
}
|
||||
|
||||
// A token for records in one view of a zone that is in two: the zone is
|
||||
// found without naming the view, and view list names the views it can
|
||||
// see zones in, without their settings.
|
||||
func TestOneViewToken(t *testing.T) {
|
||||
e := newAPIEnv(t)
|
||||
e.do("POST", "/views", e.token, map[string]any{"name": "internal", "match_clients": []string{"10.0.0.0/8"}}, 201)
|
||||
e.do("POST", "/views", e.token, map[string]any{"name": "external"}, 201)
|
||||
for _, v := range []string{"internal", "external"} {
|
||||
e.do("POST", "/zones", e.token, map[string]any{"name": "adbraun.lol", "view": v, "nameservers": []string{"ns1.example.net."}}, 201)
|
||||
}
|
||||
e.do("POST", "/users", e.token, map[string]any{"username": "certbot", "password": "", "grant_strings": []string{"records:write zones=adbraun.lol views=external names=_acme-challenge.* types=TXT"}}, 201)
|
||||
var uid int64
|
||||
for _, u := range e.list("/users", e.token) {
|
||||
if u["username"] == "certbot" {
|
||||
uid = int64(u["id"].(float64))
|
||||
}
|
||||
}
|
||||
tok := e.do("POST", "/users/"+itoa(uid)+"/tokens", e.token, map[string]any{"name": "certbot"}, 201)["token"].(string)
|
||||
|
||||
if z := e.do("GET", "/zones/adbraun.lol", tok, nil, 200); z["view"] != "external" {
|
||||
t.Errorf("zone without a view: %v", z["view"])
|
||||
}
|
||||
e.do("GET", "/zones/adbraun.lol/records", tok, nil, 200)
|
||||
views := e.list("/views", tok)
|
||||
if len(views) != 1 || views[0]["name"] != "external" || views[0]["limited"] != true || views[0]["match_clients"] != nil {
|
||||
t.Errorf("views = %v", views)
|
||||
}
|
||||
// The admin still has to say which.
|
||||
e.do("GET", "/zones/adbraun.lol", e.token, nil, 400)
|
||||
}
|
||||
@@ -57,6 +57,8 @@ func (s *Server) listViews(w http.ResponseWriter, r *http.Request, p *auth.Princ
|
||||
for _, v := range st.OrderedViews() {
|
||||
if p.Can(auth.Action{Resource: auth.ResViews, View: v.Name}) {
|
||||
out = append(out, presentView(st, v, colors))
|
||||
} else if lv := limitedView(st, v, colors, p); lv != nil {
|
||||
out = append(out, *lv)
|
||||
}
|
||||
}
|
||||
writeJSON(w, http.StatusOK, out)
|
||||
|
||||
@@ -34,7 +34,7 @@ func (s *Server) zoneByPath(r *http.Request, st *model.State, p *auth.Principal)
|
||||
name = "."
|
||||
}
|
||||
var err error
|
||||
z, err = engine.FindZoneRef(st, name, r.URL.Query().Get("view"))
|
||||
z, err = engine.FindZoneRef(st, name, r.URL.Query().Get("view"), func(z *model.Zone) bool { return p.CanSeeZone(z.Name, st.ViewName(z)) })
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -82,6 +82,22 @@ func presentView(st *model.State, v *model.View, colors map[int64]string) api.Vi
|
||||
return api.View{View: *v, Zones: len(st.ZonesInView(v.ID)), Color: colors[v.ID]}
|
||||
}
|
||||
|
||||
// limitedView presents a view to a principal without views:read who can
|
||||
// see zones in it (the zone list names the view anyway): its name and
|
||||
// colour, and how many of its zones they can see. nil when they see none.
|
||||
func limitedView(st *model.State, v *model.View, colors map[int64]string, p *auth.Principal) *api.View {
|
||||
n := 0
|
||||
for _, z := range st.ZonesInView(v.ID) {
|
||||
if p.CanSeeZone(z.Name, v.Name) {
|
||||
n++
|
||||
}
|
||||
}
|
||||
if n == 0 {
|
||||
return nil
|
||||
}
|
||||
return &api.View{View: model.View{ID: v.ID, Name: v.Name, Position: v.Position}, Zones: n, Color: colors[v.ID], Limited: true}
|
||||
}
|
||||
|
||||
func presentACL(st *model.State, a *model.ACL) api.ACL {
|
||||
used := engine.FindACLRefs(st, a.Name)
|
||||
if used == nil {
|
||||
|
||||
Reference in new issue
Block a user