A token for one view of a linked zone (an ACME token for external) couldn't add records: a new record was shared and reached the other view. A new name and type is now local when the user may not change it in the other views. A zone name in several views resolves to the only one the user can see, and the error no longer names views they can't. GET /views lists the views a user sees zones in (name, colour, count; limited) without views:read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
683 lines
19 KiB
Go
683 lines
19 KiB
Go
package server
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"slices"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"jarl/internal/api"
|
|
"jarl/internal/auth"
|
|
"jarl/internal/catalog"
|
|
"jarl/internal/dnsname"
|
|
"jarl/internal/engine"
|
|
"jarl/internal/iprange"
|
|
"jarl/internal/model"
|
|
"jarl/internal/render"
|
|
"jarl/internal/rr"
|
|
)
|
|
|
|
// state returns the configuration a request works on: the current one,
|
|
// or, when the request names an open changeset (?changeset= or the
|
|
// X-Jarl-Changeset header), the configuration as it will be after that
|
|
// changeset, so staged objects can be referred to.
|
|
func (s *Server) state(r *http.Request) (*model.State, error) {
|
|
st, _, err := s.states(r)
|
|
return st, err
|
|
}
|
|
|
|
// states is like state but also returns the current configuration when a
|
|
// changeset is involved (base is nil otherwise).
|
|
func (s *Server) states(r *http.Request) (work, base *model.State, err error) {
|
|
if csID, _ := changesetTarget(r); csID > 0 {
|
|
if p, ok := r.Context().Value(keyPrincipal).(*auth.Principal); ok {
|
|
if w, b, err := s.eng.WorkingState(r.Context(), csID, p); err == nil {
|
|
return w, b, nil
|
|
}
|
|
}
|
|
}
|
|
st, err := s.store.LoadState(r.Context())
|
|
return st, nil, err
|
|
}
|
|
|
|
// ---- views
|
|
|
|
func (s *Server) listViews(w http.ResponseWriter, r *http.Request, p *auth.Principal) error {
|
|
st, err := s.state(r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
colors, err := s.store.ViewColors(r.Context())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
out := []api.View{}
|
|
for _, v := range st.OrderedViews() {
|
|
if p.Can(auth.Action{Resource: auth.ResViews, View: v.Name}) {
|
|
out = append(out, presentView(st, v, colors))
|
|
} else if lv := limitedView(st, v, colors, p); lv != nil {
|
|
out = append(out, *lv)
|
|
}
|
|
}
|
|
writeJSON(w, http.StatusOK, out)
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) viewByPath(r *http.Request, st *model.State, p *auth.Principal) (*model.View, error) {
|
|
ref := r.PathValue("id")
|
|
var v *model.View
|
|
if id, err := strconv.ParseInt(ref, 10, 64); err == nil {
|
|
v = st.Views[id]
|
|
} else {
|
|
v = st.ViewByName(ref)
|
|
}
|
|
if v == nil || !p.Can(auth.Action{Resource: auth.ResViews, View: v.Name}) {
|
|
return nil, notFound("view %s not found", ref)
|
|
}
|
|
return v, nil
|
|
}
|
|
|
|
func (s *Server) getView(w http.ResponseWriter, r *http.Request, p *auth.Principal) error {
|
|
st, err := s.state(r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
v, err := s.viewByPath(r, st, p)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
colors, err := s.store.ViewColors(r.Context())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
writeJSON(w, http.StatusOK, presentView(st, v, colors))
|
|
return nil
|
|
}
|
|
|
|
// viewKey: GET /views/{id}/key is the TSIG key that selects the view
|
|
// (render/viewkeys.go), with its secret: like revealing a TSIG key's, it
|
|
// needs tsig:write and is audited.
|
|
func (s *Server) viewKey(w http.ResponseWriter, r *http.Request, p *auth.Principal) error {
|
|
st, err := s.state(r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
v, err := s.viewByPath(r, st, p)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := require(p, auth.Action{Resource: auth.ResTSIG, Write: true}); err != nil {
|
|
return forbidden("showing a view's key needs tsig:write")
|
|
}
|
|
seed, err := engine.ViewKeySeed(r.Context(), s.store)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
name := render.ViewKeyName(v.Name)
|
|
s.eng.Audit(r.Context(), p, "tsig.reveal", "view", v.ID, v.Name, "success", "view key "+name+" shown")
|
|
writeJSON(w, http.StatusOK, map[string]string{"name": name, "algorithm": render.ViewKeyAlgorithm, "secret": render.ViewKeySecret(seed, v.ID)})
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) viewObject(r *http.Request) func(st *model.State, id int64) any {
|
|
return func(st *model.State, id int64) any {
|
|
if v := st.Views[id]; v != nil {
|
|
colors, _ := s.store.ViewColors(r.Context())
|
|
return presentView(st, v, colors)
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// setViewColor sets the colour the web UI shows a view in. It is not part
|
|
// of the configuration, so it applies at once, outside changesets.
|
|
func (s *Server) setViewColor(w http.ResponseWriter, r *http.Request, p *auth.Principal) error {
|
|
st, err := s.state(r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
v, err := s.viewByPath(r, st, p)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := require(p, auth.Action{Resource: auth.ResViews, Write: true, View: v.Name}); err != nil {
|
|
return err
|
|
}
|
|
var in api.ViewColor
|
|
if err := decodeJSON(r, &in); err != nil {
|
|
return err
|
|
}
|
|
if in.Color != "" && !slices.Contains(model.ViewColors, in.Color) {
|
|
return badRequest("unknown colour %q: use one of %s, or \"\" for the default", in.Color, strings.Join(model.ViewColors, ", "))
|
|
}
|
|
if err := s.store.SetViewColor(r.Context(), v.ID, in.Color); err != nil {
|
|
return err
|
|
}
|
|
detail := in.Color
|
|
if detail == "" {
|
|
detail = "default"
|
|
}
|
|
s.eng.Audit(r.Context(), p, "view.color", "view", v.ID, v.Name, "success", detail)
|
|
colors, err := s.store.ViewColors(r.Context())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
writeJSON(w, http.StatusOK, presentView(st, v, colors))
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) createView(w http.ResponseWriter, r *http.Request, p *auth.Principal) error {
|
|
body, err := readBody(r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return s.mutate(w, r, p, "", []engine.Request{request(engine.OpViewCreate, 0, body)}, s.viewObject(r))
|
|
}
|
|
|
|
func (s *Server) updateView(w http.ResponseWriter, r *http.Request, p *auth.Principal) error {
|
|
st, err := s.state(r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
v, err := s.viewByPath(r, st, p)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
body, err := readBody(r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
id := v.ID
|
|
return s.mutate(w, r, p, "", []engine.Request{request(engine.OpViewUpdate, id, body)}, func(st *model.State, _ int64) any { return s.viewObject(r)(st, id) })
|
|
}
|
|
|
|
func (s *Server) deleteView(w http.ResponseWriter, r *http.Request, p *auth.Principal) error {
|
|
st, err := s.state(r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
v, err := s.viewByPath(r, st, p)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
data, _ := json.Marshal(engine.DeleteInput{Cascade: queryBool(r, "cascade")})
|
|
return s.mutate(w, r, p, "", []engine.Request{request(engine.OpViewDelete, v.ID, data)}, nil)
|
|
}
|
|
|
|
func (s *Server) reorderViews(w http.ResponseWriter, r *http.Request, p *auth.Principal) error {
|
|
body, err := readBody(r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return s.mutate(w, r, p, "", []engine.Request{request(engine.OpViewReorder, 0, body)}, nil)
|
|
}
|
|
|
|
// ---- ACLs
|
|
|
|
func (s *Server) listACLs(w http.ResponseWriter, r *http.Request, p *auth.Principal) error {
|
|
if err := require(p, auth.Action{Resource: auth.ResACLs}); err != nil {
|
|
return err
|
|
}
|
|
st, err := s.state(r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
out := []api.ACL{}
|
|
for _, a := range st.SortedACLs() {
|
|
out = append(out, presentACL(st, a))
|
|
}
|
|
writeJSON(w, http.StatusOK, out)
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) aclByPath(r *http.Request, st *model.State) (*model.ACL, error) {
|
|
ref := r.PathValue("id")
|
|
var a *model.ACL
|
|
if id, err := strconv.ParseInt(ref, 10, 64); err == nil {
|
|
a = st.ACLs[id]
|
|
} else {
|
|
a = st.ACLByName(ref)
|
|
}
|
|
if a == nil {
|
|
return nil, notFound("ACL %s not found", ref)
|
|
}
|
|
return a, nil
|
|
}
|
|
|
|
func (s *Server) getACL(w http.ResponseWriter, r *http.Request, p *auth.Principal) error {
|
|
if err := require(p, auth.Action{Resource: auth.ResACLs}); err != nil {
|
|
return err
|
|
}
|
|
st, err := s.state(r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
a, err := s.aclByPath(r, st)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
writeJSON(w, http.StatusOK, presentACL(st, a))
|
|
return nil
|
|
}
|
|
|
|
func aclObject(st *model.State, id int64) any {
|
|
if a := st.ACLs[id]; a != nil {
|
|
return presentACL(st, a)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) createACL(w http.ResponseWriter, r *http.Request, p *auth.Principal) error {
|
|
body, err := readBody(r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return s.mutate(w, r, p, "", []engine.Request{request(engine.OpACLCreate, 0, body)}, aclObject)
|
|
}
|
|
|
|
func (s *Server) updateACL(w http.ResponseWriter, r *http.Request, p *auth.Principal) error {
|
|
st, err := s.state(r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
a, err := s.aclByPath(r, st)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
body, err := readBody(r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
id := a.ID
|
|
return s.mutate(w, r, p, "", []engine.Request{request(engine.OpACLUpdate, id, body)}, func(st *model.State, _ int64) any { return aclObject(st, id) })
|
|
}
|
|
|
|
func (s *Server) deleteACL(w http.ResponseWriter, r *http.Request, p *auth.Principal) error {
|
|
st, err := s.state(r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
a, err := s.aclByPath(r, st)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return s.mutate(w, r, p, "", []engine.Request{request(engine.OpACLDelete, a.ID, nil)}, nil)
|
|
}
|
|
|
|
// ---- TSIG keys
|
|
|
|
func (s *Server) listKeys(w http.ResponseWriter, r *http.Request, p *auth.Principal) error {
|
|
if err := require(p, auth.Action{Resource: auth.ResTSIG}); err != nil {
|
|
return err
|
|
}
|
|
st, err := s.state(r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
out := []api.TSIGKey{}
|
|
for _, k := range st.SortedKeys() {
|
|
out = append(out, presentKey(st, k, false))
|
|
}
|
|
writeJSON(w, http.StatusOK, out)
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) keyByPath(r *http.Request, st *model.State) (*model.TSIGKey, error) {
|
|
ref := r.PathValue("id")
|
|
var k *model.TSIGKey
|
|
if id, err := strconv.ParseInt(ref, 10, 64); err == nil {
|
|
k = st.Keys[id]
|
|
} else {
|
|
k = st.KeyByName(ref)
|
|
}
|
|
if k == nil {
|
|
return nil, notFound("TSIG key %s not found", ref)
|
|
}
|
|
return k, nil
|
|
}
|
|
|
|
func (s *Server) getKey(w http.ResponseWriter, r *http.Request, p *auth.Principal) error {
|
|
if err := require(p, auth.Action{Resource: auth.ResTSIG}); err != nil {
|
|
return err
|
|
}
|
|
st, err := s.state(r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
k, err := s.keyByPath(r, st)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
withSecret := queryBool(r, "secret")
|
|
if withSecret {
|
|
if err := require(p, auth.Action{Resource: auth.ResTSIG, Write: true}); err != nil {
|
|
return forbidden("showing a TSIG secret needs tsig:write")
|
|
}
|
|
s.eng.Audit(r.Context(), p, "tsig.reveal", "tsig", k.ID, k.Name, "success", "secret shown")
|
|
}
|
|
writeJSON(w, http.StatusOK, presentKey(st, k, withSecret))
|
|
return nil
|
|
}
|
|
|
|
func keyObject(withSecret bool) func(st *model.State, id int64) any {
|
|
return func(st *model.State, id int64) any {
|
|
if k := st.Keys[id]; k != nil {
|
|
return presentKey(st, k, withSecret)
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func (s *Server) createKey(w http.ResponseWriter, r *http.Request, p *auth.Principal) error {
|
|
body, err := readBody(r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// The creator gets to see a generated secret once.
|
|
return s.mutate(w, r, p, "", []engine.Request{request(engine.OpTSIGCreate, 0, body)}, keyObject(true))
|
|
}
|
|
|
|
func (s *Server) updateKey(w http.ResponseWriter, r *http.Request, p *auth.Principal) error {
|
|
st, err := s.state(r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
k, err := s.keyByPath(r, st)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
body, err := readBody(r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
id := k.ID
|
|
return s.mutate(w, r, p, "", []engine.Request{request(engine.OpTSIGUpdate, id, body)}, func(st *model.State, _ int64) any { return keyObject(false)(st, id) })
|
|
}
|
|
|
|
func (s *Server) deleteKey(w http.ResponseWriter, r *http.Request, p *auth.Principal) error {
|
|
st, err := s.state(r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
k, err := s.keyByPath(r, st)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return s.mutate(w, r, p, "", []engine.Request{request(engine.OpTSIGDelete, k.ID, nil)}, nil)
|
|
}
|
|
|
|
// ---- options
|
|
|
|
func (s *Server) listOptions(w http.ResponseWriter, r *http.Request, p *auth.Principal) error {
|
|
st, err := s.state(r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
scope := r.URL.Query().Get("scope")
|
|
scopeID := int64(queryInt(r, "scope_id", 0))
|
|
out := []api.Option{}
|
|
var list []*model.Option
|
|
if scope != "" {
|
|
list = st.ScopedOptions(scope, scopeID)
|
|
} else {
|
|
for _, sc := range []string{model.ScopeTopLevel, model.ScopeOptions} {
|
|
list = append(list, st.ScopedOptions(sc, 0)...)
|
|
}
|
|
for _, v := range st.OrderedViews() {
|
|
list = append(list, st.ScopedOptions(model.ScopeView, v.ID)...)
|
|
}
|
|
for _, z := range st.SortedZones() {
|
|
list = append(list, st.ScopedOptions(model.ScopeZone, z.ID)...)
|
|
}
|
|
}
|
|
for _, o := range list {
|
|
if canReadOption(p, st, o) {
|
|
out = append(out, presentOption(st, o))
|
|
}
|
|
}
|
|
writeJSON(w, http.StatusOK, out)
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) optionByPath(r *http.Request, st *model.State, p *auth.Principal) (*model.Option, error) {
|
|
id, err := pathID(r, "id")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
o := st.Options[id]
|
|
if o == nil || !canReadOption(p, st, o) {
|
|
return nil, notFound("option %d not found", id)
|
|
}
|
|
return o, nil
|
|
}
|
|
|
|
func (s *Server) getOption(w http.ResponseWriter, r *http.Request, p *auth.Principal) error {
|
|
st, err := s.state(r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
o, err := s.optionByPath(r, st, p)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
writeJSON(w, http.StatusOK, presentOption(st, o))
|
|
return nil
|
|
}
|
|
|
|
func optionObject(st *model.State, id int64) any {
|
|
if o := st.Options[id]; o != nil {
|
|
return presentOption(st, o)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) createOption(w http.ResponseWriter, r *http.Request, p *auth.Principal) error {
|
|
body, err := readBody(r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return s.mutate(w, r, p, "", []engine.Request{request(engine.OpOptionCreate, 0, body)}, optionObject)
|
|
}
|
|
|
|
func (s *Server) updateOption(w http.ResponseWriter, r *http.Request, p *auth.Principal) error {
|
|
st, err := s.state(r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
o, err := s.optionByPath(r, st, p)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
body, err := readBody(r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
id := o.ID
|
|
return s.mutate(w, r, p, "", []engine.Request{request(engine.OpOptionUpdate, id, body)}, func(st *model.State, _ int64) any { return optionObject(st, id) })
|
|
}
|
|
|
|
func (s *Server) deleteOption(w http.ResponseWriter, r *http.Request, p *auth.Principal) error {
|
|
st, err := s.state(r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
o, err := s.optionByPath(r, st, p)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return s.mutate(w, r, p, "", []engine.Request{request(engine.OpOptionDelete, o.ID, nil)}, nil)
|
|
}
|
|
|
|
// ---- settings
|
|
|
|
func (s *Server) getSettings(w http.ResponseWriter, r *http.Request, p *auth.Principal) error {
|
|
st, err := s.state(r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
writeJSON(w, http.StatusOK, st.Settings)
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) updateSettings(w http.ResponseWriter, r *http.Request, p *auth.Principal) error {
|
|
body, err := readBody(r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return s.mutate(w, r, p, "update settings", []engine.Request{request(engine.OpSettingsUpdate, 0, body)}, func(st *model.State, _ int64) any { return st.Settings })
|
|
}
|
|
|
|
// ---- catalogs
|
|
|
|
func (s *Server) catalogOptions(w http.ResponseWriter, r *http.Request, p *auth.Principal) error {
|
|
cat := catalog.Default()
|
|
scope := r.URL.Query().Get("scope")
|
|
if scope == "" {
|
|
writeJSON(w, http.StatusOK, map[string]any{"scopes": cat.Scopes(), "zone_types": cat.ZoneTypes()})
|
|
return nil
|
|
}
|
|
list := cat.List(scope)
|
|
if list == nil {
|
|
return notFound("unknown scope %q (use one of %s)", scope, strings.Join(cat.Scopes(), ", "))
|
|
}
|
|
out := []*catalog.Statement{}
|
|
for _, st := range list {
|
|
if !st.Managed || queryBool(r, "all") {
|
|
out = append(out, st)
|
|
}
|
|
}
|
|
writeJSON(w, http.StatusOK, out)
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) catalogRRTypes(w http.ResponseWriter, r *http.Request, p *auth.Principal) error {
|
|
writeJSON(w, http.StatusOK, rr.Types())
|
|
return nil
|
|
}
|
|
|
|
// catalogRanges: GET /catalog/ranges?r=A-B,C-D shows the prefixes each
|
|
// address range is written to named.conf as (the web UI shows them while
|
|
// you type). Text that isn't a range is left out; a range with a mistake
|
|
// comes back with its error.
|
|
func (s *Server) catalogRanges(w http.ResponseWriter, r *http.Request, p *auth.Principal) error {
|
|
type rangeInfo struct {
|
|
Input string `json:"input"`
|
|
Range string `json:"range,omitempty"`
|
|
Prefixes []string `json:"prefixes,omitempty"`
|
|
Size string `json:"size,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
out := []rangeInfo{}
|
|
for _, in := range strings.Split(r.URL.Query().Get("r"), ",") {
|
|
in = strings.TrimSpace(in)
|
|
if in == "" || len(out) >= 100 {
|
|
continue
|
|
}
|
|
rg, ok, err := iprange.Parse(strings.TrimSpace(strings.TrimPrefix(in, "!")))
|
|
switch {
|
|
case !ok:
|
|
continue
|
|
case err != nil:
|
|
out = append(out, rangeInfo{Input: in, Error: err.Error()})
|
|
default:
|
|
ri := rangeInfo{Input: in, Range: rg.String(), Size: rg.SizeText()}
|
|
for _, pf := range rg.Prefixes() {
|
|
ri.Prefixes = append(ri.Prefixes, pf.String())
|
|
}
|
|
out = append(out, ri)
|
|
}
|
|
}
|
|
writeJSON(w, http.StatusOK, out)
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) catalogGrants(w http.ResponseWriter, r *http.Request, p *auth.Principal) error {
|
|
roles := map[string][]string{}
|
|
for name, gs := range auth.RoleTemplates {
|
|
for _, g := range gs {
|
|
roles[name] = append(roles[name], auth.FormatGrant(g))
|
|
}
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"resources": auth.Resources, "access": []string{auth.Read, auth.Write}, "roles": roles,
|
|
"syntax": "resource:access [zones=a,b] [views=x] [names=pattern] [types=TXT,CNAME]"})
|
|
return nil
|
|
}
|
|
|
|
// ---- search
|
|
|
|
func (s *Server) search(w http.ResponseWriter, r *http.Request, p *auth.Principal) error {
|
|
q := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("q")))
|
|
if len(q) < 2 {
|
|
return badRequest("search for at least 2 characters")
|
|
}
|
|
st, err := s.state(r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
limit := queryInt(r, "limit", 200)
|
|
out := []api.SearchResult{}
|
|
add := func(res api.SearchResult) bool {
|
|
out = append(out, res)
|
|
return len(out) >= limit
|
|
}
|
|
for _, z := range st.SortedZones() {
|
|
if strings.Contains(z.Name, q) && p.CanSeeZone(z.Name, st.ViewName(z)) {
|
|
if add(api.SearchResult{Kind: "zone", ID: z.ID, ZoneID: z.ID, Label: engine.ZoneLabel(st, z), Detail: z.Type}) {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
var recs []*model.Record
|
|
for _, r := range st.Records {
|
|
if strings.Contains(r.Name, q) || strings.Contains(strings.ToLower(r.Data), q) || strings.Contains(strings.ToLower(r.Comment), q) {
|
|
recs = append(recs, r)
|
|
}
|
|
}
|
|
sort.Slice(recs, func(i, j int) bool {
|
|
if recs[i].Name != recs[j].Name {
|
|
return model.CanonicalLess(recs[i].Name, recs[j].Name)
|
|
}
|
|
return recs[i].ID < recs[j].ID
|
|
})
|
|
for _, rec := range recs {
|
|
if len(out) >= limit {
|
|
break
|
|
}
|
|
if !canReadRecord(p, st, rec) {
|
|
continue
|
|
}
|
|
z := st.Zones[rec.ZoneID]
|
|
add(api.SearchResult{Kind: "record", ID: rec.ID, ZoneID: rec.ZoneID, Label: dnsname.Display(rec.Name) + " " + rec.Type + " " + rec.Data, Detail: engine.ZoneLabel(st, z)})
|
|
}
|
|
if p.Can(auth.Action{Resource: auth.ResACLs}) {
|
|
for _, a := range st.SortedACLs() {
|
|
if strings.Contains(strings.ToLower(a.Name), q) || strings.Contains(strings.ToLower(strings.Join(a.Elements, " ")), q) {
|
|
add(api.SearchResult{Kind: "acl", ID: a.ID, Label: "ACL " + a.Name, Detail: strings.Join(a.Elements, "; ")})
|
|
}
|
|
}
|
|
}
|
|
writeJSON(w, http.StatusOK, out)
|
|
return nil
|
|
}
|
|
|
|
// generatedOptions: GET /options/generated lists what Jarl adds to
|
|
// named.conf by itself (the statistics channel, its logging, blocking's
|
|
// response-policy, view keys), with why and what controls it.
|
|
func (s *Server) generatedOptions(w http.ResponseWriter, r *http.Request, p *auth.Principal) error {
|
|
if err := require(p, auth.Action{Resource: auth.ResOptions}); err != nil {
|
|
return err
|
|
}
|
|
st, err := s.store.LoadState(r.Context())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
out := s.eng.GeneratedStatements(r.Context(), st)
|
|
if out == nil {
|
|
out = []render.Generated{}
|
|
}
|
|
writeJSON(w, http.StatusOK, out)
|
|
return nil
|
|
}
|