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  }