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 }