github.com/wrgl/wrgl@v0.14.0/pkg/auth/fs/authn.go (about) 1 // SPDX-License-Identifier: Apache-2.0 2 // Copyright © 2022 Wrangle Ltd 3 4 package authfs 5 6 import ( 7 "bytes" 8 "encoding/csv" 9 "fmt" 10 "io" 11 "net/http" 12 "os" 13 "path/filepath" 14 "sort" 15 "sync" 16 "time" 17 18 "github.com/fsnotify/fsnotify" 19 "github.com/wrgl/wrgl/pkg/auth" 20 "github.com/wrgl/wrgl/pkg/auth/random" 21 "github.com/wrgl/wrgl/pkg/local" 22 "golang.org/x/crypto/bcrypt" 23 ) 24 25 const DefaultTokenDuration time.Duration = time.Hour * 24 * 90 // 90 days 26 27 type AuthnStore struct { 28 // sl is all current data, each element is a list of {email, name, passwordHash} 29 sl [][]string 30 rootDir string 31 secret []byte 32 tokenDuration time.Duration 33 watcher *fsnotify.Watcher 34 mutex sync.Mutex 35 ErrChan chan error 36 done chan struct{} 37 wg sync.WaitGroup 38 } 39 40 func NewAuthnStore(rd *local.RepoDir, tokenDuration time.Duration) (s *AuthnStore, err error) { 41 if tokenDuration == 0 { 42 tokenDuration = DefaultTokenDuration 43 } 44 s = &AuthnStore{ 45 rootDir: rd.FullPath, 46 tokenDuration: tokenDuration, 47 ErrChan: make(chan error, 1), 48 done: make(chan struct{}, 1), 49 } 50 if err = s.read(); err != nil { 51 return nil, err 52 } 53 s.watcher, err = rd.Watcher() 54 if err != nil { 55 return nil, err 56 } 57 go s.watch() 58 return s, nil 59 } 60 61 func (s *AuthnStore) Filepath() string { 62 return filepath.Join(s.rootDir, "authn.csv") 63 } 64 65 func (s *AuthnStore) read() error { 66 s.mutex.Lock() 67 defer s.mutex.Unlock() 68 fp := s.Filepath() 69 f, err := os.Open(fp) 70 if err == nil { 71 defer f.Close() 72 r := csv.NewReader(f) 73 s.sl, err = r.ReadAll() 74 if err != nil { 75 return err 76 } 77 } 78 return nil 79 } 80 81 func (s *AuthnStore) watch() { 82 s.wg.Add(1) 83 defer s.wg.Done() 84 for { 85 select { 86 case event, ok := <-s.watcher.Events: 87 if !ok { 88 return 89 } 90 if event.Op&fsnotify.Write == fsnotify.Write || event.Op&fsnotify.Create == fsnotify.Create { 91 if event.Name == s.Filepath() { 92 if err := s.read(); err != nil { 93 s.ErrChan <- err 94 } 95 } 96 } 97 case err, ok := <-s.watcher.Errors: 98 if !ok { 99 return 100 } 101 s.ErrChan <- err 102 case <-s.done: 103 return 104 } 105 } 106 } 107 108 func (s *AuthnStore) Len() int { 109 return len(s.sl) 110 } 111 112 func (s *AuthnStore) getSecret() ([]byte, error) { 113 if s.secret != nil { 114 return s.secret, nil 115 } 116 fp := filepath.Join(s.rootDir, "auth_secret.txt") 117 f, err := os.Open(fp) 118 if err != nil { 119 if os.IsNotExist(err) { 120 f, err = os.OpenFile(fp, os.O_CREATE|os.O_WRONLY, 0600) 121 if err != nil { 122 return nil, err 123 } 124 defer f.Close() 125 sec := []byte(random.RandomAlphaNumericString(20)) 126 _, err = f.Write(sec) 127 if err != nil { 128 return nil, err 129 } 130 s.secret = sec 131 return sec, nil 132 } 133 return nil, err 134 } 135 defer f.Close() 136 b, err := io.ReadAll(f) 137 if err != nil { 138 return nil, err 139 } 140 s.secret = b 141 return b, nil 142 } 143 144 func (s *AuthnStore) Authenticate(email, password string) (token string, err error) { 145 i, ok := s.checkPassword(email, password) 146 if !ok { 147 return "", fmt.Errorf("email/password invalid") 148 } 149 sec, err := s.getSecret() 150 if err != nil { 151 return "", err 152 } 153 name := s.sl[i][1] 154 return createIDToken(email, name, sec, s.tokenDuration) 155 } 156 157 func (s *AuthnStore) CheckToken(r *http.Request, token string) (*http.Request, *auth.Claims, error) { 158 sec, err := s.getSecret() 159 if err != nil { 160 return r, nil, err 161 } 162 claims, err := validateIDToken(token, sec) 163 if err != nil { 164 return r, nil, err 165 } 166 return r, claims, nil 167 } 168 169 func (s *AuthnStore) Flush() error { 170 s.mutex.Lock() 171 defer s.mutex.Unlock() 172 fp := s.Filepath() 173 f, err := os.OpenFile(fp, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600) 174 if err != nil { 175 return err 176 } 177 defer f.Close() 178 w := csv.NewWriter(f) 179 if err := w.WriteAll(s.sl); err != nil { 180 return err 181 } 182 w.Flush() 183 if err := w.Error(); err != nil { 184 return err 185 } 186 return f.Close() 187 } 188 189 func (s *AuthnStore) search(email string) int { 190 return sort.Search(s.Len(), func(i int) bool { 191 return s.sl[i][0] >= email 192 }) 193 } 194 195 func (s *AuthnStore) findUserIndex(email string) int { 196 ind := s.search(email) 197 if ind < s.Len() && s.sl[ind][0] == email { 198 return ind 199 } 200 return -1 201 } 202 203 func (s *AuthnStore) SetPassword(email, password string) error { 204 passwordHash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) 205 if err != nil { 206 return err 207 } 208 i := s.search(email) 209 if i < s.Len() && s.sl[i][0] == email { 210 s.sl[i][2] = string(passwordHash) 211 return nil 212 } 213 if i < s.Len() { 214 s.sl = append(s.sl[:i+1], s.sl[i:]...) 215 s.sl[i] = []string{email, "", string(passwordHash)} 216 } else { 217 s.sl = append(s.sl, []string{email, "", string(passwordHash)}) 218 } 219 return nil 220 } 221 222 func (s *AuthnStore) SetName(email, name string) error { 223 i := s.search(email) 224 if i < s.Len() && s.sl[i][0] == email { 225 s.sl[i][1] = name 226 return nil 227 } 228 if i < s.Len() { 229 s.sl = append(s.sl[:i+1], s.sl[i:]...) 230 s.sl[i] = []string{email, name, ""} 231 } else { 232 s.sl = append(s.sl, []string{email, name, ""}) 233 } 234 return nil 235 } 236 237 func (s *AuthnStore) checkPassword(email, password string) (int, bool) { 238 if i := s.findUserIndex(email); i != -1 { 239 return i, bcrypt.CompareHashAndPassword([]byte(s.sl[i][2]), []byte(password)) == nil 240 } 241 return 0, false 242 } 243 244 func (s *AuthnStore) RemoveUser(email string) error { 245 if i := s.findUserIndex(email); i != -1 { 246 s.sl = append(s.sl[:i], s.sl[i+1:]...) 247 return nil 248 } 249 return nil 250 } 251 252 func (s *AuthnStore) ListUsers() (users [][]string, err error) { 253 users = make([][]string, s.Len()) 254 for i, entry := range s.sl { 255 users[i] = []string{entry[0], entry[1]} 256 } 257 return users, nil 258 } 259 260 func (s *AuthnStore) Exist(email string) bool { 261 return s.findUserIndex(email) != -1 262 } 263 264 // InternalState returns internal state as a CSV string for debugging purpose 265 func (s *AuthnStore) InternalState() string { 266 buf := bytes.NewBuffer(nil) 267 w := csv.NewWriter(buf) 268 w.WriteAll(s.sl) 269 w.Flush() 270 return buf.String() 271 } 272 273 func (s *AuthnStore) Close() { 274 close(s.done) 275 s.wg.Wait() 276 close(s.ErrChan) 277 }