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 }