view file/file.go @ 15:9b4ec6b5c23e

Add tests for multipass files.
author Paul Fisher <paul@pfish.zone>
date Thu, 29 Oct 2015 23:56:53 -0400
parents da6c493cf08a
children 00d30c67b56d
line wrap: on
line source

// Package file handles I/O for single password files.
//
// A password file contains multiple passwords for a single user.
// It starts with a banner that indicates the version of the file,
// then has entries in the format specified by auth.Entry.

package file

import (
	"bufio"
	"errors"
	"os"
	"syscall"
	"time"

	"golang.org/x/sys/unix"
	"pfish.zone/go/multipass/auth"
)

const (
	// the Banner acts as a version indicator
	Banner = "# Multipass v0.1 password file"
)

var (
	// Raised when there's an error in the file format.
	ErrorBadFile = errors.New("multipass/file: Invalid file format")

	// we spin waiting for the file to become available, doubling our wait time
	// every time it's unavailable.  If the wait time is longer than this,
	// give up.  Variable so it can be set in tests.
	maxDelay = time.Minute
)

type ShadowFile struct {
	name string
}

// New creates a ShadowFile for reading at the given path.
// If a file needs to be created, uses the given GID to create it.
func New(name string) *ShadowFile {
	f := new(ShadowFile)
	f.name = name
	return f
}

func (f *ShadowFile) newFilename() string {
	return f.name + ".new"
}

func (f *ShadowFile) open() (*os.File, *bufio.Scanner, error) {
	file, err := os.Open(f.name)
	if err != nil {
		return nil, nil, err
	}
	scanner := bufio.NewScanner(file)
	if !scanner.Scan() {
		file.Close()
		return nil, nil, err
	}
	if scanner.Text() != Banner {
		file.Close()
		return nil, nil, ErrorBadFile
	}
	return file, scanner, nil
}

func (f *ShadowFile) Authenticate(password string) (bool, error) {
	file, scanner, err := f.open()
	if err != nil {
		return false, err
	}
	defer file.Close()

	for scanner.Scan() {
		entry, err := auth.EntryFromShadow(scanner.Text())
		// Skip invalid lines.
		if err != nil {
			continue
		}
		if entry.Authenticate(password) {
			return true, nil
		}
	}
	return false, nil
}

func (f *ShadowFile) Add(entry *auth.Entry) error {
	handle, err := f.openWrite()
	if err != nil {
		return err
	}
	err = handle.write(entry)
	if err != nil {
		handle.bail()
		return err
	}
	for handle.next() {
		if entry, err := handle.entry(); err == nil {
			// If we get an invalid entry, just skip it.
			if err := handle.write(entry); err != nil {
				handle.bail()
				return err
			}
		}
	}
	return handle.finalize()
}

func (f *ShadowFile) AllEntries() ([]*auth.Entry, error) {
	file, scanner, err := f.open()
	if err != nil {
		return nil, err
	}
	defer file.Close()

	var entries []*auth.Entry

	for scanner.Scan() {
		entry, err := auth.EntryFromShadow(scanner.Text())
		// Skip invalid lines.
		if err != nil {
			continue
		}
		entries = append(entries, entry)
	}
	return entries, nil
}

func (f *ShadowFile) Remove(id uint64) error {
	handle, err := f.openWrite()
	if err != nil {
		return err
	}
	for handle.next() {
		if entry, err := handle.entry(); err == nil {
			// If we get an invalid entry, just skip it.
			if entry.ID() != id {
				if err := handle.write(entry); err != nil {
					handle.bail()
					return err
				}
			}
		}
	}
	return handle.finalize()
}

func (f *ShadowFile) openWrite() (*writeHandle, error) {
	return openWriteHandle(f.newFilename(), f.name)
}

type writeHandle struct {
	// We write to a file handle pointing to tempName.
	tempName string
	tempFile *os.File
	writer   *bufio.Writer

	// It will eventually move to fileName, which is our source file.
	fileName string
	// If inFile is nil, we're creating a new multipass file.
	inFile  *os.File
	scanner *bufio.Scanner
}

func openWriteHandle(tempName, fileName string) (*writeHandle, error) {
	h := new(writeHandle)
	h.tempName = tempName
	h.fileName = fileName
	// Open the output file, but only if it doesn't exist.
	// This prevents race conditions.
	oldUmask := unix.Umask(077)
	var tempFile *os.File
	delay := time.Nanosecond
	for tempFile == nil {
		tempTempFile, err := os.OpenFile(tempName, os.O_CREATE|os.O_EXCL|os.O_WRONLY|os.O_SYNC, 0600)
		tempFile = tempTempFile
		if err != nil {
			perr := err.(*os.PathError)
			errno, ok := perr.Err.(syscall.Errno)
			if !ok {
				return nil, err
			}
			if errno != syscall.EEXIST || delay > maxDelay {
				return nil, err
			}
			time.Sleep(delay)
			delay *= 2
		}
	}
	unix.Umask(oldUmask)
	h.tempFile = tempFile
	// Open the input file.
	inFile, err := os.Open(fileName)
	if err == nil {
		// Prepare to read in the input file, if it exists.
		h.inFile = inFile
		h.scanner = bufio.NewScanner(h.inFile)
		if !h.scanner.Scan() {
			return nil, ErrorBadFile
		}
		if h.scanner.Text() != Banner {
			return nil, ErrorBadFile
		}
		// Change the owner and group of the new file to that of the old.
		inStat, err := h.inFile.Stat()
		if err != nil {
			h.bail()
			return nil, err
		}
		inStatUnix := inStat.Sys().(*syscall.Stat_t)
		if err := h.tempFile.Chown(int(inStatUnix.Uid), int(inStatUnix.Gid)); err != nil {
			h.bail()
			return nil, err
		}
		if err := h.tempFile.Chmod(inStat.Mode()); err != nil {
			h.bail()
			return nil, err
		}
	} else {
		// TODO(pfish): Restrict ACL to only multipass authenticators.
		if err := h.tempFile.Chmod(0644); err != nil {
			h.bail()
			return nil, err
		}
	}
	h.writer = bufio.NewWriter(h.tempFile)
	if _, err := h.writer.WriteString(Banner + "\n"); err != nil {
		return nil, err
	}
	return h, nil
}

func (h *writeHandle) bail() {
	h.tempFile.Close()
	h.inFile.Close()
	os.Remove(h.tempName)
}

func (h *writeHandle) next() bool {
	// If the scanner is nil, then we have no input file and therefore no next.
	return h.scanner != nil && h.scanner.Scan()
}

func (h *writeHandle) entry() (*auth.Entry, error) {
	return auth.EntryFromShadow(h.scanner.Text())
}

func (h *writeHandle) write(entry *auth.Entry) error {
	if _, err := h.writer.WriteString(entry.Encode()); err != nil {
		return err
	}
	_, err := h.writer.WriteString("\n")
	return err
}

func (h *writeHandle) finalize() error {
	h.inFile.Close()
	// Make absolutely completely sure we're synced.
	if err := h.writer.Flush(); err != nil {
		h.tempFile.Close()
		os.Remove(h.tempName)
		return err
	}
	if err := h.tempFile.Sync(); err != nil {
		h.tempFile.Close()
		os.Remove(h.tempName)
		return err
	}
	// Close the file.
	if err := h.tempFile.Close(); err != nil {
		os.Remove(h.tempName)
		return err
	}
	// And atomically write it over the new one.
	err := os.Rename(h.tempName, h.fileName)
	if err != nil {
		return err
	}
	// If we get here, everything succeeded, and only in this case
	// will the multipass shadow file have been updated.
	return nil
}