The zone page and jarl zone show list the addresses only other views' records use (ptr_other_views_list), with the name each would get and the view it comes from, and say plainly that they aren't missing here. The page's button reads 'Add PTR records for them in view X'. zone show lists the missing PTRs too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
227 lines
6.6 KiB
Go
227 lines
6.6 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 {
|
|
fills := engine.PTRFills(st, z.ViewID, z.Name, st.ZoneRecords(z.ID), false)
|
|
out.PTRMissing = len(fills)
|
|
for _, f := range fills[:min(len(fills), 25)] {
|
|
out.PTRMissingList = append(out.PTRMissingList, api.PTRFill(f))
|
|
}
|
|
if z.ViewID != 0 {
|
|
for _, f := range engine.PTRFills(st, z.ViewID, z.Name, st.ZoneRecords(z.ID), true) {
|
|
if f.From == "" {
|
|
continue
|
|
}
|
|
if out.PTROtherViews++; out.PTROtherViews <= 25 {
|
|
out.PTROtherViewsList = append(out.PTROtherViewsList, api.PTRFill(f))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
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]}
|
|
}
|
|
|
|
// 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 {
|
|
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
|
|
}
|