github.com/wostzone/hub/auth@v0.0.0-20220118060317-7bb375743b17/pkg/unpwstore/PasswordFileStore.go (about)

     1  package unpwstore
     2  
     3  import (
     4  	"bufio"
     5  	"fmt"
     6  	"os"
     7  	"path"
     8  	"strings"
     9  	"sync"
    10  
    11  	"github.com/fsnotify/fsnotify"
    12  	"github.com/sirupsen/logrus"
    13  	"github.com/wostzone/hub/lib/serve/pkg/watcher"
    14  )
    15  
    16  // DefaultPasswordFile is the recommended password filename for Hub authentication
    17  const DefaultPasswordFile = "hub.passwd"
    18  
    19  // PasswordFileStore stores a list of user login names and their password info
    20  // It includes a file watcher to automatically reload on update.
    21  type PasswordFileStore struct {
    22  	clientID  string            // the user of the store for logging
    23  	passwords map[string]string // loginName: bcrypted(password)
    24  	storePath string
    25  	watcher   *fsnotify.Watcher
    26  	mutex     sync.RWMutex
    27  }
    28  
    29  // Close the store
    30  func (pwStore *PasswordFileStore) Close() {
    31  	logrus.Infof("PasswordFileStore.Close. clientID='%s'", pwStore.clientID)
    32  	if pwStore.watcher != nil {
    33  		pwStore.watcher.Close()
    34  		pwStore.watcher = nil
    35  	}
    36  }
    37  
    38  // Count nr of entries in the store
    39  func (pwStore *PasswordFileStore) Count() int {
    40  	pwStore.mutex.RLock()
    41  	defer pwStore.mutex.RUnlock()
    42  
    43  	return len(pwStore.passwords)
    44  }
    45  
    46  // GetPasswordHash returns the stored encoded password hash for the given user
    47  // returns hash or "" if user not found
    48  func (pwStore *PasswordFileStore) GetPasswordHash(username string) string {
    49  	pwStore.mutex.RLock()
    50  	defer pwStore.mutex.RUnlock()
    51  	hash := pwStore.passwords[username]
    52  	return hash
    53  }
    54  
    55  // Open the store
    56  // This reads the password file and subscribes to file changes
    57  func (pwStore *PasswordFileStore) Open() error {
    58  	logrus.Infof("PasswordFileStore.Open. clientID='%s'", pwStore.clientID)
    59  	err := pwStore.Reload()
    60  	if err == nil {
    61  		pwStore.watcher, err = watcher.WatchFile(pwStore.storePath, pwStore.Reload, pwStore.clientID)
    62  	}
    63  	return err
    64  }
    65  
    66  // Reload the password store from file and subscribe to file changes
    67  // If subscription already exists it is removed and renewed.
    68  //  File format:  <loginname>:bcrypt(passwd)
    69  // Returns error if the file could not be opened
    70  func (pwStore *PasswordFileStore) Reload() error {
    71  	logrus.Infof("PasswordFileStore.Reload: clientID='%s', Reloading passwords from '%s'",
    72  		pwStore.clientID, pwStore.storePath)
    73  	pwStore.mutex.Lock()
    74  	defer pwStore.mutex.Unlock()
    75  
    76  	pwList := make(map[string]string)
    77  	file, err := os.Open(pwStore.storePath)
    78  	if err != nil {
    79  		err := fmt.Errorf("PasswordFileStore.Reload: clientID='%s', Failed to open password file: %s", pwStore.clientID, err)
    80  		logrus.Error(err)
    81  		return err
    82  	}
    83  	defer file.Close()
    84  
    85  	scanner := bufio.NewScanner(file)
    86  	scanner.Split(bufio.ScanLines)
    87  	for scanner.Scan() {
    88  		line := scanner.Text()
    89  		if len(line) == 0 || line[0] == '#' {
    90  			// ignore
    91  		} else {
    92  			parts := strings.Split(line, ":")
    93  			if len(parts) == 2 {
    94  				username := parts[0]
    95  				pass := parts[1]
    96  				pwList[username] = pass
    97  			}
    98  		}
    99  	}
   100  	logrus.Infof("Reload: clientID='%s', loaded %d passwords", pwStore.clientID, len(pwList))
   101  
   102  	pwStore.passwords = pwList
   103  
   104  	return err
   105  }
   106  
   107  // SetPasswordHash adds/updates the password hash for the given login ID
   108  // Intended for use by administrators to add a new user or clients to update their password
   109  func (pwStore *PasswordFileStore) SetPasswordHash(loginID string, hash string) error {
   110  	logrus.Infof("PasswordFileStore.SetPasswordHash clientID='%s', for login '%s'", pwStore.clientID, loginID)
   111  	pwStore.mutex.Lock()
   112  	defer pwStore.mutex.Unlock()
   113  	if pwStore.passwords == nil {
   114  		logrus.Panic("Use of password store before open")
   115  	}
   116  	pwStore.passwords[loginID] = hash
   117  
   118  	folder := path.Dir(pwStore.storePath)
   119  	tmpPath, err := WritePasswordsToTempFile(folder, pwStore.passwords)
   120  	if err != nil {
   121  		logrus.Errorf("SetPasswordHash clientID='%s'. loginID='%s' write to temp failed: %s",
   122  			pwStore.clientID, loginID, err)
   123  		return err
   124  	}
   125  
   126  	err = os.Rename(tmpPath, pwStore.storePath)
   127  	if err != nil {
   128  		logrus.Errorf("SetPasswordHash clientID='%s'. loginID='%s' rename to password file failed: %s",
   129  			pwStore.clientID, loginID, err)
   130  		return err
   131  	}
   132  
   133  	logrus.Infof("PasswordFileStore.SetPasswordHash: clientID='%s'. password hash for loginID '%s' updated", pwStore.clientID, loginID)
   134  	return err
   135  }
   136  
   137  // WritePasswordsToTempFile write the given passwords to temp file in the given folder
   138  // This returns the name of the new temp file.
   139  func WritePasswordsToTempFile(
   140  	folder string, passwords map[string]string) (tempFileName string, err error) {
   141  
   142  	file, err := os.CreateTemp(folder, "hub-pwfilestore")
   143  
   144  	// file, err := os.OpenFile(path, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0600)
   145  	if err != nil {
   146  		err := fmt.Errorf("PasswordFileStore.Write: Failed open temp password file: %s", err)
   147  		logrus.Error(err)
   148  		return "", err
   149  	}
   150  	tempFileName = file.Name()
   151  	logrus.Infof("PasswordFileStore.WritePasswordsToTempFile: %s", tempFileName)
   152  
   153  	defer file.Close()
   154  	writer := bufio.NewWriter(file)
   155  
   156  	for key, value := range passwords {
   157  		_, err = writer.WriteString(fmt.Sprintf("%s:%s\n", key, value))
   158  		if err != nil {
   159  			err := fmt.Errorf("PasswordFileStore.Write: Failed writing password file: %s", err)
   160  			logrus.Error(err)
   161  			return tempFileName, err
   162  		}
   163  	}
   164  	writer.Flush()
   165  	return tempFileName, err
   166  }
   167  
   168  // NewPasswordFileStore creates a new instance of a file based username/password store
   169  // Note: this store is intended for one writer and many readers.
   170  // Multiple concurrent writes are not supported and might lead to one write being ignored.
   171  //  filepath location of the file store. See also DefaultPasswordFile for the recommended name
   172  //  clientID is authservice ID to include in logging
   173  func NewPasswordFileStore(filepath string, clientID string) *PasswordFileStore {
   174  	store := &PasswordFileStore{
   175  		clientID:  clientID,
   176  		storePath: filepath,
   177  	}
   178  	return store
   179  }