test(auth): gate local and OIDC authentication release

This commit is contained in:
2026-08-18 06:02:25 +02:00
parent 7cf7d9db6b
commit 8a3fa5031d
23 changed files with 1711 additions and 168 deletions
+122 -3
View File
@@ -91,6 +91,14 @@ type verifiedArchive struct {
limits PreflightLimits
}
// stagedArchive holds an installation-private, immutable copy of the exact bytes accepted by
// Preflight. The original archive remains retained only for provenance revalidation.
type stagedArchive struct {
file *os.File
path string
directory string
}
type inspectedArchiveEntry struct {
metadata ArchiveEntryMetadata
member *zip.File
@@ -176,12 +184,17 @@ func Preflight(ctx context.Context, installation config.Installation, request Pr
if err != nil {
return PreflightResult{}, err
}
stagingBytes := uint64(openedInfo.Size())
if stagingBytes > ^uint64(0)-requiredBytes {
return PreflightResult{}, errors.New("restore staging requirement exceeds supported size")
}
requiredWithStaging := requiredBytes + stagingBytes
freeBytes, err := dependencies.FreeBytes(installation.ProjectDirectory)
if err != nil {
return PreflightResult{}, fmt.Errorf("check free disk space: %w", err)
}
if freeBytes < requiredBytes {
return PreflightResult{}, fmt.Errorf("insufficient free disk space for restore: need %d bytes, have %d", requiredBytes, freeBytes)
if freeBytes < requiredWithStaging {
return PreflightResult{}, fmt.Errorf("insufficient free disk space for restore: need %d bytes, have %d", requiredWithStaging, freeBytes)
}
if err := contextError(ctx); err != nil {
return PreflightResult{}, err
@@ -214,6 +227,10 @@ func Preflight(ctx context.Context, installation config.Installation, request Pr
// it immediately before a restore transaction and use the returned retained handle, never reopen
// ArchivePath. It refuses a path replacement or in-place content change.
func (result PreflightResult) RevalidateArchive() (*os.File, error) {
return result.revalidateArchive(context.Background())
}
func (result PreflightResult) revalidateArchive(ctx context.Context) (*os.File, error) {
if result.archive == nil || result.archive.file == nil {
return nil, errors.New("backup archive has not been retained by preflight")
}
@@ -228,13 +245,115 @@ func (result PreflightResult) RevalidateArchive() (*os.File, error) {
if err != nil || !os.SameFile(result.archive.info, heldInfo) || heldInfo.Size() != result.ArchiveSize {
return nil, errors.New("backup archive changed after preflight")
}
digest, err := digestArchive(context.Background(), result.archive.file, result.archive.limits.MaxArchiveBytes)
digest, err := digestArchive(ctx, result.archive.file, result.archive.limits.MaxArchiveBytes)
if err != nil || digest != result.archive.digest {
return nil, errors.New("backup archive changed after preflight")
}
return result.archive.file, nil
}
// StageArchive revalidates the retained archive and copies its exact bytes into a private file
// immediately before extraction. Later writes to the source archive cannot affect extraction.
func (result PreflightResult) StageArchive(ctx context.Context) (_ *stagedArchive, resultErr error) {
source, err := result.revalidateArchive(ctx)
if err != nil {
return nil, err
}
directory, err := os.MkdirTemp("", "tht-restore-stage-")
if err != nil {
return nil, errors.New("create private restore staging directory")
}
if err := os.Chmod(directory, 0o700); err != nil {
_ = os.Remove(directory)
return nil, errors.New("protect private restore staging directory")
}
path := filepath.Join(directory, "archive.zip")
file, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0o600)
if err != nil {
_ = os.Remove(directory)
return nil, errors.New("create private restore staging archive")
}
staged := &stagedArchive{file: file, path: path, directory: directory}
completed := false
defer func() {
if !completed {
_ = staged.Close()
}
}()
if _, err := source.Seek(0, io.SeekStart); err != nil {
return nil, errors.New("seek verified backup archive for staging")
}
digest := sha256.New()
buffer := make([]byte, 128*1024)
var total int64
for {
if err := contextError(ctx); err != nil {
return nil, err
}
count, readErr := source.Read(buffer)
if count > 0 {
if int64(count) > result.ArchiveSize-total {
return nil, errors.New("backup archive changed after preflight")
}
written, writeErr := file.Write(buffer[:count])
if writeErr != nil || written != count {
return nil, errors.New("write private restore staging archive")
}
if _, writeErr := digest.Write(buffer[:count]); writeErr != nil {
return nil, errors.New("hash private restore staging archive")
}
total += int64(count)
}
if errors.Is(readErr, io.EOF) {
break
}
if readErr != nil {
return nil, errors.New("read verified backup archive for staging")
}
}
if total != result.ArchiveSize || digestForHash(digest) != result.archive.digest {
return nil, errors.New("backup archive changed after preflight")
}
if err := file.Sync(); err != nil {
return nil, errors.New("sync private restore staging archive")
}
if _, err := file.Seek(0, io.SeekStart); err != nil {
return nil, errors.New("rewind private restore staging archive")
}
completed = true
return staged, nil
}
// Close removes only the staging file and directory created by StageArchive.
func (staged *stagedArchive) Close() error {
if staged == nil {
return nil
}
var failed bool
if staged.file != nil {
if err := staged.file.Close(); err != nil {
failed = true
}
staged.file = nil
}
if staged.path != "" {
if err := os.Remove(staged.path); err != nil && !errors.Is(err, os.ErrNotExist) {
failed = true
}
staged.path = ""
}
if staged.directory != "" {
if err := os.Remove(staged.directory); err != nil && !errors.Is(err, os.ErrNotExist) {
failed = true
}
staged.directory = ""
}
if failed {
return errors.New("destroy private restore staging archive")
}
return nil
}
// CloseArchive releases the retained read-only archive handle after the caller finishes the
// restore transaction or decides not to proceed.
func (result PreflightResult) CloseArchive() error {