github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/model/move/store.go (about)

     1  package move
     2  
     3  import (
     4  	"context"
     5  	"encoding/hex"
     6  	"encoding/json"
     7  	"errors"
     8  	"sync"
     9  	"time"
    10  
    11  	"github.com/cozy/cozy-stack/pkg/config/config"
    12  	"github.com/cozy/cozy-stack/pkg/crypto"
    13  	"github.com/cozy/cozy-stack/pkg/prefixer"
    14  	"github.com/redis/go-redis/v9"
    15  )
    16  
    17  // Store is essentially an object to store and retrieve move requests
    18  type Store interface {
    19  	GetRequest(db prefixer.Prefixer, secret string) (*Request, error)
    20  	SaveRequest(db prefixer.Prefixer, req *Request) (string, error)
    21  	SetAllowDeleteAccounts(db prefixer.Prefixer) error
    22  	ClearAllowDeleteAccounts(db prefixer.Prefixer) error
    23  	AllowDeleteAccounts(db prefixer.Prefixer) bool
    24  }
    25  
    26  // storeTTL is the time an entry stay alive
    27  var storeTTL = 5 * time.Minute
    28  
    29  // storeCleanInterval is the time interval between each cleanup.
    30  var storeCleanInterval = 1 * time.Hour
    31  
    32  var mu sync.Mutex
    33  var globalStore Store
    34  
    35  // GetStore returns the store for temporary move objects.
    36  func GetStore() Store {
    37  	mu.Lock()
    38  	defer mu.Unlock()
    39  	if globalStore != nil {
    40  		return globalStore
    41  	}
    42  	cli := config.GetConfig().SessionStorage
    43  	if cli == nil {
    44  		globalStore = newMemStore()
    45  	} else {
    46  		ctx := context.Background()
    47  		globalStore = &redisStore{cli, ctx}
    48  	}
    49  	return globalStore
    50  }
    51  
    52  type memRef struct {
    53  	val *Request
    54  	exp time.Time
    55  }
    56  
    57  func newMemStore() Store {
    58  	store := &memStore{vals: make(map[string]*memRef)}
    59  	go store.cleaner()
    60  	return store
    61  }
    62  
    63  type memStore struct {
    64  	mu   sync.Mutex
    65  	vals map[string]*memRef
    66  }
    67  
    68  func (s *memStore) cleaner() {
    69  	for range time.Tick(storeCleanInterval) {
    70  		now := time.Now()
    71  		for k, v := range s.vals {
    72  			if now.After(v.exp) {
    73  				delete(s.vals, k)
    74  			}
    75  		}
    76  	}
    77  }
    78  
    79  func (s *memStore) GetRequest(db prefixer.Prefixer, secret string) (*Request, error) {
    80  	s.mu.Lock()
    81  	defer s.mu.Unlock()
    82  	key := requestKey(db, secret)
    83  	ref, ok := s.vals[key]
    84  	if !ok {
    85  		return nil, nil
    86  	}
    87  	if time.Now().After(ref.exp) {
    88  		delete(s.vals, key)
    89  		return nil, nil
    90  	}
    91  	return ref.val, nil
    92  }
    93  
    94  func (s *memStore) SaveRequest(db prefixer.Prefixer, req *Request) (string, error) {
    95  	secret := makeSecret()
    96  	s.mu.Lock()
    97  	defer s.mu.Unlock()
    98  	key := requestKey(db, secret)
    99  	s.vals[key] = &memRef{
   100  		val: req,
   101  		exp: time.Now().Add(storeTTL),
   102  	}
   103  	return secret, nil
   104  }
   105  
   106  func (s *memStore) SetAllowDeleteAccounts(db prefixer.Prefixer) error {
   107  	s.mu.Lock()
   108  	defer s.mu.Unlock()
   109  	key := deleteAccountsKey(db)
   110  	s.vals[key] = &memRef{
   111  		val: nil,
   112  		exp: time.Now().Add(storeTTL),
   113  	}
   114  	return nil
   115  }
   116  
   117  func (s *memStore) ClearAllowDeleteAccounts(db prefixer.Prefixer) error {
   118  	s.mu.Lock()
   119  	defer s.mu.Unlock()
   120  	key := deleteAccountsKey(db)
   121  	delete(s.vals, key)
   122  	return nil
   123  }
   124  
   125  func (s *memStore) AllowDeleteAccounts(db prefixer.Prefixer) bool {
   126  	s.mu.Lock()
   127  	defer s.mu.Unlock()
   128  	key := deleteAccountsKey(db)
   129  	ref, ok := s.vals[key]
   130  	if !ok {
   131  		return false
   132  	}
   133  	if time.Now().After(ref.exp) {
   134  		delete(s.vals, key)
   135  		return false
   136  	}
   137  	return true
   138  }
   139  
   140  type redisStore struct {
   141  	c   redis.UniversalClient
   142  	ctx context.Context
   143  }
   144  
   145  func (s *redisStore) GetRequest(db prefixer.Prefixer, secret string) (*Request, error) {
   146  	key := requestKey(db, secret)
   147  	b, err := s.c.Get(s.ctx, key).Bytes()
   148  	if errors.Is(err, redis.Nil) {
   149  		return nil, nil
   150  	}
   151  	if err != nil {
   152  		return nil, err
   153  	}
   154  	var req Request
   155  	if err = json.Unmarshal(b, &req); err != nil {
   156  		return nil, err
   157  	}
   158  	return &req, nil
   159  }
   160  
   161  func (s *redisStore) SaveRequest(db prefixer.Prefixer, req *Request) (string, error) {
   162  	v, err := json.Marshal(req)
   163  	if err != nil {
   164  		return "", err
   165  	}
   166  	secret := makeSecret()
   167  	key := requestKey(db, secret)
   168  	if err = s.c.Set(s.ctx, key, v, storeTTL).Err(); err != nil {
   169  		return "", err
   170  	}
   171  	return secret, nil
   172  }
   173  
   174  func (s *redisStore) SetAllowDeleteAccounts(db prefixer.Prefixer) error {
   175  	key := deleteAccountsKey(db)
   176  	return s.c.Set(s.ctx, key, true, storeTTL).Err()
   177  }
   178  
   179  func (s *redisStore) ClearAllowDeleteAccounts(db prefixer.Prefixer) error {
   180  	key := deleteAccountsKey(db)
   181  	return s.c.Del(s.ctx, key).Err()
   182  }
   183  
   184  func (s *redisStore) AllowDeleteAccounts(db prefixer.Prefixer) bool {
   185  	key := deleteAccountsKey(db)
   186  	r, err := s.c.Exists(s.ctx, key).Result()
   187  	if err != nil {
   188  		return false
   189  	}
   190  	return r > 0
   191  }
   192  
   193  func requestKey(db prefixer.Prefixer, suffix string) string {
   194  	return db.DBPrefix() + ":req:" + suffix
   195  }
   196  
   197  func deleteAccountsKey(db prefixer.Prefixer) string {
   198  	return db.DBPrefix() + ":allow_delete_accounts"
   199  }
   200  
   201  func makeSecret() string {
   202  	return hex.EncodeToString(crypto.GenerateRandomBytes(8))
   203  }