test(auth): gate local and OIDC authentication release
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user