store: host_credentials becomes kind-aware (repo + admin slots)

This commit is contained in:
2026-05-03 22:06:05 +01:00
parent 9f2cb18e42
commit f801fdf65b
7 changed files with 151 additions and 23 deletions
+1 -1
View File
@@ -167,7 +167,7 @@ func (s *Server) handleAgentEnroll(w stdhttp.ResponseWriter, r *stdhttp.Request)
// /api/hosts/{id}/repo-credentials. Failing the whole enrolment
// here would leave a half-burned token + an orphan host.
if encForHost != "" {
if err := s.deps.Store.SetHostCredentials(r.Context(), hostID, encForHost); err != nil {
if err := s.deps.Store.SetHostCredentials(r.Context(), hostID, store.CredKindRepo, encForHost); err != nil {
slog.Error("enrollment: set host credentials failed",
"host_id", hostID, "err", err)
}
+5 -5
View File
@@ -39,7 +39,7 @@ func (s *Server) handleGetHostCredentials(w stdhttp.ResponseWriter, r *stdhttp.R
writeJSONError(w, stdhttp.StatusBadRequest, "missing_id", "")
return
}
enc, err := s.deps.Store.GetHostCredentials(r.Context(), hostID)
enc, err := s.deps.Store.GetHostCredentials(r.Context(), hostID, store.CredKindRepo)
if err != nil {
if errors.Is(err, store.ErrNotFound) {
writeJSONError(w, stdhttp.StatusNotFound, "not_set", "")
@@ -107,7 +107,7 @@ func (s *Server) handleSetHostCredentials(w stdhttp.ResponseWriter, r *stdhttp.R
// Merge with the existing row, if any.
existing := repoCredsBlob{}
if cur, err := s.deps.Store.GetHostCredentials(r.Context(), hostID); err == nil {
if cur, err := s.deps.Store.GetHostCredentials(r.Context(), hostID, store.CredKindRepo); err == nil {
plain, err := s.deps.AEAD.Decrypt(cur, []byte("host:"+hostID))
if err != nil {
writeJSONError(w, stdhttp.StatusInternalServerError, "decrypt_failed", "")
@@ -139,7 +139,7 @@ func (s *Server) handleSetHostCredentials(w stdhttp.ResponseWriter, r *stdhttp.R
writeJSONError(w, stdhttp.StatusInternalServerError, "internal", "")
return
}
if err := s.deps.Store.SetHostCredentials(r.Context(), hostID, enc); err != nil {
if err := s.deps.Store.SetHostCredentials(r.Context(), hostID, store.CredKindRepo, enc); err != nil {
writeJSONError(w, stdhttp.StatusInternalServerError, "internal", "")
return
}
@@ -212,7 +212,7 @@ func (s *Server) onAgentHello(ctx context.Context, hostID string, conn *ws.Conn)
// them the runner can't talk to the repo). We rely on Restic's
// idempotent init for re-runs.
func (s *Server) maybeAutoInit(ctx context.Context, hostID string, conn *ws.Conn) {
if _, err := s.deps.Store.GetHostCredentials(ctx, hostID); err != nil {
if _, err := s.deps.Store.GetHostCredentials(ctx, hostID, store.CredKindRepo); err != nil {
// No creds bound yet — operator hasn't supplied them. The next
// hello after creds land will pick this up.
return
@@ -266,7 +266,7 @@ func (s *Server) maybeAutoInit(ctx context.Context, hostID string, conn *ws.Conn
// credentials. Silent no-op when the host has nothing on file
// (the operator hasn't bound creds to it yet).
func (s *Server) pushRepoCredsOnHello(ctx context.Context, hostID string, conn *ws.Conn) {
enc, err := s.deps.Store.GetHostCredentials(ctx, hostID)
enc, err := s.deps.Store.GetHostCredentials(ctx, hostID, store.CredKindRepo)
if err != nil {
if !errors.Is(err, store.ErrNotFound) {
slog.Warn("on-hello: load host creds", "host_id", hostID, "err", err)
@@ -5,6 +5,8 @@ import (
"encoding/json"
"testing"
"time"
"gitea.dcglab.co.uk/steve/restic-manager/internal/store"
)
// TestEnrollmentTransfersRepoCreds verifies the round-trip:
@@ -57,12 +59,12 @@ func TestEnrollmentTransfersRepoCreds(t *testing.T) {
hostID, "host42", "linux", "amd64", "2026-01-01T00:00:00Z"); err != nil {
t.Fatalf("insert host: %v", err)
}
if err := st.SetHostCredentials(ctx, hostID, encForHost); err != nil {
if err := st.SetHostCredentials(ctx, hostID, store.CredKindRepo, encForHost); err != nil {
t.Fatalf("set host credentials: %v", err)
}
// host_credentials row should now hold the host-bound ciphertext.
got, err := st.GetHostCredentials(ctx, hostID)
got, err := st.GetHostCredentials(ctx, hostID, store.CredKindRepo)
if err != nil {
t.Fatalf("get host creds: %v", err)
}
+1 -1
View File
@@ -99,7 +99,7 @@ func enrolHostForWS(t *testing.T, srv *Server, st *store.Store, name string) (ho
if err != nil {
t.Fatalf("encrypt: %v", err)
}
if err := st.SetHostCredentials(context.Background(), hostID, enc); err != nil {
if err := st.SetHostCredentials(context.Background(), hostID, store.CredKindRepo, enc); err != nil {
t.Fatalf("set creds: %v", err)
}
return hostID, token
+3 -3
View File
@@ -61,7 +61,7 @@ func (s *Server) loadHostRepoPage(r *stdhttp.Request, host store.Host) (*hostRep
}
// Credentials (redacted).
enc, err := s.deps.Store.GetHostCredentials(r.Context(), host.ID)
enc, err := s.deps.Store.GetHostCredentials(r.Context(), host.ID, store.CredKindRepo)
switch {
case err == nil:
plain, derr := s.deps.AEAD.Decrypt(enc, []byte("host:"+host.ID))
@@ -204,7 +204,7 @@ func (s *Server) handleUIRepoCredentialsSave(w stdhttp.ResponseWriter, r *stdhtt
// Merge with existing blob — same semantics as the JSON PUT.
existing := repoCredsBlob{}
if cur, err := s.deps.Store.GetHostCredentials(r.Context(), host.ID); err == nil {
if cur, err := s.deps.Store.GetHostCredentials(r.Context(), host.ID, store.CredKindRepo); err == nil {
if plain, derr := s.deps.AEAD.Decrypt(cur, []byte("host:"+host.ID)); derr == nil {
_ = json.Unmarshal(plain, &existing)
}
@@ -227,7 +227,7 @@ func (s *Server) handleUIRepoCredentialsSave(w stdhttp.ResponseWriter, r *stdhtt
stdhttp.Error(w, "internal", stdhttp.StatusInternalServerError)
return
}
if err := s.deps.Store.SetHostCredentials(r.Context(), host.ID, enc); err != nil {
if err := s.deps.Store.SetHostCredentials(r.Context(), host.ID, store.CredKindRepo, enc); err != nil {
slog.Error("ui repo creds: persist", "err", err)
stdhttp.Error(w, "internal", stdhttp.StatusInternalServerError)
return