Files
jarl/internal/server/present.go
T
adamandClaude Opus 5.5 c27a6c10bd Mail reverse DNS check, what Jarl adds to named.conf, lists one per line
jarld checks the reverse DNS of the public zones' MX and SPF addresses as the internet sees it, every 6 hours (Exposure page, jarl exposure mail, jarl_mail_reverse_dns_ok). The Server options page, jarl option generated and GET /options/generated list what Jarl writes into named.conf by itself; stats.rate_limit_log turns off the query-errors logging. Option lists of three or more elements are shown one per line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 21:02:11 +01:00

182 lines
5.0 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))
}
}
}
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
}