code.vegaprotocol.io/vega@v0.79.0/wallet/service/v2/connections/store/longliving/v1/file_store.go (about)

     1  // Copyright (C) 2023 Gobalsky Labs Limited
     2  //
     3  // This program is free software: you can redistribute it and/or modify
     4  // it under the terms of the GNU Affero General Public License as
     5  // published by the Free Software Foundation, either version 3 of the
     6  // License, or (at your option) any later version.
     7  //
     8  // This program is distributed in the hope that it will be useful,
     9  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    10  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    11  // GNU Affero General Public License for more details.
    12  //
    13  // You should have received a copy of the GNU Affero General Public License
    14  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    15  
    16  package v1
    17  
    18  import (
    19  	"context"
    20  	"errors"
    21  	"fmt"
    22  	"os"
    23  	"sync"
    24  	"time"
    25  
    26  	vgfs "code.vegaprotocol.io/vega/libs/fs"
    27  	vgjob "code.vegaprotocol.io/vega/libs/job"
    28  	"code.vegaprotocol.io/vega/paths"
    29  	"code.vegaprotocol.io/vega/wallet/service/v2/connections"
    30  
    31  	"github.com/fsnotify/fsnotify"
    32  )
    33  
    34  var ErrStoreNotInitialized = errors.New("the tokens store has not been initialized")
    35  
    36  type FileStore struct {
    37  	tokensFilePath string
    38  
    39  	passphrase string
    40  
    41  	// jobRunner is used to start and stop the file watcher routines.
    42  	jobRunner *vgjob.Runner
    43  
    44  	// listeners are callback functions to be called when a change occurs on
    45  	// the tokens.
    46  	listeners []func(context.Context, ...connections.TokenDescription)
    47  
    48  	mu sync.Mutex
    49  }
    50  
    51  func (s *FileStore) TokenExists(token connections.Token) (bool, error) {
    52  	s.mu.Lock()
    53  	defer s.mu.Unlock()
    54  
    55  	tokens, err := s.readTokensFile()
    56  	if err != nil {
    57  		return false, err
    58  	}
    59  
    60  	tokenStr := token.String()
    61  	for _, tokenInfo := range tokens.Tokens {
    62  		if tokenInfo.Token == tokenStr {
    63  			return true, nil
    64  		}
    65  	}
    66  	return false, nil
    67  }
    68  
    69  func (s *FileStore) ListTokens() ([]connections.TokenSummary, error) {
    70  	s.mu.Lock()
    71  	defer s.mu.Unlock()
    72  
    73  	tokensFile, err := s.readTokensFile()
    74  	if err != nil {
    75  		return nil, err
    76  	}
    77  
    78  	summaries := make([]connections.TokenSummary, 0, len(tokensFile.Tokens))
    79  
    80  	for _, tokenInfo := range tokensFile.Tokens {
    81  		token, err := connections.AsToken(tokenInfo.Token)
    82  		if err != nil {
    83  			return nil, fmt.Errorf("token %q is not a valid token: %w", token, err)
    84  		}
    85  		summaries = append(summaries, connections.TokenSummary{
    86  			CreationDate:   tokenInfo.CreationDate,
    87  			Description:    tokenInfo.Description,
    88  			Token:          token,
    89  			ExpirationDate: tokenInfo.ExpirationDate,
    90  		})
    91  	}
    92  
    93  	return summaries, nil
    94  }
    95  
    96  func (s *FileStore) DescribeToken(token connections.Token) (connections.TokenDescription, error) {
    97  	s.mu.Lock()
    98  	defer s.mu.Unlock()
    99  
   100  	tokens, err := s.readTokensFile()
   101  	if err != nil {
   102  		return connections.TokenDescription{}, err
   103  	}
   104  
   105  	tokenStr := token.String()
   106  	for _, tokenInfo := range tokens.Tokens {
   107  		if tokenInfo.Token == tokenStr {
   108  			return connections.TokenDescription{
   109  				Description:    tokenInfo.Description,
   110  				ExpirationDate: tokenInfo.ExpirationDate,
   111  				Token:          token,
   112  				Wallet: connections.WalletCredentials{
   113  					Name:       tokenInfo.Wallet,
   114  					Passphrase: tokens.Resources.Wallets[tokenInfo.Wallet],
   115  				},
   116  			}, nil
   117  		}
   118  	}
   119  
   120  	return connections.TokenDescription{}, ErrTokenDoesNotExist
   121  }
   122  
   123  func (s *FileStore) SaveToken(token connections.TokenDescription) error {
   124  	s.mu.Lock()
   125  	defer s.mu.Unlock()
   126  
   127  	tokensFile, err := s.readTokensFile()
   128  	if err != nil {
   129  		return err
   130  	}
   131  
   132  	tokensFile.Resources.Wallets[token.Wallet.Name] = token.Wallet.Passphrase
   133  
   134  	tokensFile.Tokens = append(tokensFile.Tokens, tokenContent{
   135  		Token:          token.Token.String(),
   136  		CreationDate:   token.CreationDate,
   137  		Description:    token.Description,
   138  		Wallet:         token.Wallet.Name,
   139  		ExpirationDate: token.ExpirationDate,
   140  	})
   141  
   142  	return s.writeTokensFile(tokensFile)
   143  }
   144  
   145  func (s *FileStore) DeleteToken(token connections.Token) error {
   146  	s.mu.Lock()
   147  	defer s.mu.Unlock()
   148  
   149  	tokens, err := s.readTokensFile()
   150  	if err != nil {
   151  		return err
   152  	}
   153  
   154  	tokenStr := token.String()
   155  	walletsInUse := map[string]interface{}{}
   156  	tokensContent := make([]tokenContent, 0, len(tokens.Tokens)-1)
   157  	for _, tokenContent := range tokens.Tokens {
   158  		if tokenStr != tokenContent.Token {
   159  			walletsInUse[tokenContent.Wallet] = nil
   160  			tokensContent = append(tokensContent, tokenContent)
   161  		}
   162  	}
   163  	tokens.Tokens = tokensContent
   164  
   165  	wallets := tokens.Resources.Wallets
   166  	for wallet := range wallets {
   167  		if _, ok := walletsInUse[wallet]; !ok {
   168  			delete(tokens.Resources.Wallets, wallet)
   169  		}
   170  	}
   171  
   172  	return s.writeTokensFile(tokens)
   173  }
   174  
   175  func (s *FileStore) OnUpdate(callbackFn func(ctx context.Context, tokens ...connections.TokenDescription)) {
   176  	s.mu.Lock()
   177  	defer s.mu.Unlock()
   178  
   179  	s.listeners = append(s.listeners, callbackFn)
   180  }
   181  
   182  func (s *FileStore) Close() {
   183  	if s.jobRunner != nil {
   184  		s.jobRunner.StopAllJobs()
   185  	}
   186  }
   187  
   188  func (s *FileStore) readTokensFile() (tokens tokensFile, rerr error) {
   189  	defer func() {
   190  		if r := recover(); r != nil {
   191  			tokens, rerr = tokensFile{}, fmt.Errorf("a system error occurred while reading the tokens file: %s", r)
   192  		}
   193  	}()
   194  
   195  	exists, err := vgfs.FileExists(s.tokensFilePath)
   196  	if err != nil {
   197  		return tokensFile{}, fmt.Errorf("could not verify the existence of the tokens file: %w", err)
   198  	} else if !exists {
   199  		return defaultTokensFileContent(), nil
   200  	}
   201  
   202  	if err := paths.ReadEncryptedFile(s.tokensFilePath, s.passphrase, &tokens); err != nil {
   203  		if err.Error() == "couldn't decrypt content: cipher: message authentication failed" {
   204  			return tokensFile{}, ErrWrongPassphrase
   205  		}
   206  		return tokensFile{}, fmt.Errorf("couldn't read the file %s: %w", s.tokensFilePath, err)
   207  	}
   208  
   209  	if tokens.Resources.Wallets == nil {
   210  		tokens.Resources.Wallets = map[string]string{}
   211  	}
   212  
   213  	if tokens.Tokens == nil {
   214  		tokens.Tokens = []tokenContent{}
   215  	}
   216  
   217  	return tokens, nil
   218  }
   219  
   220  func (s *FileStore) writeTokensFile(tokens tokensFile) (rerr error) {
   221  	defer func() {
   222  		if r := recover(); r != nil {
   223  			rerr = fmt.Errorf("a system error occurred while writing the tokens file:: %s", r)
   224  		}
   225  	}()
   226  	if err := paths.WriteEncryptedFile(s.tokensFilePath, s.passphrase, tokens); err != nil {
   227  		return fmt.Errorf("couldn't write the file %s: %w", s.tokensFilePath, err)
   228  	}
   229  
   230  	return nil
   231  }
   232  
   233  func (s *FileStore) wipeOut() error {
   234  	exists, err := vgfs.FileExists(s.tokensFilePath)
   235  	if err != nil {
   236  		return fmt.Errorf("could not verify the existence of the tokens file: %w", err)
   237  	}
   238  
   239  	if exists {
   240  		if err := os.Remove(s.tokensFilePath); err != nil {
   241  			return fmt.Errorf("could not remove the tokens file: %w", err)
   242  		}
   243  	}
   244  
   245  	return nil
   246  }
   247  
   248  func (s *FileStore) initDefault() error {
   249  	return s.writeTokensFile(defaultTokensFileContent())
   250  }
   251  
   252  func (s *FileStore) startFileWatcher() error {
   253  	watcher, err := fsnotify.NewWatcher()
   254  	if err != nil {
   255  		return fmt.Errorf("could not start the token store watcher: %w", err)
   256  	}
   257  
   258  	s.jobRunner = vgjob.NewRunner(context.Background())
   259  
   260  	toBroadcastCh := make(chan []connections.TokenDescription, 10)
   261  
   262  	s.jobRunner.Go(func(ctx context.Context) {
   263  		s.broadcastToListeners(ctx, toBroadcastCh)
   264  	})
   265  
   266  	s.jobRunner.Go(func(ctx context.Context) {
   267  		s.watchFile(ctx, watcher, toBroadcastCh)
   268  	})
   269  
   270  	if err := watcher.Add(s.tokensFilePath); err != nil {
   271  		return fmt.Errorf("could not start watching the token file: %w", err)
   272  	}
   273  
   274  	return nil
   275  }
   276  
   277  func (s *FileStore) broadcastToListeners(ctx context.Context, toBroadcastCh chan []connections.TokenDescription) {
   278  	for {
   279  		select {
   280  		case <-ctx.Done():
   281  			return
   282  		case tokenDescriptions, ok := <-toBroadcastCh:
   283  			if !ok {
   284  				return
   285  			}
   286  			for _, listener := range s.listeners {
   287  				listener(ctx, tokenDescriptions...)
   288  			}
   289  		}
   290  	}
   291  }
   292  
   293  func (s *FileStore) watchFile(ctx context.Context, watcher *fsnotify.Watcher, toBroadcastCh chan []connections.TokenDescription) {
   294  	defer func() {
   295  		_ = watcher.Close()
   296  		close(toBroadcastCh)
   297  	}()
   298  
   299  	for {
   300  		select {
   301  		case <-ctx.Done():
   302  			return
   303  		case event, ok := <-watcher.Events:
   304  			if !ok {
   305  				return
   306  			}
   307  			s.convertFileChangesToEvents(watcher, event, toBroadcastCh)
   308  		case _, ok := <-watcher.Errors:
   309  			if !ok {
   310  				return
   311  			}
   312  			// Something went wrong, but tons of thing can go wrong on a file
   313  			// system, and there is nothing we can do about that. Let's ignore it.
   314  		}
   315  	}
   316  }
   317  
   318  func (s *FileStore) convertFileChangesToEvents(watcher *fsnotify.Watcher, event fsnotify.Event, toBroadcastCh chan<- []connections.TokenDescription) {
   319  	if event.Op == fsnotify.Chmod {
   320  		// If this is solely a CHMOD, we do not trigger an update.
   321  		return
   322  	}
   323  
   324  	s.mu.Lock()
   325  	defer s.mu.Unlock()
   326  
   327  	if event.Has(fsnotify.Remove) {
   328  		_ = watcher.Remove(s.tokensFilePath)
   329  		exists, err := vgfs.FileExists(s.tokensFilePath)
   330  		if err != nil {
   331  			return
   332  		}
   333  		if !exists {
   334  			// The file could have been re-created before we acquire the
   335  			// lock.
   336  			_ = s.initDefault()
   337  		}
   338  		_ = watcher.Add(s.tokensFilePath)
   339  	}
   340  
   341  	tokenDescriptions, err := s.readFileAsTokenDescriptions()
   342  	if err != nil {
   343  		// This can be the result of concurrent modification on the token file.
   344  		// The best thing to do is to carry on and ignore the changes.
   345  		return
   346  	}
   347  
   348  	// The changes are queued, and processed in a separate go routine, so we can
   349  	// release the mutex on the store, while the update is broadcast to the
   350  	// listeners.
   351  	toBroadcastCh <- tokenDescriptions
   352  }
   353  
   354  func (s *FileStore) readFileAsTokenDescriptions() ([]connections.TokenDescription, error) {
   355  	exists, err := vgfs.FileExists(s.tokensFilePath)
   356  	if err != nil {
   357  		return nil, fmt.Errorf("could not verify the token file existence: %w", err)
   358  	}
   359  	if !exists {
   360  		// The file got deleted.
   361  		//
   362  		// This can the result of a "desperate" action to kill all long-living
   363  		// connections, using the `api-token init --force` command.
   364  		//
   365  		// As a result, we return an empty list of tokens, meaning, all tokens
   366  		// should be considered invalid from now on.
   367  		return nil, nil
   368  	}
   369  
   370  	tokensFile, err := s.readTokensFile()
   371  	if err != nil {
   372  		return nil, fmt.Errorf("could not read the token file: %w", err)
   373  	}
   374  
   375  	tokenDescriptions := make([]connections.TokenDescription, 0, len(tokensFile.Tokens))
   376  
   377  	for _, tokenInfo := range tokensFile.Tokens {
   378  		token, err := connections.AsToken(tokenInfo.Token)
   379  		if err != nil {
   380  			// It's all or nothing.
   381  			return nil, fmt.Errorf("the token %q could not be parse: %w", tokenInfo.Token, err)
   382  		}
   383  		tokenDescriptions = append(tokenDescriptions, connections.TokenDescription{
   384  			Description:    tokenInfo.Description,
   385  			CreationDate:   tokenInfo.CreationDate,
   386  			ExpirationDate: tokenInfo.ExpirationDate,
   387  			Token:          token,
   388  			Wallet: connections.WalletCredentials{
   389  				Name:       tokenInfo.Wallet,
   390  				Passphrase: tokensFile.Resources.Wallets[tokenInfo.Wallet],
   391  			},
   392  		})
   393  	}
   394  
   395  	return tokenDescriptions, nil
   396  }
   397  
   398  func InitialiseStore(p paths.Paths, passphrase string) (*FileStore, error) {
   399  	tokensFilePath, err := p.CreateDataPathFor(paths.WalletServiceAPITokensDataFile)
   400  	if err != nil {
   401  		return nil, fmt.Errorf("couldn't get data path for %s: %w", paths.WalletServiceAPITokensDataFile, err)
   402  	}
   403  
   404  	store := &FileStore{
   405  		tokensFilePath: tokensFilePath,
   406  		passphrase:     passphrase,
   407  		listeners:      []func(context.Context, ...connections.TokenDescription){},
   408  	}
   409  
   410  	exists, err := vgfs.FileExists(tokensFilePath)
   411  	if err != nil || !exists {
   412  		return nil, ErrStoreNotInitialized
   413  	}
   414  
   415  	if _, err := store.readTokensFile(); err != nil {
   416  		return nil, err
   417  	}
   418  
   419  	if err := store.startFileWatcher(); err != nil {
   420  		store.Close()
   421  		return nil, err
   422  	}
   423  
   424  	return store, nil
   425  }
   426  
   427  func ReinitialiseStore(p paths.Paths, passphrase string) (*FileStore, error) {
   428  	tokensFilePath, err := tokensFilePath(p)
   429  	if err != nil {
   430  		return nil, err
   431  	}
   432  
   433  	store := &FileStore{
   434  		tokensFilePath: tokensFilePath,
   435  		passphrase:     passphrase,
   436  		listeners:      []func(context.Context, ...connections.TokenDescription){},
   437  	}
   438  
   439  	if err := store.wipeOut(); err != nil {
   440  		return nil, err
   441  	}
   442  
   443  	if err := store.initDefault(); err != nil {
   444  		return nil, err
   445  	}
   446  
   447  	if err := store.startFileWatcher(); err != nil {
   448  		store.Close()
   449  		return nil, err
   450  	}
   451  
   452  	return store, nil
   453  }
   454  
   455  func IsStoreBootstrapped(p paths.Paths) (bool, error) {
   456  	tokensFilePath, err := tokensFilePath(p)
   457  	if err != nil {
   458  		return false, err
   459  	}
   460  
   461  	exists, err := vgfs.FileExists(tokensFilePath)
   462  
   463  	return err == nil && exists, nil
   464  }
   465  
   466  func tokensFilePath(p paths.Paths) (string, error) {
   467  	tokensFilePath, err := p.CreateDataPathFor(paths.WalletServiceAPITokensDataFile)
   468  	if err != nil {
   469  		return "", fmt.Errorf("couldn't get data path for %s: %w", paths.WalletServiceAPITokensDataFile, err)
   470  	}
   471  	return tokensFilePath, nil
   472  }
   473  
   474  type tokensFile struct {
   475  	FileVersion   int              `json:"fileVersion"`
   476  	TokensVersion int              `json:"tokensVersion"`
   477  	Resources     resourcesContent `json:"resources"`
   478  	Tokens        []tokenContent   `json:"tokens"`
   479  }
   480  
   481  func defaultTokensFileContent() tokensFile {
   482  	return tokensFile{
   483  		FileVersion:   1,
   484  		TokensVersion: 1,
   485  		Resources: resourcesContent{
   486  			Wallets: map[string]string{},
   487  		},
   488  		Tokens: []tokenContent{},
   489  	}
   490  }
   491  
   492  type resourcesContent struct {
   493  	Wallets map[string]string `json:"wallets"`
   494  }
   495  
   496  type tokenContent struct {
   497  	Token          string     `json:"token"`
   498  	CreationDate   time.Time  `json:"creationDate"`
   499  	ExpirationDate *time.Time `json:"expirationDate"`
   500  	Description    string     `json:"description"`
   501  	Wallet         string     `json:"wallet"`
   502  }