package server import ( "encoding/json" "errors" "net/http" "strconv" "time" "jarl/internal/api" "jarl/internal/auth" "jarl/internal/engine" "jarl/internal/model" "jarl/internal/store" ) func toRequests(in []api.ChangeRequest) []engine.Request { out := make([]engine.Request, len(in)) for i, c := range in { out[i] = engine.Request{Op: c.Op, ID: c.ID, BaseVersion: c.BaseVersion, Data: c.Data} } return out } func presentChangeset(cs *model.Changeset) api.Changeset { c := *cs c.Ops = engine.RedactOps(cs.Ops) undoable := false if cs.Status == model.ChangesetApplied && len(cs.Result) > 0 { var r struct { Undo []json.RawMessage `json:"undo"` } if json.Unmarshal(cs.Result, &r) == nil { undoable = len(r.Undo) > 0 } // The undo operations hold old TSIG secrets; don't send them. var m map[string]json.RawMessage if json.Unmarshal(cs.Result, &m) == nil { delete(m, "undo") c.Result, _ = json.Marshal(m) } } return api.Changeset{Changeset: c, Undoable: undoable} } // applyBatch: POST /apply applies several requests atomically. func (s *Server) applyBatch(w http.ResponseWriter, r *http.Request, p *auth.Principal) error { var in api.BatchRequest if err := decodeJSON(r, &in); err != nil { return err } if len(in.Ops) == 0 { return badRequest("no operations given") } q := r.URL.Query() if in.DryRun { q.Set("dry_run", "1") } if in.Force { q.Set("force", "1") } r.URL.RawQuery = q.Encode() return s.mutate(w, r, p, in.Description, toRequests(in.Ops), nil) } func (s *Server) listChangesets(w http.ResponseWriter, r *http.Request, p *auth.Principal) error { f := store.ChangesetFilter{Status: r.URL.Query().Get("status"), Limit: queryInt(r, "limit", 100), Before: int64(queryInt(r, "before", 0))} all := p.IsAdmin() || p.Can(auth.Action{Resource: auth.ResAudit}) if !all || queryBool(r, "mine") { f.UserID = p.User.ID } list, err := s.store.ListChangesets(r.Context(), f) if err != nil { return err } out := []api.Changeset{} for _, cs := range list { out = append(out, presentChangeset(cs)) } writeJSON(w, http.StatusOK, out) return nil } func (s *Server) createChangeset(w http.ResponseWriter, r *http.Request, p *auth.Principal) error { var in api.BatchRequest body, err := readBody(r) if err != nil { return err } if len(body) > 0 { if err := json.Unmarshal(body, &in); err != nil { return badRequest("invalid JSON: %v", err) } } cs, err := s.eng.CreateChangeset(r.Context(), p, via(r, p), in.Description) if err != nil { return err } if len(in.Ops) > 0 { res, err := s.eng.Stage(r.Context(), cs.ID, toRequests(in.Ops), p) if err != nil { s.eng.Discard(r.Context(), cs.ID, p) return err } cs = res.Changeset if in.Apply { out, err := s.eng.Apply(r.Context(), cs.ID, p, engine.ApplyOptions{DryRun: in.DryRun, Force: in.Force}) if err != nil { s.writeErr(w, r, err, out) return nil } writeJSON(w, http.StatusCreated, api.MutationResponse{Applied: out.Applied, DryRun: out.DryRun, ChangesetID: cs.ID, Result: raw(out), Warnings: out.Warnings}) return nil } writeJSON(w, http.StatusCreated, api.MutationResponse{Staged: true, ChangesetID: cs.ID, Changeset: raw(presentChangeset(cs)), Added: engine.RedactOps(res.Added), Warnings: res.Warnings, Problems: raw(res.Problems)}) return nil } writeJSON(w, http.StatusCreated, presentChangeset(cs)) return nil } func (s *Server) changesetByPath(r *http.Request, p *auth.Principal) (*model.Changeset, error) { id, err := pathID(r, "id") if err != nil { return nil, err } cs, err := s.store.GetChangeset(r.Context(), id) if errors.Is(err, store.ErrNotFound) || err == nil && !engine.CanAccessChangeset(p, cs, false) { return nil, notFound("changeset %d not found", id) } return cs, err } func (s *Server) getChangeset(w http.ResponseWriter, r *http.Request, p *auth.Principal) error { cs, err := s.changesetByPath(r, p) if err != nil { return err } writeJSON(w, http.StatusOK, presentChangeset(cs)) return nil } func (s *Server) updateChangeset(w http.ResponseWriter, r *http.Request, p *auth.Principal) error { id, err := pathID(r, "id") if err != nil { return err } var in struct { Description string `json:"description"` } if err := decodeJSON(r, &in); err != nil { return err } cs, err := s.eng.UpdateDescription(r.Context(), id, in.Description, p) if err != nil { return err } writeJSON(w, http.StatusOK, presentChangeset(cs)) return nil } func (s *Server) stageOps(w http.ResponseWriter, r *http.Request, p *auth.Principal) error { id, err := pathID(r, "id") if err != nil { return err } var in api.BatchRequest if err := decodeJSON(r, &in); err != nil { return err } res, err := s.eng.Stage(r.Context(), id, toRequests(in.Ops), p) if err != nil { return err } writeJSON(w, http.StatusAccepted, api.MutationResponse{Staged: true, ChangesetID: id, Changeset: raw(presentChangeset(res.Changeset)), Added: engine.RedactOps(res.Added), Warnings: res.Warnings, Problems: raw(res.Problems)}) return nil } func (s *Server) unstageOp(w http.ResponseWriter, r *http.Request, p *auth.Principal) error { id, err := pathID(r, "id") if err != nil { return err } seq, err := strconv.Atoi(r.PathValue("seq")) if err != nil { return badRequest("invalid operation number") } cs, err := s.eng.RemoveOps(r.Context(), id, []int{seq}, p) if err != nil { return err } writeJSON(w, http.StatusOK, presentChangeset(cs)) return nil } func (s *Server) previewChangeset(w http.ResponseWriter, r *http.Request, p *auth.Principal) error { id, err := pathID(r, "id") if err != nil { return err } res, err := s.eng.Preview(r.Context(), id, p) if err != nil { return err } writeJSON(w, http.StatusOK, res) return nil } func (s *Server) applyChangeset(w http.ResponseWriter, r *http.Request, p *auth.Principal) error { id, err := pathID(r, "id") if err != nil { return err } var in struct { DryRun bool `json:"dry_run"` Force bool `json:"force"` } if body, err := readBody(r); err != nil { return err } else if len(body) > 0 { if err := json.Unmarshal(body, &in); err != nil { return badRequest("invalid JSON: %v", err) } } res, err := s.eng.Apply(r.Context(), id, p, engine.ApplyOptions{DryRun: in.DryRun || queryBool(r, "dry_run"), Force: in.Force || queryBool(r, "force")}) if err != nil { s.writeErr(w, r, err, res) return nil } writeJSON(w, http.StatusOK, api.MutationResponse{Applied: res.Applied, DryRun: res.DryRun, ChangesetID: res.ChangesetID, Result: raw(res), Warnings: res.Warnings}) return nil } func (s *Server) discardChangeset(w http.ResponseWriter, r *http.Request, p *auth.Principal) error { id, err := pathID(r, "id") if err != nil { return err } if err := s.eng.Discard(r.Context(), id, p); err != nil { return err } w.WriteHeader(http.StatusNoContent) return nil } func (s *Server) revertChangeset(w http.ResponseWriter, r *http.Request, p *auth.Principal) error { id, err := pathID(r, "id") if err != nil { return err } cs, err := s.eng.Revert(r.Context(), id, p) if err != nil { return err } writeJSON(w, http.StatusCreated, presentChangeset(cs)) return nil } // ---- status, system, audit func (s *Server) status(w http.ResponseWriter, r *http.Request, p *auth.Principal) error { st, err := s.eng.Status(r.Context()) if err != nil { return err } if !p.IsAdmin() && !p.Can(auth.Action{Resource: auth.ResOptions}) { // Keep it to the essentials for restricted principals. st.Problems = nil st.Drift = nil if st.Named != nil { st.Named.Raw = "" } } writeJSON(w, http.StatusOK, api.StatusResponse{Version: s.cfg.Version, Engine: raw(st), Time: time.Now()}) return nil } func (s *Server) systemCheck(w http.ResponseWriter, r *http.Request, p *auth.Principal) error { if err := require(p, auth.Action{Resource: auth.ResOptions}); err != nil { return err } res, err := s.eng.CheckCurrent(r.Context()) if err != nil { return err } writeJSON(w, http.StatusOK, res) return nil } func (s *Server) systemResync(w http.ResponseWriter, r *http.Request, p *auth.Principal) error { if err := requireAdmin(p); err != nil { return err } var in struct { DryRun bool `json:"dry_run"` Force bool `json:"force"` } if body, err := readBody(r); err != nil { return err } else if len(body) > 0 { if err := json.Unmarshal(body, &in); err != nil { return badRequest("invalid JSON: %v", err) } } res, err := s.eng.Resync(r.Context(), p, engine.ApplyOptions{DryRun: in.DryRun, Force: in.Force}) if err != nil { s.writeErr(w, r, err, res) return nil } writeJSON(w, http.StatusOK, res) return nil } func (s *Server) listAudit(w http.ResponseWriter, r *http.Request, p *auth.Principal) error { q := r.URL.Query() f := store.AuditFilter{Username: q.Get("user"), Action: q.Get("action"), Outcome: q.Get("outcome"), ObjectType: q.Get("object_type"), ChangesetID: int64(queryInt(r, "changeset", 0)), Search: q.Get("q"), Before: int64(queryInt(r, "before", 0)), Limit: queryInt(r, "limit", 200)} for name, dst := range map[string]*time.Time{"since": &f.Since, "until": &f.Until} { if v := q.Get(name); v != "" { t, err := time.Parse(time.RFC3339, v) if err != nil { if d, derr := time.Parse("2006-01-02", v); derr == nil { t = d } else { return badRequest("invalid %s %q (use RFC 3339 or YYYY-MM-DD)", name, v) } } *dst = t } } if !p.IsAdmin() && !p.Can(auth.Action{Resource: auth.ResAudit}) { // Everyone may see their own activity. f.Username = p.User.Username } evs, err := s.store.ListAudit(r.Context(), f) if err != nil { return err } if evs == nil { evs = []*model.AuditEvent{} } writeJSON(w, http.StatusOK, evs) return nil }