feat(auth): integrate authentication with installation lifecycle
This commit is contained in:
@@ -20,8 +20,10 @@ type RestoreRequest struct {
|
||||
Drain bool
|
||||
}
|
||||
|
||||
// RestoreResult records the retained recovery point and the final service state.
|
||||
// RestoreResult records the final service state. Secret-bearing recovery checkpoints are always
|
||||
// destroyed internally and are therefore never exposed to callers.
|
||||
type RestoreResult struct {
|
||||
// Deprecated: always empty. Recovery checkpoints are private transaction internals.
|
||||
Checkpoint string
|
||||
Restarted bool
|
||||
Verified bool
|
||||
@@ -33,6 +35,9 @@ type restoreVerify func(context.Context, config.Installation, archiveRunner) err
|
||||
type restoreDependencies struct {
|
||||
preflight func(context.Context, config.Installation, PreflightRequest) (PreflightResult, error)
|
||||
checkpoint func(context.Context, config.Installation, CreateRequest) (Result, error)
|
||||
prepareRecovery func(context.Context, config.Installation, string) (PreflightResult, error)
|
||||
recover func(context.Context, config.Installation, PreflightResult, bool) error
|
||||
cleanupCheckpoint func(string) error
|
||||
acquireLock func(config.Installation) (restoreLock, error)
|
||||
runner archiveRunner
|
||||
sleep func(duration time.Duration)
|
||||
@@ -56,7 +61,7 @@ func restoreWithDependencies(ctx context.Context, installation config.Installati
|
||||
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 {
|
||||
if deps.preflight == nil || deps.checkpoint == nil || deps.prepareRecovery == nil || deps.recover == nil || deps.cleanupCheckpoint == 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})
|
||||
@@ -69,11 +74,21 @@ func restoreWithDependencies(ctx context.Context, installation config.Installati
|
||||
return RestoreResult{}, err
|
||||
}
|
||||
|
||||
checkpoint, err := deps.checkpoint(ctx, installation, CreateRequest{})
|
||||
checkpoint, err := deps.checkpoint(ctx, installation, CreateRequest{IncludeSecrets: true, Confirm: true})
|
||||
if err != nil {
|
||||
return RestoreResult{}, fmt.Errorf("create recovery checkpoint: %w", err)
|
||||
}
|
||||
result.Checkpoint = checkpoint.Path
|
||||
recovery, err := deps.prepareRecovery(ctx, installation, checkpoint.Path)
|
||||
if err != nil {
|
||||
cleanupErr := deps.cleanupCheckpoint(checkpoint.Path)
|
||||
return RestoreResult{}, errors.Join(fmt.Errorf("validate recovery checkpoint: %w", err), cleanupErr)
|
||||
}
|
||||
defer func() {
|
||||
if cleanupErr := deps.cleanupCheckpoint(checkpoint.Path); cleanupErr != nil {
|
||||
resultErr = errors.Join(resultErr, fmt.Errorf("destroy recovery checkpoint: %w", cleanupErr))
|
||||
}
|
||||
}()
|
||||
defer recovery.CloseArchive()
|
||||
lock, err := deps.acquireLock(installation)
|
||||
if err != nil {
|
||||
return result, err
|
||||
@@ -91,7 +106,9 @@ func restoreWithDependencies(ctx context.Context, installation config.Installati
|
||||
mutated := false
|
||||
defer func() {
|
||||
if resultErr != nil && mutated {
|
||||
_ = runCompose(context.Background(), installation, deps.runner, "stop")
|
||||
if recoveryErr := deps.recover(context.Background(), installation, recovery, wasRunning); recoveryErr != nil {
|
||||
resultErr = errors.Join(resultErr, fmt.Errorf("restore recovery checkpoint: %w", recoveryErr))
|
||||
}
|
||||
}
|
||||
}()
|
||||
if wasRunning {
|
||||
@@ -105,42 +122,9 @@ func restoreWithDependencies(ctx context.Context, installation config.Installati
|
||||
return result, err
|
||||
}
|
||||
}
|
||||
reader, err := zip.NewReader(archive, preflight.ArchiveSize)
|
||||
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 _, entry := range preflight.Entries {
|
||||
member := members[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)
|
||||
}
|
||||
mutated = true
|
||||
var restoreErr error
|
||||
if entry.Kind == EntryVolume {
|
||||
volume, found := restoreVolumeMetadata(preflight.Manifest, entry.LogicalName)
|
||||
if !found {
|
||||
_ = stream.Close()
|
||||
return result, errors.New("verified volume metadata is incomplete")
|
||||
}
|
||||
restoreErr = deps.restoreVolume(ctx, installation, volume, stream)
|
||||
} else {
|
||||
restoreErr = deps.restoreFile(ctx, installation, entry, stream)
|
||||
}
|
||||
closeErr := stream.Close()
|
||||
if restoreErr != nil {
|
||||
return result, restoreErr
|
||||
}
|
||||
if closeErr != nil {
|
||||
return result, closeErr
|
||||
}
|
||||
mutated = true
|
||||
if err := restoreVerifiedEntries(ctx, installation, preflight, archive, deps.restoreFile, deps.restoreVolume); err != nil {
|
||||
return result, err
|
||||
}
|
||||
if err := deps.resetAuthenticationState(ctx, installation, deps.runner); err != nil {
|
||||
return result, fmt.Errorf("reset authentication state: %w", err)
|
||||
@@ -164,6 +148,53 @@ func restoreWithDependencies(ctx context.Context, installation config.Installati
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func restoreVerifiedEntries(
|
||||
ctx context.Context,
|
||||
installation config.Installation,
|
||||
preflight PreflightResult,
|
||||
archive io.ReaderAt,
|
||||
restoreFile func(context.Context, config.Installation, ArchiveEntryMetadata, io.Reader) error,
|
||||
restoreVolume func(context.Context, config.Installation, VolumeMetadata, io.Reader) error,
|
||||
) error {
|
||||
reader, err := zip.NewReader(archive, preflight.ArchiveSize)
|
||||
if err != nil {
|
||||
return 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 _, entry := range preflight.Entries {
|
||||
member := members[entry.Path]
|
||||
if member == nil {
|
||||
return fmt.Errorf("verified archive is missing %q", entry.Path)
|
||||
}
|
||||
stream, openErr := member.Open()
|
||||
if openErr != nil {
|
||||
return fmt.Errorf("open verified archive member %q: %w", entry.Path, openErr)
|
||||
}
|
||||
var restoreErr error
|
||||
if entry.Kind == EntryVolume {
|
||||
volume, found := restoreVolumeMetadata(preflight.Manifest, entry.LogicalName)
|
||||
if !found {
|
||||
_ = stream.Close()
|
||||
return errors.New("verified volume metadata is incomplete")
|
||||
}
|
||||
restoreErr = restoreVolume(ctx, installation, volume, stream)
|
||||
} else {
|
||||
restoreErr = restoreFile(ctx, installation, entry, stream)
|
||||
}
|
||||
closeErr := stream.Close()
|
||||
if restoreErr != nil {
|
||||
return restoreErr
|
||||
}
|
||||
if closeErr != nil {
|
||||
return closeErr
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func restoreVolumeMetadata(manifest Manifest, logicalName string) (VolumeMetadata, bool) {
|
||||
if logicalName == "" {
|
||||
return VolumeMetadata{}, false
|
||||
@@ -177,12 +208,12 @@ func restoreVolumeMetadata(manifest Manifest, logicalName string) (VolumeMetadat
|
||||
}
|
||||
|
||||
// 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.
|
||||
// installation-global auth.yaml or users.yaml. A root-scoped one-shot repairs ownership and mode
|
||||
// before clearing children; links and malformed roots are rejected before any recursive removal.
|
||||
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",
|
||||
"find /data/auth -mindepth 1 -maxdepth 1 -exec rm -rf -- {} + && install -d -m 0700 /data/auth /data/auth/sessions /data/auth/oidc && test -z \"$(find /data/auth/sessions /data/auth/oidc -mindepth 1 -print -quit)\"",
|
||||
"run", "--rm", "--no-deps", "--no-TTY", "--user", "0:0", "--entrypoint", "sh", "core", "-ceu",
|
||||
"test ! -L /data/auth && { test ! -e /data/auth || test -d /data/auth; } && install -d -o 10001 -g 10001 -m 0700 /data/auth && find /data/auth -mindepth 1 -maxdepth 1 -exec rm -rf -- {} + && install -d -o 10001 -g 10001 -m 0700 /data/auth/sessions /data/auth/oidc && chmod 0700 /data/auth /data/auth/sessions /data/auth/oidc && chown 10001:10001 /data/auth /data/auth/sessions /data/auth/oidc && test -z \"$(find /data/auth/sessions /data/auth/oidc -mindepth 1 -print -quit)\"",
|
||||
), nil)
|
||||
if err != nil {
|
||||
return dockerError("reset authentication state", result, err)
|
||||
|
||||
Reference in New Issue
Block a user