The local index lists every backed-up path and chunk hash, but its file mode was left to the SQLite driver and the umask, so under a typical 022 umask a fresh index (and its -wal/-shm side files) landed world-readable. The snapshot export copied the index to snapshot.db with a permissive create as well. provideDatabase now calls ensureIndexFileMode before opening the driver: it creates the index 0600 if missing and chmods an existing one to 0600. Doing this before the driver opens the file matters because SQLite gives its -wal and -shm files the mode of the main database file. The export copy is now created 0600. Tests under umask 022 cover a fresh index, an existing 0644 index, and the export copy. Model: opus-4-8
105 lines
2.6 KiB
Go
105 lines
2.6 KiB
Go
package database
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"go.uber.org/fx"
|
|
"sneak.berlin/go/vaultik/internal/config"
|
|
"sneak.berlin/go/vaultik/internal/log"
|
|
)
|
|
|
|
// indexDirPerm restricts the local index directory to the owning user;
|
|
// the index describes the backed-up file tree and must stay private.
|
|
const indexDirPerm = 0o700
|
|
|
|
// indexFilePerm restricts the index file to the owning user; it lists every
|
|
// backed-up path and chunk hash and must stay private.
|
|
const indexFilePerm = 0o600
|
|
|
|
// ensureIndexFileMode makes the index file owner-only before the SQLite
|
|
// driver opens it: it creates the file 0600 if absent, or chmods an existing
|
|
// one to 0600. Doing this first matters because SQLite creates its -wal and
|
|
// -shm side files with the mode of the main database file, so a private main
|
|
// file yields private side files. The driver treats a zero-byte file as an
|
|
// empty database, so pre-creating it here is safe.
|
|
func ensureIndexFileMode(path string) error {
|
|
info, err := os.Stat(path)
|
|
|
|
switch {
|
|
case err == nil:
|
|
if info.Mode().Perm() == indexFilePerm {
|
|
return nil
|
|
}
|
|
|
|
err = os.Chmod(path, indexFilePerm)
|
|
if err != nil {
|
|
return fmt.Errorf("restricting index file permissions: %w", err)
|
|
}
|
|
|
|
return nil
|
|
case errors.Is(err, os.ErrNotExist):
|
|
//nolint:gosec // G304: the index path is operator-configured by design
|
|
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, indexFilePerm)
|
|
if err != nil {
|
|
return fmt.Errorf("creating index file: %w", err)
|
|
}
|
|
|
|
return f.Close()
|
|
default:
|
|
return fmt.Errorf("checking index file: %w", err)
|
|
}
|
|
}
|
|
|
|
// Module provides database dependencies
|
|
//
|
|
//nolint:gochecknoglobals // fx module definitions are package globals by convention
|
|
var Module = fx.Module("database",
|
|
fx.Provide(
|
|
provideDatabase,
|
|
NewRepositories,
|
|
),
|
|
)
|
|
|
|
func provideDatabase(lc fx.Lifecycle, cfg *config.Config) (*DB, error) {
|
|
// Ensure the index directory exists
|
|
indexDir := filepath.Dir(cfg.IndexPath)
|
|
|
|
err := os.MkdirAll(indexDir, indexDirPerm)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("creating index directory: %w", err)
|
|
}
|
|
|
|
err = ensureIndexFileMode(cfg.IndexPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
db, err := New(context.Background(), cfg.IndexPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("opening database: %w", err)
|
|
}
|
|
|
|
lc.Append(fx.Hook{
|
|
OnStop: func(_ context.Context) error {
|
|
log.Debug("Database module OnStop hook called")
|
|
|
|
err := db.Close()
|
|
if err != nil {
|
|
log.Error("Failed to close database in OnStop hook", "error", err)
|
|
|
|
return err
|
|
}
|
|
|
|
log.Debug("Database closed successfully in OnStop hook")
|
|
|
|
return nil
|
|
},
|
|
})
|
|
|
|
return db, nil
|
|
}
|