view file/file.go @ 2:a4fa4f28b472

Actually shave off last character of password description.
author Paul Fisher <paul@pfish.zone>
date Sat, 24 Oct 2015 22:34:07 -0400
parents c18bc7b9d1d9
children e58bfc7fc207
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"
	"os/user"
	"syscall"

	"path"

	"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
	ErrorBadFile = errors.New("multipass/file: Invalid file format")
)

type ShadowFile struct {
	name string
}

// ForUser gets the given user's ShadowFile.
func ForUser(username string) (*ShadowFile, error) {
	u, err := user.Lookup(username)
	if err != nil {
		return nil, err
	}
	return New(path.Join(u.HomeDir, MultipassFile)), nil
}

// ForMe gets the current user's ShadowFile.
func ForMe() (*ShadowFile, error) {
	u, err := user.Current()
	if err != nil {
		return nil, err
	}
	return New(path.Join(u.HomeDir, MultipassFile)), nil
}

// 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) 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, readable only by the current user.
	oldUmask := unix.Umask(077)
	tempFile, err := os.Create(tempName)
	unix.Umask(oldUmask)
	if err != nil {
		return nil, err
	}
	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
		}
	}
	// TODO(pfish): If there is no input file, set the right permissions + group on the output file.
	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
}