cert get: combined file for HAProxy, --local, saved NAME; cert list shows get and last download

- jarl cert get --combined PATH writes the key and full chain in one file
  (mode 600, atomic), kept outside --dir so it can live in HAProxy's crt
  directory; JARL_CERT_COMBINED for the hook. Turning it on, moving it or
  removing it ('') applies from the files on disk and runs the hook.
- jarl cert get --local writes the combined file and runs the hook from
  this host's files, without asking jarld.
- jarl cert saved NAME shows all of one certificate's settings.
- jarl cert list: USES "get" for certificates for other hosts, and a
  PULLED column (last download, "(old)" while a host is behind). Owners
  now see their own downloads (pulled_by), not only administrators.
- jarl cert get without a name refuses per-certificate flags.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adamandClaude Opus 5.5 committed 2026-10-25 00:24:33 +01:00
1 parent 3fb159cf4d
commit 42e79af678
9 files changed
+578 -76

No files matched your search

+26 -12
View File
@@ -33,7 +33,8 @@ type certView struct {
// IssuingSince is set while Jarl is obtaining it.
IssuingSince *time.Time `json:"issuing_since,omitempty"`
// Owner is the user a certificate for another host belongs to; PulledBy
// who downloaded it (list and get, administrators only).
// who downloaded it (list and get; an owner who isn't an administrator
// sees only their own downloads).
Owner string `json:"owner,omitempty"`
PulledBy []model.CertificatePull `json:"pulled_by,omitempty"`
}
@@ -116,12 +117,27 @@ func (s *Server) certFor(r *http.Request, p *auth.Principal, write bool) (*model
return c, nil
}
// withOwners adds owners and downloads to views (for administrators).
func (s *Server) withOwners(ctx context.Context, views []certView) error {
// withOwners adds owners and downloads to views: all of them for an
// administrator, the principal's own downloads for anyone else (who sees
// only their own certificates).
func (s *Server) withOwners(ctx context.Context, p *auth.Principal, views []certView) error {
pulls, err := s.store.CertificatePulls(ctx)
if err != nil {
return err
}
if !p.IsAdmin() {
for i := range views {
for _, pl := range pulls[views[i].ID] {
if p.User != nil && pl.UserID == p.User.ID {
views[i].PulledBy = append(views[i].PulledBy, pl)
}
}
if p.User != nil && views[i].OwnerID != 0 {
views[i].Owner = p.User.Username
}
}
return nil
}
users, err := s.store.ListUsers(ctx)
if err != nil {
return err
@@ -179,13 +195,13 @@ func (s *Server) certList(w http.ResponseWriter, r *http.Request, p *auth.Princi
v.NamedUse = encdns.CertUses(st, s.cfg.Certs.FilesDir(c.Name))
out = append(out, v)
}
if err := s.withOwners(r.Context(), p, out); err != nil {
return err
}
if !p.IsAdmin() {
writeJSON(w, http.StatusOK, map[string]any{"certificates": out})
return nil
}
if err := s.withOwners(r.Context(), out); err != nil {
return err
}
writeJSON(w, http.StatusOK, map[string]any{"certificates": out, "account": s.cfg.Certs.AccountInfo(r.Context())})
return nil
}
@@ -205,13 +221,11 @@ func (s *Server) certGet(w http.ResponseWriter, r *http.Request, p *auth.Princip
}
v := s.presentCert(c)
v.NamedUse = encdns.CertUses(st, s.cfg.Certs.FilesDir(c.Name))
if p.IsAdmin() {
vs := []certView{v}
if err := s.withOwners(r.Context(), vs); err != nil {
return err
}
v = vs[0]
vs := []certView{v}
if err := s.withOwners(r.Context(), p, vs); err != nil {
return err
}
v = vs[0]
writeJSON(w, http.StatusOK, v)
return nil
}