The header says "also in VIEW" for every zone of the same name in another view, linked or not, in the view's colour, keeping the open tab where the other zone has it. The zone API gains in_views. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
197 lines
5.5 KiB
Go
197 lines
5.5 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
|
|
"jarl/internal/api"
|
|
"jarl/internal/auth"
|
|
"jarl/internal/catalog"
|
|
"jarl/internal/dnsname"
|
|
"jarl/internal/engine"
|
|
"jarl/internal/model"
|
|
"jarl/internal/namedconf"
|
|
"jarl/internal/render"
|
|
"jarl/internal/rr"
|
|
"jarl/internal/version"
|
|
)
|
|
|
|
func (s *Server) presentZone(st *model.State, z *model.Zone, counts map[int64]recordCount) api.Zone {
|
|
out := api.Zone{Zone: *z, Display: dnsname.Display(z.Name), View: st.ViewName(z), Records: counts[z.ID].all, DisabledRecords: counts[z.ID].disabled}
|
|
out.ZoneFile = render.ZoneFilePath(st, z, s.eng.Config().Paths)
|
|
if z.Type == model.ZonePrimary {
|
|
out.RName = dnsname.EmailFromMailbox(z.SOA.RName)
|
|
}
|
|
out.Registrar = s.registrarFor(z)
|
|
if z.Type == model.ZonePrimary && z.ViewID != 0 {
|
|
for _, o := range st.ZonesNamed(z.Name) {
|
|
if o.ID != z.ID && o.Type == model.ZonePrimary {
|
|
out.OtherViews = append(out.OtherViews, st.ViewName(o))
|
|
}
|
|
}
|
|
}
|
|
if z.ViewID != 0 {
|
|
others := st.ZonesNamed(z.Name)
|
|
st.SortByView(others)
|
|
for _, o := range others {
|
|
if o.ID != z.ID {
|
|
out.InViews = append(out.InViews, api.ZoneLink{ID: o.ID, View: st.ViewName(o), Type: o.Type, Linked: z.LinkGroup != 0 && o.LinkGroup == z.LinkGroup})
|
|
}
|
|
}
|
|
}
|
|
if pf, ok := dnsname.PrefixFromReverse(z.Name); ok {
|
|
out.Prefix = pf.String()
|
|
if z.Type == model.ZonePrimary {
|
|
out.PTRMissing = len(engine.PTRFills(st, z.ViewID, z.Name, st.ZoneRecords(z.ID)))
|
|
}
|
|
}
|
|
for _, o := range st.LinkedWith(z) {
|
|
out.LinkedWith = append(out.LinkedWith, st.ViewName(o))
|
|
out.Links = append(out.Links, api.ZoneLink{ID: o.ID, View: st.ViewName(o)})
|
|
}
|
|
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 z.LinkGroup != 0 {
|
|
out.Linked = "local"
|
|
if r.SharedID != 0 {
|
|
out.Linked = "shared"
|
|
}
|
|
}
|
|
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
|
|
}
|
|
if f := namedconf.FormatValueList(o.Name, o.Value, 3); f != o.Value {
|
|
out.Formatted = f
|
|
}
|
|
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
|
|
}
|
|
|
|
// userOut is presentUser with what depends on the server's settings.
|
|
func (s *Server) userOut(u *model.User) api.User {
|
|
out := presentUser(u)
|
|
out.TOTPRequired = s.totpRequired(u)
|
|
return out
|
|
}
|
|
|
|
func (s *Server) whoamiOf(ctx context.Context, p *auth.Principal, se *model.Session) api.Whoami {
|
|
out := api.Whoami{User: s.userOut(p.User), Method: p.Method, Permissions: p.Summary(), Admin: p.IsAdmin(), Version: s.cfg.Version, Build: version.Info()}
|
|
if p.Method == "session" {
|
|
st := s.totpStatus(ctx, p.User)
|
|
out.TOTP = &st
|
|
out.TOTPSetupRequired = s.setupOnly(p)
|
|
}
|
|
if p.Token != nil {
|
|
t := presentToken(p.Token)
|
|
out.Token = &t
|
|
}
|
|
if se != nil {
|
|
out.CSRFToken = se.CSRFToken
|
|
}
|
|
return out
|
|
}
|