The view shown in the zone list and on the zone page now links to that
view. The zone page and changeset page get breadcrumbs, and the zone list
keeps its filters in the address so going back restores them. Views can
be given a colour (Views > Edit, `jarl view color`, PUT /views/{id}/color)
so zones of the same view are easy to spot; the colour is UI-only, kept
outside changesets, and needs no deploy.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
145 lines
4.0 KiB
Go
145 lines
4.0 KiB
Go
package server
|
|
|
|
import (
|
|
"jarl/internal/api"
|
|
"jarl/internal/auth"
|
|
"jarl/internal/catalog"
|
|
"jarl/internal/dnsname"
|
|
"jarl/internal/engine"
|
|
"jarl/internal/model"
|
|
"jarl/internal/render"
|
|
"jarl/internal/rr"
|
|
)
|
|
|
|
func (s *Server) presentZone(st *model.State, z *model.Zone, counts map[int64]int) api.Zone {
|
|
out := api.Zone{Zone: *z, Display: dnsname.Display(z.Name), View: st.ViewName(z), Records: counts[z.ID]}
|
|
out.ZoneFile = render.ZoneFilePath(st, z, s.eng.Config().Paths)
|
|
if z.Type == model.ZonePrimary {
|
|
out.RName = dnsname.EmailFromMailbox(z.SOA.RName)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func presentRecord(st *model.State, r *model.Record) api.Record {
|
|
z := st.Zones[r.ZoneID]
|
|
out := api.Record{Record: *r}
|
|
if z != nil {
|
|
out.Zone, out.View, out.RelativeName, out.EffectiveTTL = z.Name, st.ViewName(z), dnsname.Relative(r.Name, z.Name), r.EffectiveTTL(z)
|
|
if rr.IsTextType(r.Type) {
|
|
if p, err := rr.Parse(z.Name, r.Name, r.Type, r.Data); err == nil {
|
|
if t, ok := rr.TextValue(p.RR); ok {
|
|
out.Text = &t
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func canReadRecord(p *auth.Principal, st *model.State, r *model.Record) bool {
|
|
z := st.Zones[r.ZoneID]
|
|
return z != nil && p.Can(auth.Action{Resource: auth.ResRecords, Zone: z.Name, View: st.ViewName(z), Name: r.Name, Type: r.Type})
|
|
}
|
|
|
|
func presentView(st *model.State, v *model.View, colors map[int64]string) api.View {
|
|
return api.View{View: *v, Zones: len(st.ZonesInView(v.ID)), Color: colors[v.ID]}
|
|
}
|
|
|
|
func presentACL(st *model.State, a *model.ACL) api.ACL {
|
|
used := engine.FindACLRefs(st, a.Name)
|
|
if used == nil {
|
|
used = []string{}
|
|
}
|
|
return api.ACL{ACL: *a, UsedBy: used}
|
|
}
|
|
|
|
func presentKey(st *model.State, k *model.TSIGKey, withSecret bool) api.TSIGKey {
|
|
out := api.TSIGKey{TSIGKey: *k, UsedBy: engine.FindKeyRefs(st, k.Name)}
|
|
if out.UsedBy == nil {
|
|
out.UsedBy = []string{}
|
|
}
|
|
if !withSecret {
|
|
out.Secret = ""
|
|
}
|
|
return out
|
|
}
|
|
|
|
func optionWhere(st *model.State, o *model.Option) string {
|
|
switch o.Scope {
|
|
case model.ScopeView:
|
|
if v := st.Views[o.ScopeID]; v != nil {
|
|
return "view " + v.Name
|
|
}
|
|
case model.ScopeZone:
|
|
if z := st.Zones[o.ScopeID]; z != nil {
|
|
return "zone " + engine.ZoneLabel(st, z)
|
|
}
|
|
case model.ScopeTopLevel:
|
|
return "top level"
|
|
}
|
|
return "server options"
|
|
}
|
|
|
|
func presentOption(st *model.State, o *model.Option) api.Option {
|
|
out := api.Option{Option: *o, Where: optionWhere(st, o)}
|
|
scope := catalog.ScopeOptions
|
|
switch o.Scope {
|
|
case model.ScopeView:
|
|
scope = catalog.ScopeView
|
|
case model.ScopeTopLevel:
|
|
scope = catalog.ScopeTop
|
|
case model.ScopeZone:
|
|
if z := st.Zones[o.ScopeID]; z != nil {
|
|
scope = catalog.ZoneScope(z.Type)
|
|
}
|
|
}
|
|
if c, ok := catalog.Default().Lookup(scope, o.Name); ok {
|
|
out.Description, out.Status = c.Description, c.Status
|
|
}
|
|
return out
|
|
}
|
|
|
|
// canReadOption reports whether p may see an option.
|
|
func canReadOption(p *auth.Principal, st *model.State, o *model.Option) bool {
|
|
switch o.Scope {
|
|
case model.ScopeView:
|
|
v := st.Views[o.ScopeID]
|
|
return v != nil && p.Can(auth.Action{Resource: auth.ResViews, View: v.Name})
|
|
case model.ScopeZone:
|
|
z := st.Zones[o.ScopeID]
|
|
return z != nil && p.Can(auth.Action{Resource: auth.ResZones, Zone: z.Name, View: st.ViewName(z)})
|
|
}
|
|
return p.Can(auth.Action{Resource: auth.ResOptions})
|
|
}
|
|
|
|
func presentUser(u *model.User) api.User {
|
|
out := api.User{User: *u}
|
|
for _, g := range u.Grants {
|
|
out.GrantStrings = append(out.GrantStrings, auth.FormatGrant(g))
|
|
}
|
|
if out.GrantStrings == nil {
|
|
out.GrantStrings = []string{}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func presentToken(t *model.Token) api.Token {
|
|
out := api.Token{Token: *t}
|
|
for _, g := range t.Grants {
|
|
out.GrantStrings = append(out.GrantStrings, auth.FormatGrant(g))
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (s *Server) whoamiOf(p *auth.Principal, se *model.Session) api.Whoami {
|
|
out := api.Whoami{User: presentUser(p.User), Method: p.Method, Permissions: p.Summary(), Admin: p.IsAdmin(), Version: s.cfg.Version}
|
|
if p.Token != nil {
|
|
t := presentToken(p.Token)
|
|
out.Token = &t
|
|
}
|
|
if se != nil {
|
|
out.CSRFToken = se.CSRFToken
|
|
}
|
|
return out
|
|
}
|