oficina / docs Public

Prepare maintenance entry point and encrypted restore rehearsal

Paul Campbell pushed by paul@keymail.dev 61de0758b423dfd44e78118cd780e182858f01ec parent 4bd6cca
2 files changed, +148 −0
  • internal/docs/content_maintenance.go +52 −0
  • internal/docs/content_maintenance_test.go +96 −0
diff --git a/internal/docs/content_maintenance.go b/internal/docs/content_maintenance.go
new file mode 100644
index 0000000..af3f84d
--- /dev/null
+++ b/internal/docs/content_maintenance.go
@@ -0,0 +1,52 @@
+package docs
+
+import (
+ "context"
+ "errors"
+ "log/slog"
+ "os"
+
+ "github.com/carlosframework/rastrillo/db"
+ "github.com/carlosframework/rastrillo/migrate"
+)
+
+type ContentMaintenanceConfig struct {
+ DBPath string
+ ContentKey string
+ ExpectedFingerprint string
+ Scrub bool
+ Logger *slog.Logger
+}
+
+func MaintainContent(ctx context.Context, cfg ContentMaintenanceConfig) error {
+ c, err := newContentCipher(cfg.ContentKey, "production")
+ if err != nil {
+ return err
+ }
+ if cfg.ExpectedFingerprint == "" || cfg.ExpectedFingerprint != c.fingerprint {
+ return errContentKey
+ }
+ info, err := os.Stat(cfg.DBPath)
+ if err != nil {
+ return err
+ }
+ if !info.Mode().IsRegular() {
+ return errContentState
+ }
+ d, err := db.Open(cfg.DBPath, cfg.Logger)
+ if err != nil {
+ return err
+ }
+ if _, err := migrate.Apply(ctx, d, BootSchema); err != nil {
+ return errors.Join(err, d.Close())
+ }
+ if err := convertContent(ctx, d.G, c); err != nil {
+ return errors.Join(err, d.Close())
+ }
+ if cfg.Scrub {
+ if err := scrubContent(ctx, d, c); err != nil {
+ return errors.Join(err, d.Close())
+ }
+ }
+ return d.Close()
+}
diff --git a/internal/docs/content_maintenance_test.go b/internal/docs/content_maintenance_test.go
new file mode 100644
index 0000000..bf52cac
--- /dev/null
+++ b/internal/docs/content_maintenance_test.go
@@ -0,0 +1,96 @@
+package docs
+
+import (
+ "bytes"
+ "context"
+ "encoding/base64"
+ "errors"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/carlosframework/rastrillo/db"
+)
+
+func TestContentMaintenanceRequiresExpectedKeyBeforeOpeningDatabase(t *testing.T) {
+ path := filepath.Join(t.TempDir(), "missing.db")
+ encoded := base64.StdEncoding.EncodeToString(bytes.Repeat([]byte{62}, 32))
+ c, err := newContentCipher(encoded, "production")
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, fingerprint := range []string{"", "incorrect"} {
+ err := MaintainContent(context.Background(), ContentMaintenanceConfig{DBPath: path, ContentKey: encoded, ExpectedFingerprint: fingerprint})
+ if !errors.Is(err, errContentKey) {
+ t.Fatalf("fingerprint %q: %v", fingerprint, err)
+ }
+ }
+ err = MaintainContent(context.Background(), ContentMaintenanceConfig{DBPath: path, ContentKey: encoded, ExpectedFingerprint: c.fingerprint})
+ if !errors.Is(err, os.ErrNotExist) {
+ t.Fatalf("missing database: %v", err)
+ }
+ if _, err := os.Stat(path); !errors.Is(err, os.ErrNotExist) {
+ t.Fatalf("maintenance created missing database: %v", err)
+ }
+}
+
+func TestContentMaintenancePreservesContentInRestoredCopy(t *testing.T) {
+ ctx := context.Background()
+ path := filepath.Join(t.TempDir(), "source.db")
+ app, err := New(Config{Env: "development", DBPath: path})
+ if err != nil {
+ t.Fatal(err)
+ }
+ doc, err := app.Store().Create(ctx, app.TestPerson(t, "owner@example.com"), "Restore")
+ if err != nil {
+ app.Close()
+ t.Fatal(err)
+ }
+ if _, err := app.Store().ApplyUpdate(ctx, doc, app.TestActorFor(t, doc), validUpdate(t)); err != nil {
+ app.Close()
+ t.Fatal(err)
+ }
+ if err := app.Close(); err != nil {
+ t.Fatal(err)
+ }
+ encoded := base64.StdEncoding.EncodeToString(bytes.Repeat([]byte{63}, 32))
+ c, err := newContentCipher(encoded, "production")
+ if err != nil {
+ t.Fatal(err)
+ }
+ cfg := ContentMaintenanceConfig{DBPath: path, ContentKey: encoded, ExpectedFingerprint: c.fingerprint, Scrub: true}
+ if err := MaintainContent(ctx, cfg); err != nil {
+ t.Fatal(err)
+ }
+ snapshot, err := os.ReadFile(path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ restoredPath := filepath.Join(t.TempDir(), "restored.db")
+ if err := os.WriteFile(restoredPath, snapshot, 0600); err != nil {
+ t.Fatal(err)
+ }
+ cfg.DBPath = restoredPath
+ if err := MaintainContent(ctx, cfg); err != nil {
+ t.Fatalf("restored maintenance: %v", err)
+ }
+ d, err := db.Open(restoredPath, nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer d.Close()
+ state, err := readContentSealing(ctx, d.G, c)
+ if err != nil || state == nil || !state.Scrubbed {
+ t.Fatalf("restored state: %#v, %v", state, err)
+ }
+ var row DocUpdate
+ if err := d.G.Where("doc_id = ?", doc.ID).First(&row).Error; err != nil {
+ t.Fatal(err)
+ }
+ if err := c.openUpdate(doc.GUID, &row); err != nil {
+ t.Fatal(err)
+ }
+ if !bytes.Equal(row.UpdateBin, validUpdate(t)) {
+ t.Fatal("restored document changed")
+ }
+}