package verification import ( "context" "errors" "fmt" "log/slog" "github.com/google/uuid" "github.com/syncova/syncova/packages/platform/crypto" "github.com/syncova/syncova/packages/platform/logging" "github.com/syncova/syncova/packages/repository" ) // RepositoryResolver liefert Repository und Backupkennung zu einem Backup. // // Dieselbe Schnittstelle wie bei der Wiederherstellung, und aus demselben // Grund: Wo ein Backup liegt, weiss die Control Plane — nicht die Pruefung. type RepositoryResolver interface { // ResolveBackup liefert Pfad und Kennung eines Backups. ResolveBackup(resolveContext context.Context, backupIdentifier uuid.UUID) (repositoryPath string, backupIDInRepository string, resolveError error) } // RunnerOptions steuern den Pruef-Runner. type RunnerOptions struct { // SecretStore entschluesselt die Datenschluessel der Repositories. // // Nur der Wiederherstellungstest braucht ihn: Die Blockpruefung arbeitet // ueber StoredDigest und kommt ohne Schluessel aus. Das ist beabsichtigt — // eine Integritaetspruefung soll auch dort laufen koennen, wo der Schluessel // nicht vorliegt. SecretStore crypto.SecretStore // RestoreTestWorkingDirectory ist der Ort der Wegwerfziele. // // Leer bedeutet das Temporaerverzeichnis des Betriebssystems. Bei grossen // Backups gehoert hier ein Ort mit ausreichend Platz hin. RestoreTestWorkingDirectory string } // RepositoryRunner fuehrt Pruefungen gegen lokale Repositories aus. type RepositoryRunner struct { // resolver loest Backups auf ihr Repository auf. resolver RepositoryResolver // options sind die Einstellungen. options RunnerOptions // logger protokolliert den Verlauf. logger *slog.Logger } // Sicherstellen, dass die Schnittstelle erfuellt wird. var _ Runner = (*RepositoryRunner)(nil) // NewRepositoryRunner erzeugt den Pruef-Runner. func NewRepositoryRunner(resolver RepositoryResolver, runnerOptions RunnerOptions, baseLogger *slog.Logger) (*RepositoryRunner, error) { if resolver == nil { return nil, errors.New("der pruef-runner braucht eine aufloesung fuer backups") } return &RepositoryRunner{ resolver: resolver, options: runnerOptions, logger: logging.WithComponent(baseLogger, "verification-runner"), }, nil } // Run fuehrt die Pruefung aus. func (runner *RepositoryRunner) Run(runContext context.Context, verificationJob Job) (*Report, *RestoreTestReport, error) { repositoryPath, backupIDInRepository, resolveError := runner.resolver.ResolveBackup( runContext, verificationJob.BackupID) if resolveError != nil { return nil, nil, fmt.Errorf("das backup konnte nicht aufgeloest werden: %w", resolveError) } // Schreibgeschuetzt: Eine Pruefung darf nichts anfassen und soll neben einer // laufenden Sicherung stattfinden koennen, statt auf deren Schreibsperre zu // warten. openedRepository, openError := repository.Open(runContext, repositoryPath, repository.OpenOptions{ReadOnly: true}, runner.logger) if openError != nil { return nil, nil, fmt.Errorf("das repository ist nicht erreichbar: %w", openError) } defer func() { _ = openedRepository.Close() }() if verificationJob.VerificationType == TypeRestoreTest { return runner.runRestoreTest(runContext, openedRepository, backupIDInRepository) } verifier := NewVerifier(openedRepository) if verificationJob.VerificationType == TypeChain { chainReport, chainError := verifier.VerifyChain(runContext, backupIDInRepository) return chainReport, nil, chainError } backupReport, verifyError := verifier.VerifyBackup(runContext, backupIDInRepository, verificationJob.VerificationType) return backupReport, nil, verifyError } // runRestoreTest fuehrt einen Wiederherstellungstest aus. func (runner *RepositoryRunner) runRestoreTest(testContext context.Context, openedRepository *repository.LocalRepository, backupIDInRepository string) (*Report, *RestoreTestReport, error) { if runner.options.SecretStore == nil { // Ohne Schluessel liesse sich der Test nur scheinbar ausfuehren: Die // wiederhergestellten Dateien waeren Geheimtext und der Vergleich schluege // fehl. Das als Beschaedigung zu melden waere ein Fehlalarm. return nil, nil, errors.New( "fuer einen wiederherstellungstest fehlt der zugriff auf die datenschluessel") } tester := NewRestoreTester(openedRepository, runner.options.SecretStore, runner.logger) testReport, testError := tester.RunTest(testContext, RestoreTestOptions{ BackupID: backupIDInRepository, WorkingDirectory: runner.options.RestoreTestWorkingDirectory, }) if testError != nil { return nil, nil, testError } return &testReport.Report, testReport, nil }