feat(auth): integrate authentication with installation lifecycle
This commit is contained in:
@@ -21,7 +21,9 @@ import (
|
||||
"github.com/aritmolab/thothii/tools/tht/internal/compose"
|
||||
"github.com/aritmolab/thothii/tools/tht/internal/config"
|
||||
"github.com/aritmolab/thothii/tools/tht/internal/lifecycle"
|
||||
"github.com/aritmolab/thothii/tools/tht/internal/safeio"
|
||||
"github.com/aritmolab/thothii/tools/tht/internal/service"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -130,6 +132,10 @@ func createWithDependencies(ctx context.Context, installation config.Installatio
|
||||
if err != nil {
|
||||
return Result{}, fmt.Errorf("installation external secret references could not be read: %w", err)
|
||||
}
|
||||
authenticationPaths, err := authenticationConfigFiles(installation)
|
||||
if err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
revision, err := dependencies.revision(ctx, installation.ProjectDirectory)
|
||||
if err != nil {
|
||||
return Result{}, err
|
||||
@@ -208,12 +214,12 @@ func createWithDependencies(ctx context.Context, installation config.Installatio
|
||||
InstallationID: installationID,
|
||||
CreatedAt: dependencies.now().UTC(),
|
||||
SourceRevision: revision,
|
||||
IncludesSecrets: request.IncludeSecrets && len(secretPaths) > 0,
|
||||
IncludesSecrets: request.IncludeSecrets && len(secretPaths)+len(authenticationPaths) > 0,
|
||||
ComposeProject: installation.ProjectName(),
|
||||
Images: images,
|
||||
Volumes: volumes,
|
||||
}
|
||||
if err := writeArchive(ctx, output, reservation, installation, request, secretPaths, manifest, volumes, dependencies); err != nil {
|
||||
if err := writeArchive(ctx, output, reservation, installation, request, secretPaths, authenticationPaths, manifest, volumes, dependencies); err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
published = true
|
||||
@@ -228,8 +234,8 @@ func createWithDependencies(ctx context.Context, installation config.Installatio
|
||||
maintenanceActive = false
|
||||
}
|
||||
result = Result{Path: output}
|
||||
if request.IncludeSecrets {
|
||||
result.Warning = "The archive contains external secret files. Protect its custody and access."
|
||||
if manifest.IncludesSecrets {
|
||||
result.Warning = "The archive contains external secret files, including authentication configuration. Protect its custody and access."
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
@@ -631,7 +637,7 @@ func runCompose(ctx context.Context, installation config.Installation, runner ar
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeArchive(ctx context.Context, output string, reservation *archiveReservation, installation config.Installation, request CreateRequest, secretPaths []string, manifest Manifest, volumes []VolumeMetadata, dependencies dependencies) (resultErr error) {
|
||||
func writeArchive(ctx context.Context, output string, reservation *archiveReservation, installation config.Installation, request CreateRequest, secretPaths, authenticationPaths []string, manifest Manifest, volumes []VolumeMetadata, dependencies dependencies) (resultErr error) {
|
||||
directory := filepath.Dir(output)
|
||||
temporary, err := os.CreateTemp(directory, ".tht-backup-*.tmp")
|
||||
if err != nil {
|
||||
@@ -732,6 +738,23 @@ func writeArchive(ctx context.Context, output string, reservation *archiveReserv
|
||||
SourcePath: source, SHA256: "sha256:" + hex.EncodeToString(hash.Sum(nil)), Size: size, Sensitive: true,
|
||||
})
|
||||
}
|
||||
for index, source := range authenticationPaths {
|
||||
name := fmt.Sprintf("authentication-secrets/%03d-%s", index, filepath.Base(source))
|
||||
if request.IncludeSecrets {
|
||||
if err := addFile(name, "authentication-configuration", true, source); err != nil {
|
||||
return err
|
||||
}
|
||||
entry := &manifest.Entries[len(manifest.Entries)-1]
|
||||
entry.Kind, entry.SourcePath, entry.Sensitive = EntryExternalSecret, source, true
|
||||
continue
|
||||
}
|
||||
if filepath.Base(source) != "auth.yaml" {
|
||||
continue
|
||||
}
|
||||
if err := addAuthenticationReference(&manifest, name, source); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, volume := range volumes {
|
||||
headerName := "volumes/" + volume.LogicalName + ".tar"
|
||||
header := &zip.FileHeader{Name: headerName, Method: zip.Deflate}
|
||||
@@ -804,6 +827,51 @@ func configurationInputs(installation config.Installation) []configurationInput
|
||||
return inputs
|
||||
}
|
||||
|
||||
const maxAuthenticationConfigurationBytes = 1 << 20
|
||||
|
||||
func authenticationConfigFiles(installation config.Installation) ([]string, error) {
|
||||
directory := installation.AuthenticationDirectory()
|
||||
if directory == "" {
|
||||
return nil, nil
|
||||
}
|
||||
if err := safeio.ValidatePrivateDirectory(directory); err != nil {
|
||||
return nil, errors.New("authentication configuration directory is unavailable or unsafe")
|
||||
}
|
||||
authPath := filepath.Join(directory, "auth.yaml")
|
||||
contents, err := safeio.ReadCanonicalRegular(authPath, maxAuthenticationConfigurationBytes)
|
||||
if err != nil {
|
||||
return nil, errors.New("authentication configuration is unavailable or unsafe")
|
||||
}
|
||||
var configuration struct {
|
||||
Mode string `yaml:"mode"`
|
||||
}
|
||||
if err := yaml.Unmarshal(contents, &configuration); err != nil || (configuration.Mode != "local" && configuration.Mode != "oidc") {
|
||||
return nil, errors.New("authentication configuration is unavailable or invalid")
|
||||
}
|
||||
paths := []string{authPath}
|
||||
if configuration.Mode == "local" {
|
||||
usersPath := filepath.Join(directory, "users.yaml")
|
||||
if _, err := safeio.ReadCanonicalRegular(usersPath, maxAuthenticationConfigurationBytes); err != nil {
|
||||
return nil, errors.New("authentication user registry is unavailable or unsafe")
|
||||
}
|
||||
paths = append(paths, usersPath)
|
||||
}
|
||||
return paths, nil
|
||||
}
|
||||
|
||||
func addAuthenticationReference(manifest *Manifest, name, source string) error {
|
||||
contents, err := safeio.ReadCanonicalRegular(source, maxAuthenticationConfigurationBytes)
|
||||
if err != nil {
|
||||
return errors.New("authentication configuration is unavailable or unsafe")
|
||||
}
|
||||
digest := sha256.Sum256(contents)
|
||||
manifest.Entries = append(manifest.Entries, Entry{
|
||||
Path: name, Kind: EntrySecretReference, Owner: "authentication-configuration", SourcePath: source,
|
||||
SHA256: "sha256:" + hex.EncodeToString(digest[:]), Size: int64(len(contents)), Sensitive: true,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func volumeArchiveCommand(volume string) []string {
|
||||
return []string{"run", "--rm", "--network", "none", "--mount", "type=volume,src=" + volume + ",dst=/source,readonly", helperImage, "tar", "--numeric-owner", "-C", "/source", "-cf", "-", "."}
|
||||
}
|
||||
@@ -837,6 +905,10 @@ func archivePreservationRoots(installation config.Installation, secretPaths []st
|
||||
if err != nil || !info.IsDir() {
|
||||
return errors.New("server preservation root is unavailable")
|
||||
}
|
||||
authStateRoot := ""
|
||||
if variable == "THT_DATA_ROOT" {
|
||||
authStateRoot = filepath.Join(root, "auth")
|
||||
}
|
||||
err = filepath.WalkDir(root, func(path string, entry os.DirEntry, walkErr error) error {
|
||||
if walkErr != nil {
|
||||
return walkErr
|
||||
@@ -847,6 +919,12 @@ func archivePreservationRoots(installation config.Installation, secretPaths []st
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if authStateRoot != "" && path == authStateRoot {
|
||||
if entry.IsDir() {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if entry.Type()&os.ModeSymlink != 0 {
|
||||
return errors.New("server preservation root contains a symlink")
|
||||
}
|
||||
|
||||
@@ -70,6 +70,75 @@ func TestCreateWritesManifestLastWithConfigurationMetadataAndSevenVolumes(t *tes
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateReferencesAuthFilesByDefaultAndArchivesThemOnlyWithSecretCustody(t *testing.T) {
|
||||
fixture := newBackupFixture(t, "local")
|
||||
authDirectory := filepath.Join(filepath.Dir(fixture.installation.Path), "auth")
|
||||
if err := os.Mkdir(authDirectory, 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
authPath := filepath.Join(authDirectory, "auth.yaml")
|
||||
usersPath := filepath.Join(authDirectory, "users.yaml")
|
||||
if err := os.WriteFile(authPath, []byte("version: 1\nmode: local\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(usersPath, []byte("users:\n - passwordHash: must-not-be-archived-by-default\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fixture.installation.Authentication.ConfigDirectory = authDirectory
|
||||
|
||||
defaultOutput := filepath.Join(t.TempDir(), "default.zip")
|
||||
defaultResult, err := createWithDependencies(context.Background(), fixture.installation, CreateRequest{Output: defaultOutput}, testDependencies(t, newBackupRunner(fixture.installation, false)))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if defaultResult.Warning != "" {
|
||||
t.Fatalf("default backup warning = %q, want no custody warning", defaultResult.Warning)
|
||||
}
|
||||
defaultArchive := readFixtureArchive(t, defaultOutput)
|
||||
defaultBytes := bytes.Join(mapValues(defaultArchive.files), nil)
|
||||
for _, value := range []string{"mode: local", "must-not-be-archived-by-default"} {
|
||||
if bytes.Contains(defaultBytes, []byte(value)) {
|
||||
t.Fatalf("default backup contains authentication content %q", value)
|
||||
}
|
||||
}
|
||||
if !manifestHasReference(defaultArchive.manifest, authPath) || manifestHasReference(defaultArchive.manifest, usersPath) {
|
||||
t.Fatalf("default backup did not record only the auth.yaml configuration path: %#v", defaultArchive.manifest.Entries)
|
||||
}
|
||||
|
||||
secretOutput := filepath.Join(t.TempDir(), "with-auth-secrets.zip")
|
||||
secretResult, err := createWithDependencies(context.Background(), fixture.installation, CreateRequest{Output: secretOutput, IncludeSecrets: true, Confirm: true}, testDependencies(t, newBackupRunner(fixture.installation, false)))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(secretResult.Warning, "custody") {
|
||||
t.Fatalf("secret backup warning = %q, want custody guidance", secretResult.Warning)
|
||||
}
|
||||
secretArchive := readFixtureArchive(t, secretOutput)
|
||||
for _, path := range []string{authPath, usersPath} {
|
||||
if !manifestHasArchivedSecret(secretArchive.manifest, path) {
|
||||
t.Fatalf("secret backup did not archive authentication file %q", path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func manifestHasReference(manifest Manifest, sourcePath string) bool {
|
||||
for _, entry := range manifest.Entries {
|
||||
if entry.Kind == EntrySecretReference && entry.SourcePath == sourcePath && !entry.Archived {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func manifestHasArchivedSecret(manifest Manifest, sourcePath string) bool {
|
||||
for _, entry := range manifest.Entries {
|
||||
if entry.Kind == EntryExternalSecret && entry.SourcePath == sourcePath && entry.Archived && entry.Sensitive {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func TestCreateRestartsAndVerifiesAnInstallationThatWasRunning(t *testing.T) {
|
||||
fixture := newBackupFixture(t, "local")
|
||||
runner := newBackupRunner(fixture.installation, true)
|
||||
|
||||
@@ -27,18 +27,19 @@ type RestoreResult struct {
|
||||
Verified bool
|
||||
}
|
||||
|
||||
type restoreLock interface { Release() error }
|
||||
type restoreLock interface{ Release() error }
|
||||
type restoreVerify func(context.Context, config.Installation, archiveRunner) error
|
||||
|
||||
type restoreDependencies struct {
|
||||
preflight func(context.Context, config.Installation, PreflightRequest) (PreflightResult, error)
|
||||
checkpoint func(context.Context, config.Installation, CreateRequest) (Result, error)
|
||||
acquireLock func(config.Installation) (restoreLock, error)
|
||||
runner archiveRunner
|
||||
sleep func(duration time.Duration)
|
||||
restoreFile func(context.Context, config.Installation, ArchiveEntryMetadata, io.Reader) error
|
||||
restoreVolume func(context.Context, config.Installation, VolumeMetadata, io.Reader) error
|
||||
verify map[string]restoreVerify
|
||||
preflight func(context.Context, config.Installation, PreflightRequest) (PreflightResult, error)
|
||||
checkpoint func(context.Context, config.Installation, CreateRequest) (Result, error)
|
||||
acquireLock func(config.Installation) (restoreLock, error)
|
||||
runner archiveRunner
|
||||
sleep func(duration time.Duration)
|
||||
restoreFile func(context.Context, config.Installation, ArchiveEntryMetadata, io.Reader) error
|
||||
restoreVolume func(context.Context, config.Installation, VolumeMetadata, io.Reader) error
|
||||
resetAuthenticationState func(context.Context, config.Installation, archiveRunner) error
|
||||
verify map[string]restoreVerify
|
||||
}
|
||||
|
||||
// Restore runs the host transaction. Concrete host dependencies are intentionally kept outside
|
||||
@@ -48,60 +49,126 @@ func Restore(ctx context.Context, installation config.Installation, request Rest
|
||||
}
|
||||
|
||||
func restoreWithDependencies(ctx context.Context, installation config.Installation, request RestoreRequest, deps restoreDependencies) (result RestoreResult, resultErr error) {
|
||||
if !request.Confirm { return RestoreResult{}, ErrRestoreConfirmationRequired }
|
||||
if request.Archive == "" { return RestoreResult{}, errors.New("restore archive is required") }
|
||||
if deps.preflight == nil || deps.checkpoint == nil || deps.acquireLock == nil || deps.runner == nil || deps.restoreFile == nil || deps.restoreVolume == nil || deps.verify == nil {
|
||||
if !request.Confirm {
|
||||
return RestoreResult{}, ErrRestoreConfirmationRequired
|
||||
}
|
||||
if request.Archive == "" {
|
||||
return RestoreResult{}, errors.New("restore archive is required")
|
||||
}
|
||||
if deps.preflight == nil || deps.checkpoint == nil || deps.acquireLock == nil || deps.runner == nil || deps.restoreFile == nil || deps.restoreVolume == nil || deps.resetAuthenticationState == nil || deps.verify == nil {
|
||||
return RestoreResult{}, errors.New("restore dependencies are incomplete")
|
||||
}
|
||||
preflight, err := deps.preflight(ctx, installation, PreflightRequest{Archive: request.Archive, Confirm: true, AllowExternalSecrets: true})
|
||||
if err != nil { return RestoreResult{}, err }
|
||||
if err != nil {
|
||||
return RestoreResult{}, err
|
||||
}
|
||||
defer preflight.CloseArchive()
|
||||
archive, err := preflight.RevalidateArchive()
|
||||
if err != nil { return RestoreResult{}, err }
|
||||
if err != nil {
|
||||
return RestoreResult{}, err
|
||||
}
|
||||
|
||||
checkpoint, err := deps.checkpoint(ctx, installation, CreateRequest{})
|
||||
if err != nil { return RestoreResult{}, fmt.Errorf("create recovery checkpoint: %w", err) }
|
||||
if err != nil {
|
||||
return RestoreResult{}, fmt.Errorf("create recovery checkpoint: %w", err)
|
||||
}
|
||||
result.Checkpoint = checkpoint.Path
|
||||
lock, err := deps.acquireLock(installation)
|
||||
if err != nil { return result, err }
|
||||
defer func() { if releaseErr := lock.Release(); releaseErr != nil && resultErr == nil { resultErr = releaseErr } }()
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
defer func() {
|
||||
if releaseErr := lock.Release(); releaseErr != nil && resultErr == nil {
|
||||
resultErr = releaseErr
|
||||
}
|
||||
}()
|
||||
|
||||
wasRunning, err := installationRunning(ctx, installation, deps.runner)
|
||||
if err != nil { return result, err }
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
mutated := false
|
||||
defer func() {
|
||||
if resultErr != nil && mutated { _ = runCompose(context.Background(), installation, deps.runner, "stop") }
|
||||
if resultErr != nil && mutated {
|
||||
_ = runCompose(context.Background(), installation, deps.runner, "stop")
|
||||
}
|
||||
}()
|
||||
if wasRunning {
|
||||
if err := maintenance(ctx, installation, deps.runner, true); err != nil { return result, err }
|
||||
if err := waitForNoActiveSessions(ctx, installation, deps.runner, request.Drain, deps.sleep); err != nil { return result, err }
|
||||
if err := runCompose(ctx, installation, deps.runner, "stop"); err != nil { return result, err }
|
||||
if err := maintenance(ctx, installation, deps.runner, true); err != nil {
|
||||
return result, err
|
||||
}
|
||||
if err := waitForNoActiveSessions(ctx, installation, deps.runner, request.Drain, deps.sleep); err != nil {
|
||||
return result, err
|
||||
}
|
||||
if err := runCompose(ctx, installation, deps.runner, "stop"); err != nil {
|
||||
return result, err
|
||||
}
|
||||
}
|
||||
reader, err := zip.NewReader(archive, preflight.ArchiveSize)
|
||||
if err != nil { return result, fmt.Errorf("read verified restore archive: %w", err) }
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("read verified restore archive: %w", err)
|
||||
}
|
||||
members := make(map[string]*zip.File, len(reader.File))
|
||||
for _, member := range reader.File { members[member.Name] = member }
|
||||
for _, member := range reader.File {
|
||||
members[member.Name] = member
|
||||
}
|
||||
for _, entry := range preflight.Entries {
|
||||
if entry.Kind == EntryVolume { continue }
|
||||
if entry.Kind == EntryVolume {
|
||||
continue
|
||||
}
|
||||
member := members[entry.Path]
|
||||
if member == nil { return result, fmt.Errorf("verified archive is missing %q", entry.Path) }
|
||||
if member == nil {
|
||||
return result, fmt.Errorf("verified archive is missing %q", entry.Path)
|
||||
}
|
||||
stream, openErr := member.Open()
|
||||
if openErr != nil { return result, fmt.Errorf("open verified archive member %q: %w", entry.Path, openErr) }
|
||||
if openErr != nil {
|
||||
return result, fmt.Errorf("open verified archive member %q: %w", entry.Path, openErr)
|
||||
}
|
||||
mutated = true
|
||||
restoreErr := deps.restoreFile(ctx, installation, entry, stream)
|
||||
closeErr := stream.Close()
|
||||
if restoreErr != nil { return result, restoreErr }
|
||||
if closeErr != nil { return result, closeErr }
|
||||
if restoreErr != nil {
|
||||
return result, restoreErr
|
||||
}
|
||||
if closeErr != nil {
|
||||
return result, closeErr
|
||||
}
|
||||
}
|
||||
if err := deps.resetAuthenticationState(ctx, installation, deps.runner); err != nil {
|
||||
return result, fmt.Errorf("reset authentication state: %w", err)
|
||||
}
|
||||
if wasRunning {
|
||||
if err := composeStartAndVerify(ctx, installation, deps.runner); err != nil { return result, err }
|
||||
if err := composeStartAndVerify(ctx, installation, deps.runner); err != nil {
|
||||
return result, err
|
||||
}
|
||||
result.Restarted = true
|
||||
}
|
||||
for _, name := range []string{"health", "doctor", "pi", "workspace"} {
|
||||
check := deps.verify[name]
|
||||
if check == nil { return result, fmt.Errorf("restore verification %q is unavailable", name) }
|
||||
if err := check(ctx, installation, deps.runner); err != nil { return result, fmt.Errorf("restore verification %s: %w", name, err) }
|
||||
if check == nil {
|
||||
return result, fmt.Errorf("restore verification %q is unavailable", name)
|
||||
}
|
||||
if err := check(ctx, installation, deps.runner); err != nil {
|
||||
return result, fmt.Errorf("restore verification %s: %w", name, err)
|
||||
}
|
||||
}
|
||||
result.Verified = true
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// resetAuthenticationState clears browser sessions and pending OIDC transactions without touching
|
||||
// installation-global auth.yaml or users.yaml. The command runs as the unprivileged core user so
|
||||
// the recreated state root is private to the service on both the local volume and server /data bind.
|
||||
func resetAuthenticationState(ctx context.Context, installation config.Installation, runner archiveRunner) error {
|
||||
result, err := runner.Run(ctx, installation.ComposeArgs(
|
||||
"run", "--rm", "--no-deps", "--no-TTY", "--entrypoint", "sh", "core", "-ceu",
|
||||
"rm -rf /data/auth && install -d -m 0700 /data/auth /data/auth/sessions /data/auth/oidc",
|
||||
), nil)
|
||||
if err != nil {
|
||||
return dockerError("reset authentication state", result, err)
|
||||
}
|
||||
if result.ExitCode != 0 {
|
||||
return dockerError("reset authentication state", result, errors.New("Compose returned a nonzero exit status"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -59,6 +59,58 @@ func TestRestoreStoppedInstallationRunsCheckpointRestoreAndVerification(t *testi
|
||||
}
|
||||
}
|
||||
|
||||
func TestRestoreResetsAuthenticationStateBeforeRestart(t *testing.T) {
|
||||
installation := preflightTestInstallation(t)
|
||||
archive := restoreArchive(t)
|
||||
runner := newBackupRunner(installation, true)
|
||||
deps := restoreTestDependencies(t, runner)
|
||||
var events []string
|
||||
deps.restoreFile = func(_ context.Context, _ config.Installation, entry ArchiveEntryMetadata, _ io.Reader) error {
|
||||
events = append(events, "file:"+entry.Path)
|
||||
return nil
|
||||
}
|
||||
deps.resetAuthenticationState = func(context.Context, config.Installation, archiveRunner) error {
|
||||
events = append(events, "reset-auth-state")
|
||||
return nil
|
||||
}
|
||||
for _, name := range []string{"health", "doctor", "pi", "workspace"} {
|
||||
name := name
|
||||
deps.verify[name] = func(context.Context, config.Installation, archiveRunner) error {
|
||||
events = append(events, name)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
result, err := restoreWithDependencies(context.Background(), installation, RestoreRequest{Archive: archive, Confirm: true}, deps)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !result.Restarted || !result.Verified {
|
||||
t.Fatalf("restore result = %#v", result)
|
||||
}
|
||||
if got, want := events, []string{"file:configuration/operator.env", "reset-auth-state", "health", "doctor", "pi", "workspace"}; !equalStrings(got, want) {
|
||||
t.Fatalf("restore events = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResetAuthenticationStateCreatesOnlyPrivateEmptyStateDirectories(t *testing.T) {
|
||||
installation := preflightTestInstallation(t)
|
||||
runner := &authenticationStateResetRunner{}
|
||||
|
||||
if err := resetAuthenticationState(context.Background(), installation, runner); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
joined := strings.Join(runner.args, "\x00")
|
||||
for _, required := range []string{
|
||||
"run", "--rm", "--no-deps", "--no-TTY", "--entrypoint", "sh", "core", "-ceu",
|
||||
"rm -rf /data/auth && install -d -m 0700 /data/auth /data/auth/sessions /data/auth/oidc",
|
||||
} {
|
||||
if !strings.Contains(joined, required) {
|
||||
t.Fatalf("authentication state reset command omits %q: %#v", required, runner.args)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRestorePreflightFailureDoesNotMutateTarget(t *testing.T) {
|
||||
installation := preflightTestInstallation(t)
|
||||
runner := newBackupRunner(installation, true)
|
||||
@@ -213,6 +265,19 @@ type fakeRestoreLock struct {
|
||||
release func()
|
||||
}
|
||||
|
||||
type authenticationStateResetRunner struct{ args []string }
|
||||
|
||||
func (runner *authenticationStateResetRunner) Run(_ context.Context, args []string, _ io.Reader) (compose.Result, error) {
|
||||
runner.args = append([]string(nil), args...)
|
||||
return compose.Result{}, nil
|
||||
}
|
||||
|
||||
func (runner *authenticationStateResetRunner) Stream(context.Context, []string, io.Reader, io.Writer) (compose.Result, error) {
|
||||
return compose.Result{}, errors.New("authentication state reset must not stream a volume archive")
|
||||
}
|
||||
|
||||
func (*authenticationStateResetRunner) SessionInventoryScope() string { return "mine" }
|
||||
|
||||
func (lock fakeRestoreLock) Release() error {
|
||||
if lock.release != nil {
|
||||
lock.release()
|
||||
@@ -248,6 +313,9 @@ func restoreTestDependencies(t *testing.T, runner archiveRunner) restoreDependen
|
||||
restoreVolume: func(context.Context, config.Installation, VolumeMetadata, io.Reader) error {
|
||||
return nil
|
||||
},
|
||||
resetAuthenticationState: func(context.Context, config.Installation, archiveRunner) error {
|
||||
return nil
|
||||
},
|
||||
verify: map[string]restoreVerify{
|
||||
"health": func(context.Context, config.Installation, archiveRunner) error { return nil },
|
||||
"doctor": func(context.Context, config.Installation, archiveRunner) error { return nil },
|
||||
|
||||
Reference in New Issue
Block a user