The "linked with" badge links each view's zone (the API's zones carry links: id and view). The Link dialog lists the views one per row, with record count and any link they already have; nothing is ticked, Link waits for a choice and names every zone that will join. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
175 lines
4.8 KiB
Go
175 lines
4.8 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)
|
|
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.FormatValue(o.Name, o.Value); 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
|
|
}
|