55f7621c90
gates / gates (push) Successful in 30s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
296 lines
10 KiB
Go
296 lines
10 KiB
Go
package offsitekeys
|
||
|
||
import (
|
||
"context"
|
||
"strings"
|
||
"log"
|
||
"os"
|
||
"path/filepath"
|
||
"testing"
|
||
|
||
"gitea.dooplex.hu/admin/felhom-hub/internal/store"
|
||
)
|
||
|
||
func svcFixture(t *testing.T) (*Service, *fakeFS, *[]string) {
|
||
t.Helper()
|
||
st, err := store.New(filepath.Join(t.TempDir(), "hub.db"), log.New(os.Stderr, "", 0))
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
t.Cleanup(func() { st.Close() })
|
||
cfg := `{"offsite":{"enabled":true,"type":"shared","host":"u1-sub4.example","user":"u1-sub4","port":23,"repo_path":"/home/felhom-repo","host_fingerprint":"SHA256:host"}}`
|
||
if err := st.SaveCustomerConfig(&store.CustomerConfig{CustomerID: "c1", APIKey: "k", RetrievalPassword: "p", ConfigJSON: cfg}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := st.SaveOneTimeSecret("c1", "SubPw1%"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
fs := newFS()
|
||
var events []string
|
||
s := &Service{Store: st, Reg: &Registrar{Dialer: fakeDialer{fs}},
|
||
Emit: func(_, typ, _, _, _, _ string) { events = append(events, typ) }}
|
||
return s, fs, &events
|
||
}
|
||
|
||
// Register → confirm → the window is refused while weekly windows are off; an operator one-shot opens
|
||
// it (deleting line FIRST); the box's close removes it; a drop beyond the bound alarms.
|
||
func TestWindow_GrantOpenCloseAndDropAlarm(t *testing.T) {
|
||
s, fs, events := svcFixture(t)
|
||
ctx := context.Background()
|
||
pub, fp := newKey(t)
|
||
if _, err := s.RegisterKey(ctx, "c1", pub); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if _, err := s.ConfirmKey(ctx, "c1", fp); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if g, err := s.OpenWindowFor(ctx, "c1", 20); err != nil || g.Granted {
|
||
t.Fatalf("windows off: granted=%v err=%v — must refuse", g.Granted, err)
|
||
}
|
||
if err := s.Store.GrantOffsiteWindowOnce("c1"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
g, err := s.OpenWindowFor(ctx, "c1", 20)
|
||
if err != nil || !g.Granted || g.MaxRemove != 10 {
|
||
t.Fatalf("one-shot: %+v %v", g, err)
|
||
}
|
||
if lines := ParseLines(fs.files[".ssh/authorized_keys"], "/home/felhom-repo"); !lines[0].Window || lines[0].Fingerprint != fp {
|
||
t.Fatalf("window line not first: %+v", lines)
|
||
}
|
||
if !s.Store.OffsiteWindowOpen("c1") {
|
||
t.Fatal("the ledger does not show the window open — the daily check would alarm on it")
|
||
}
|
||
if g2, _ := s.OpenWindowFor(ctx, "c1", 20); g2.Granted {
|
||
t.Fatal("the one-shot grant was not consumed")
|
||
}
|
||
// The box reports a fall of 12 (allowed 10) → offsite_window_drop.
|
||
if err := s.CloseWindowFor(ctx, "c1", WindowResult{WindowID: g.WindowID, CountAfter: 8, Outcome: "pruned"}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if a := audit(fs.files[".ssh/authorized_keys"], "/home/felhom-repo", false); len(a.Findings) != 0 {
|
||
t.Fatalf("window line left behind: %+v", a)
|
||
}
|
||
found := false
|
||
for _, e := range *events {
|
||
if e == EventWindowDrop {
|
||
found = true
|
||
}
|
||
}
|
||
if !found {
|
||
t.Fatalf("no %s event for a fall beyond the bound: %v", EventWindowDrop, *events)
|
||
}
|
||
}
|
||
|
||
// A window for a box that never confirmed its key is refused (nothing to scope the window to).
|
||
func TestWindow_NoConfirmedKeyRefused(t *testing.T) {
|
||
s, _, _ := svcFixture(t)
|
||
_ = s.Store.GrantOffsiteWindowOnce("c1")
|
||
pub, _ := newKey(t)
|
||
if _, err := s.RegisterKey(context.Background(), "c1", pub); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if g, _ := s.OpenWindowFor(context.Background(), "c1", 10); g.Granted {
|
||
t.Fatal("granted without a confirmed key")
|
||
}
|
||
}
|
||
|
||
// A window the box never closes is closed by the hub at its bound: the deleting line goes, the ledger
|
||
// row closes with reason "timeout", and the operator hears offsite_window_failed.
|
||
func TestWindow_LeftOpenIsClosedByTheSweep(t *testing.T) {
|
||
s, fs, events := svcFixture(t)
|
||
ctx := context.Background()
|
||
pub, fp := newKey(t)
|
||
if _, err := s.RegisterKey(ctx, "c1", pub); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if _, err := s.ConfirmKey(ctx, "c1", fp); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
_ = s.Store.GrantOffsiteWindowOnce("c1")
|
||
g, err := s.OpenWindowFor(ctx, "c1", 10)
|
||
if err != nil || !g.Granted {
|
||
t.Fatalf("%+v %v", g, err)
|
||
}
|
||
// Make it overdue: the box crashed and never reported.
|
||
if err := s.Store.ForceOffsiteWindowDueForTest(g.WindowID); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
s.SweepExpiredWindows(ctx)
|
||
if a := audit(fs.files[".ssh/authorized_keys"], "/home/felhom-repo", false); len(a.Findings) != 0 {
|
||
t.Fatalf("the sweep left the deleting line: %+v", a)
|
||
}
|
||
w, _ := s.Store.GetOffsiteWindow(g.WindowID)
|
||
if w == nil || w.ClosedAt.IsZero() || w.CloseReason != "timeout" {
|
||
t.Fatalf("ledger = %+v", w)
|
||
}
|
||
last := (*events)[len(*events)-1]
|
||
if last != EventWindowFailed {
|
||
t.Fatalf("last event = %s", last)
|
||
}
|
||
}
|
||
|
||
// Decision 74 (R-823): a set-aside deletion is NOT acted on before the delay, a cancelled request deletes
|
||
// nothing, and the live repository can never be named.
|
||
func TestAbandon_DelayCancelAndScope(t *testing.T) {
|
||
s, fs, events := svcFixture(t)
|
||
ctx := context.Background()
|
||
aside := "/home/felhom-repo.orphaned-20261004"
|
||
fs.dirs[aside] = true
|
||
fs.dirs["/home/felhom-repo"] = true
|
||
for _, bad := range []string{"/home/felhom-repo", "/home/felhom-repo.orphaned-../x", "/home/other.orphaned-1"} {
|
||
if _, err := s.RequestAbandon(ctx, "c1", bad); err == nil {
|
||
t.Fatalf("accepted a non-set-aside path %q", bad)
|
||
}
|
||
}
|
||
st, err := s.RequestAbandon(ctx, "c1", aside)
|
||
if err != nil || st.State != "pending" {
|
||
t.Fatalf("%+v %v", st, err)
|
||
}
|
||
// Not due yet (7-day default): the sweep deletes nothing.
|
||
s.SweepAbandons(ctx)
|
||
if !fs.dirs[aside] {
|
||
t.Fatal("deleted BEFORE the delay")
|
||
}
|
||
// Cancelled, then made due: still nothing deleted.
|
||
if n, _ := s.CancelAbandon("c1", "operator"); n != 1 {
|
||
t.Fatalf("cancelled %d", n)
|
||
}
|
||
a, _ := s.Store.LatestOffsiteAbandon("c1", aside)
|
||
_ = s.Store.ForceOffsiteAbandonDueForTest(a.ID)
|
||
s.SweepAbandons(ctx)
|
||
if !fs.dirs[aside] {
|
||
t.Fatal("a CANCELLED request deleted the copy")
|
||
}
|
||
// A fresh request, due: deleted, and only that directory.
|
||
if _, err := s.RequestAbandon(ctx, "c1", aside); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
a, _ = s.Store.LatestOffsiteAbandon("c1", aside)
|
||
_ = s.Store.ForceOffsiteAbandonDueForTest(a.ID)
|
||
s.SweepAbandons(ctx)
|
||
if fs.dirs[aside] || !fs.dirs["/home/felhom-repo"] {
|
||
t.Fatalf("after the due sweep: %v", fs.dirs)
|
||
}
|
||
if st, _ := s.AbandonStatusFor("c1"); st.State != "deleted" {
|
||
t.Fatalf("state = %s", st.State)
|
||
}
|
||
last := (*events)[len(*events)-1]
|
||
if last != EventAbandonDeleted {
|
||
t.Fatalf("last event %s", last)
|
||
}
|
||
}
|
||
|
||
// Decision 72 (R-826): the operator clean-up keeps pinned lines and drops the rest; an empty file is fine.
|
||
func TestRemoveUnpinned_KeepsOnlyPinned(t *testing.T) {
|
||
s, fs, _ := svcFixture(t)
|
||
a, _ := newKey(t)
|
||
b, _ := newKey(t)
|
||
fs.files[".ssh/authorized_keys"] = a + "\n" + b + "\n"
|
||
n, err := s.RemoveUnpinnedKeys(context.Background(), "c1")
|
||
if err != nil || n != 2 || fs.files[".ssh/authorized_keys"] != "" {
|
||
t.Fatalf("n=%d err=%v file=%q", n, err, fs.files[".ssh/authorized_keys"])
|
||
}
|
||
if n, _ := s.RemoveUnpinnedKeys(context.Background(), "c1"); n != 0 {
|
||
t.Fatal("second run changed something")
|
||
}
|
||
}
|
||
|
||
// R-827: the daily check is read-only — no .ssh is created on a sub-account that has none.
|
||
func TestAudit_DoesNotCreateSSHDir(t *testing.T) {
|
||
fs := newFS()
|
||
delete(fs.dirs, ".ssh")
|
||
r := &Registrar{Dialer: fakeDialer{fs}}
|
||
if _, err := r.Audit(context.Background(), tgt, "pw", false); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
for _, c := range fs.cmds {
|
||
if strings.HasPrefix(c, "mkdir") {
|
||
t.Fatalf("the audit wrote: %q", c)
|
||
}
|
||
}
|
||
}
|
||
|
||
// The honest weekly removal (7 of ~17 per app, ~41 %) must fit under the cap — demo-hp's real shape:
|
||
// 9 apps × 17 = 153 snapshots, 63 removed in a week.
|
||
func TestMaxRemove_HonestWeekFits(t *testing.T) {
|
||
if MaxRemove(153) < 63 {
|
||
t.Fatalf("MaxRemove(153) = %d < 63 — every honest window would be refused", MaxRemove(153))
|
||
}
|
||
if MaxRemove(4) != 5 {
|
||
t.Fatal("floor of 5 lost")
|
||
}
|
||
}
|
||
|
||
// R-833: after a long gap the honest backlog exceeds half the snapshots, and the default cap makes the
|
||
// box's guard refuse every window. The operator's raised-cap grant opens ONE window with a larger cap;
|
||
// it is consumed, the next window has the default cap again, the close check uses the raised cap (no
|
||
// false drop alarm), and the grant is an operator event. Red-proof: drop the `if raised` assignment in
|
||
// OpenWindowFor and the first assertion fails.
|
||
func TestWindow_RaisedCapIsOneWindowOnly(t *testing.T) {
|
||
s, _, events := svcFixture(t)
|
||
ctx := context.Background()
|
||
pub, fp := newKey(t)
|
||
if _, err := s.RegisterKey(ctx, "c1", pub); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if _, err := s.ConfirmKey(ctx, "c1", fp); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
// Without the raised grant: a plain one-shot gives half of 40 = 20.
|
||
_ = s.Store.GrantOffsiteWindowOnce("c1")
|
||
g0, err := s.OpenWindowFor(ctx, "c1", 40)
|
||
if err != nil || !g0.Granted || g0.MaxRemove != 20 {
|
||
t.Fatalf("plain grant: %+v %v — want the default cap 20", g0, err)
|
||
}
|
||
_ = s.CloseWindowFor(ctx, "c1", WindowResult{WindowID: g0.WindowID, CountAfter: 40, Outcome: "guard-refused"})
|
||
|
||
if err := s.GrantLargeWindow("c1", 30); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
g, err := s.OpenWindowFor(ctx, "c1", 40)
|
||
if err != nil || !g.Granted || g.MaxRemove != 30 {
|
||
t.Fatalf("raised grant: %+v %v — want MaxRemove 30", g, err)
|
||
}
|
||
*events = nil
|
||
if err := s.CloseWindowFor(ctx, "c1", WindowResult{WindowID: g.WindowID, CountAfter: 12, Outcome: "pruned"}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
for _, e := range *events {
|
||
if e == EventWindowDrop {
|
||
t.Fatal("a drop of 28 under a raised cap of 30 alarmed — the close check ignored the window's own cap")
|
||
}
|
||
}
|
||
// The grant was consumed: no window without a new grant (weekly windows are off here) …
|
||
if g2, _ := s.OpenWindowFor(ctx, "c1", 12); g2.Granted {
|
||
t.Fatal("the raised grant was not consumed")
|
||
}
|
||
// … and the next granted window is back on the default cap.
|
||
_ = s.Store.GrantOffsiteWindowOnce("c1")
|
||
g3, _ := s.OpenWindowFor(ctx, "c1", 40)
|
||
if !g3.Granted || g3.MaxRemove != 20 {
|
||
t.Fatalf("next window: %+v — want the default cap 20 again", g3)
|
||
}
|
||
}
|
||
|
||
// The grant is an operator event, bounded, and only for a known customer.
|
||
func TestGrantLargeWindow_EventAndBounds(t *testing.T) {
|
||
s, _, events := svcFixture(t)
|
||
for _, bad := range []int{0, -1, MaxRemoveGrantCeiling + 1} {
|
||
if err := s.GrantLargeWindow("c1", bad); err == nil {
|
||
t.Fatalf("max_remove %d accepted", bad)
|
||
}
|
||
}
|
||
if err := s.GrantLargeWindow("nobody", 10); err == nil {
|
||
t.Fatal("a grant for an unknown customer was accepted")
|
||
}
|
||
if ok, _ := s.Store.TakeOffsiteWindowGrant("c1"); ok {
|
||
t.Fatal("a refused grant left a grant behind")
|
||
}
|
||
if err := s.GrantLargeWindow("c1", 10); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if len(*events) != 1 || (*events)[0] != EventWindowLargeGrant {
|
||
t.Fatalf("events = %v, want one %s", *events, EventWindowLargeGrant)
|
||
}
|
||
}
|