From 6ae27c9a9b8dc099558701cf95352ae02bbfa401 Mon Sep 17 00:00:00 2001 From: Mikhail Bragin Date: Mon, 27 Dec 2021 13:17:15 +0100 Subject: [PATCH] Refactor: support multiple users under the same account (#170) * feature: add User entity to Account * test: new file store creation test * test: add FileStore persist-restore tests * test: add GetOrCreateAccountByUser Accountmanager test * refactor: rename account manager users file * refactor: use userId instead of accountId when handling Management HTTP API * fix: new account creation for every request * fix: golint * chore: add account creator to Account Entity to identify who created the account. * chore: use xid ID generator for account IDs * fix: test failures * test: check that CreatedBy is stored when account is stored * chore: add account copy method * test: remove test for non existent GetOrCreateAccount func * chore: add accounts conversion function * fix: golint * refactor: simplify admin user creation * refactor: move migration script to a separate package --- go.mod | 2 + go.sum | 2 + management/server/account.go | 78 ++++---- management/server/account_test.go | 79 ++++---- management/server/file_store.go | 22 +++ management/server/file_store_test.go | 171 ++++++++++++++++++ management/server/http/handler/peers.go | 20 +- management/server/http/handler/setupkeys.go | 34 ++-- management/server/http/handler/util.go | 4 +- management/server/migration/README.md | 13 ++ .../server/migration/convert_accounts.go | 56 ++++++ .../server/migration/convert_accounts_test.go | 76 ++++++++ management/server/network.go | 8 + management/server/peer.go | 5 +- management/server/store.go | 1 + management/server/testdata/store.json | 12 +- management/server/testdata/storev1.json | 154 ++++++++++++++++ management/server/user.go | 71 ++++++++ 18 files changed, 700 insertions(+), 108 deletions(-) create mode 100644 management/server/file_store_test.go create mode 100644 management/server/migration/README.md create mode 100644 management/server/migration/convert_accounts.go create mode 100644 management/server/migration/convert_accounts_test.go create mode 100644 management/server/testdata/storev1.json create mode 100644 management/server/user.go diff --git a/go.mod b/go.mod index e7dab3632..c81e1a2d1 100644 --- a/go.mod +++ b/go.mod @@ -27,6 +27,8 @@ require ( gopkg.in/natefinch/lumberjack.v2 v2.0.0 ) +require github.com/rs/xid v1.3.0 + require ( github.com/BurntSushi/toml v0.4.1 // indirect github.com/fsnotify/fsnotify v1.5.1 // indirect diff --git a/go.sum b/go.sum index 3e424b265..3301286f9 100644 --- a/go.sum +++ b/go.sum @@ -398,6 +398,8 @@ github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6L github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rs/cors v1.8.0 h1:P2KMzcFwrPoSjkF1WLRPsp3UMLyql8L4v9hQpVeK5so= github.com/rs/cors v1.8.0/go.mod h1:EBwu+T5AvHOcXwvZIkQFjUN6s8Czyqw12GL/Y0tUyRM= +github.com/rs/xid v1.3.0 h1:6NjYksEUlhurdVehpc7S7dk6DAmcKv8V9gG0FsVN2U4= +github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig= diff --git a/management/server/account.go b/management/server/account.go index 9f8e10e6c..29a589478 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -2,6 +2,7 @@ package server import ( "github.com/google/uuid" + "github.com/rs/xid" log "github.com/sirupsen/logrus" "github.com/wiretrustee/wiretrustee/util" "google.golang.org/grpc/codes" @@ -19,10 +20,39 @@ type AccountManager struct { // Account represents a unique account of the system type Account struct { - Id string + Id string + // User.Id it was created by + CreatedBy string SetupKeys map[string]*SetupKey Network *Network Peers map[string]*Peer + Users map[string]*User +} + +func (a *Account) Copy() *Account { + peers := map[string]*Peer{} + for id, peer := range a.Peers { + peers[id] = peer.Copy() + } + + users := map[string]*User{} + for id, user := range a.Users { + users[id] = user.Copy() + } + + setupKeys := map[string]*SetupKey{} + for id, key := range a.SetupKeys { + setupKeys[id] = key.Copy() + } + + return &Account{ + Id: a.Id, + CreatedBy: a.CreatedBy, + SetupKeys: setupKeys, + Network: a.Network.Copy(), + Peers: peers, + Users: users, + } } // NewManager creates a new AccountManager with a provided Store @@ -125,29 +155,6 @@ func (am *AccountManager) GetAccount(accountId string) (*Account, error) { return account, nil } -// GetOrCreateAccount returns an existing account or creates a new one if doesn't exist -func (am *AccountManager) GetOrCreateAccount(accountId string) (*Account, error) { - am.mux.Lock() - defer am.mux.Unlock() - - _, err := am.Store.GetAccount(accountId) - if err != nil { - if s, ok := status.FromError(err); ok && s.Code() == codes.NotFound { - return am.createAccount(accountId) - } else { - // other error - return nil, err - } - } - - account, err := am.Store.GetAccount(accountId) - if err != nil { - return nil, status.Errorf(codes.Internal, "failed retrieving account") - } - - return account, nil -} - //AccountExists checks whether account exists (returns true) or not (returns false) func (am *AccountManager) AccountExists(accountId string) (*bool, error) { am.mux.Lock() @@ -168,18 +175,18 @@ func (am *AccountManager) AccountExists(accountId string) (*bool, error) { return &res, nil } -// AddAccount generates a new Account with a provided accountId and saves to the Store -func (am *AccountManager) AddAccount(accountId string) (*Account, error) { +// AddAccount generates a new Account with a provided accountId and userId, saves to the Store +func (am *AccountManager) AddAccount(accountId string, userId string) (*Account, error) { am.mux.Lock() defer am.mux.Unlock() - return am.createAccount(accountId) + return am.createAccount(accountId, userId) } -func (am *AccountManager) createAccount(accountId string) (*Account, error) { - account, _ := newAccountWithId(accountId) +func (am *AccountManager) createAccount(accountId string, userId string) (*Account, error) { + account, _ := newAccountWithId(accountId, userId) err := am.Store.SaveAccount(account) if err != nil { @@ -190,7 +197,7 @@ func (am *AccountManager) createAccount(accountId string) (*Account, error) { } // newAccountWithId creates a new Account with a default SetupKey (doesn't store in a Store) and provided id -func newAccountWithId(accountId string) (*Account, *SetupKey) { +func newAccountWithId(accountId string, userId string) (*Account, *SetupKey) { log.Debugf("creating new account") @@ -204,16 +211,17 @@ func newAccountWithId(accountId string) (*Account, *SetupKey) { Net: net.IPNet{IP: net.ParseIP("100.64.0.0"), Mask: net.IPMask{255, 192, 0, 0}}, Dns: ""} peers := make(map[string]*Peer) + users := make(map[string]*User) log.Debugf("created new account %s with setup key %s", accountId, defaultKey.Key) - return &Account{Id: accountId, SetupKeys: setupKeys, Network: network, Peers: peers}, defaultKey + return &Account{Id: accountId, SetupKeys: setupKeys, Network: network, Peers: peers, Users: users, CreatedBy: userId}, defaultKey } -// newAccount creates a new Account with a default SetupKey (doesn't store in a Store) -func newAccount() (*Account, *SetupKey) { - accountId := uuid.New().String() - return newAccountWithId(accountId) +// newAccount creates a new Account with a default SetupKey and a provided User.Id of a user who issued account creation (doesn't store in a Store) +func newAccount(userId string) (*Account, *SetupKey) { + accountId := xid.New().String() + return newAccountWithId(accountId, userId) } func getAccountSetupKeyById(acc *Account, keyId string) *SetupKey { diff --git a/management/server/account_test.go b/management/server/account_test.go index 5eb755848..832d2474f 100644 --- a/management/server/account_test.go +++ b/management/server/account_test.go @@ -2,12 +2,36 @@ package server import ( "golang.zx2c4.com/wireguard/wgctrl/wgtypes" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" "net" "testing" ) +func TestAccountManager_GetOrCreateAccountByUser(t *testing.T) { + manager, err := createManager(t) + if err != nil { + t.Fatal(err) + return + } + + userId := "test_user" + account, err := manager.GetOrCreateAccountByUser(userId) + if err != nil { + t.Fatal(err) + } + if account == nil { + t.Fatalf("expected to create an account for a user %s", userId) + } + + account, err = manager.GetAccountByUser(userId) + if err != nil { + t.Errorf("expected to get existing account after creation, no account was found for a user %s", userId) + } + + if account != nil && account.Users[userId] == nil { + t.Fatalf("expected to create an account for a user %s but no user was found after creation udner the account %s", userId, account.Id) + } +} + func TestAccountManager_AddAccount(t *testing.T) { manager, err := createManager(t) if err != nil { @@ -16,6 +40,7 @@ func TestAccountManager_AddAccount(t *testing.T) { } expectedId := "test_account" + userId := "account_creator" expectedPeersSize := 0 expectedSetupKeysSize := 2 expectedNetwork := net.IPNet{ @@ -23,7 +48,7 @@ func TestAccountManager_AddAccount(t *testing.T) { Mask: net.IPMask{255, 192, 0, 0}, } - account, err := manager.AddAccount(expectedId) + account, err := manager.AddAccount(expectedId, userId) if err != nil { t.Fatal(err) } @@ -45,46 +70,6 @@ func TestAccountManager_AddAccount(t *testing.T) { } } -func TestAccountManager_GetOrCreateAccount(t *testing.T) { - manager, err := createManager(t) - if err != nil { - t.Fatal(err) - return - } - - expectedId := "test_account" - - //make sure account doesn't exist - account, err := manager.GetAccount(expectedId) - if err != nil { - errStatus, ok := status.FromError(err) - if !(ok && errStatus.Code() == codes.NotFound) { - t.Fatal(err) - } - } - if account != nil { - t.Fatal("expecting empty account") - } - - account, err = manager.GetOrCreateAccount(expectedId) - if err != nil { - t.Fatal(err) - } - - if account.Id != expectedId { - t.Fatalf("expected to create an account, got wrong account") - } - - account, err = manager.GetOrCreateAccount(expectedId) - if err != nil { - t.Errorf("expected to get existing account after creation, failed") - } - - if account.Id != expectedId { - t.Fatalf("expected to create an account, got wrong account") - } -} - func TestAccountManager_AccountExists(t *testing.T) { manager, err := createManager(t) if err != nil { @@ -93,7 +78,8 @@ func TestAccountManager_AccountExists(t *testing.T) { } expectedId := "test_account" - _, err = manager.AddAccount(expectedId) + userId := "account_creator" + _, err = manager.AddAccount(expectedId, userId) if err != nil { t.Fatal(err) } @@ -117,7 +103,8 @@ func TestAccountManager_GetAccount(t *testing.T) { } expectedId := "test_account" - account, err := manager.AddAccount(expectedId) + userId := "account_creator" + account, err := manager.AddAccount(expectedId, userId) if err != nil { t.Fatal(err) } @@ -154,7 +141,7 @@ func TestAccountManager_AddPeer(t *testing.T) { return } - account, err := manager.AddAccount("test_account") + account, err := manager.AddAccount("test_account", "account_creator") if err != nil { t.Fatal(err) } diff --git a/management/server/file_store.go b/management/server/file_store.go index 2b693c82b..5fc8b9e54 100644 --- a/management/server/file_store.go +++ b/management/server/file_store.go @@ -20,6 +20,7 @@ type FileStore struct { Accounts map[string]*Account SetupKeyId2AccountId map[string]string `json:"-"` PeerKeyId2AccountId map[string]string `json:"-"` + UserId2AccountId map[string]string `json:"-"` // mutex to synchronise Store read/write operations mux sync.Mutex `json:"-"` @@ -45,6 +46,7 @@ func restore(file string) (*FileStore, error) { mux: sync.Mutex{}, SetupKeyId2AccountId: make(map[string]string), PeerKeyId2AccountId: make(map[string]string), + UserId2AccountId: make(map[string]string), storeFile: file, } @@ -65,6 +67,7 @@ func restore(file string) (*FileStore, error) { store.storeFile = file store.SetupKeyId2AccountId = make(map[string]string) store.PeerKeyId2AccountId = make(map[string]string) + store.UserId2AccountId = make(map[string]string) for accountId, account := range store.Accounts { for setupKeyId := range account.SetupKeys { store.SetupKeyId2AccountId[strings.ToUpper(setupKeyId)] = accountId @@ -72,6 +75,9 @@ func restore(file string) (*FileStore, error) { for _, peer := range account.Peers { store.PeerKeyId2AccountId[peer.Key] = accountId } + for _, user := range account.Users { + store.UserId2AccountId[user.Id] = accountId + } } return store, nil @@ -168,6 +174,10 @@ func (s *FileStore) SaveAccount(account *Account) error { s.PeerKeyId2AccountId[peer.Key] = account.Id } + for _, user := range account.Users { + s.UserId2AccountId[user.Id] = account.Id + } + err := s.persist(s.storeFile) if err != nil { return err @@ -217,6 +227,18 @@ func (s *FileStore) GetAccount(accountId string) (*Account, error) { return account, nil } +func (s *FileStore) GetUserAccount(userId string) (*Account, error) { + s.mux.Lock() + defer s.mux.Unlock() + + accountId, accountIdFound := s.UserId2AccountId[userId] + if !accountIdFound { + return nil, status.Errorf(codes.NotFound, "account not found") + } + + return s.GetAccount(accountId) +} + func (s *FileStore) GetPeerAccount(peerKey string) (*Account, error) { s.mux.Lock() defer s.mux.Unlock() diff --git a/management/server/file_store_test.go b/management/server/file_store_test.go new file mode 100644 index 000000000..5b23be40d --- /dev/null +++ b/management/server/file_store_test.go @@ -0,0 +1,171 @@ +package server + +import ( + "github.com/wiretrustee/wiretrustee/util" + "net" + "path/filepath" + "testing" + "time" +) + +func TestNewStore(t *testing.T) { + store := newStore(t) + + if store.Accounts == nil || len(store.Accounts) != 0 { + t.Errorf("expected to create a new empty Accounts map when creating a new FileStore") + } + + if store.SetupKeyId2AccountId == nil || len(store.SetupKeyId2AccountId) != 0 { + t.Errorf("expected to create a new empty SetupKeyId2AccountId map when creating a new FileStore") + } + + if store.PeerKeyId2AccountId == nil || len(store.PeerKeyId2AccountId) != 0 { + t.Errorf("expected to create a new empty PeerKeyId2AccountId map when creating a new FileStore") + } + + if store.UserId2AccountId == nil || len(store.UserId2AccountId) != 0 { + t.Errorf("expected to create a new empty UserId2AccountId map when creating a new FileStore") + } + +} + +func TestSaveAccount(t *testing.T) { + store := newStore(t) + + account, _ := newAccount("testuser") + account.Users["testuser"] = NewAdminUser("testuser") + setupKey := GenerateDefaultSetupKey() + account.SetupKeys[setupKey.Key] = setupKey + account.Peers["testpeer"] = &Peer{ + Key: "peerkey", + SetupKey: "peerkeysetupkey", + IP: net.IP{127, 0, 0, 1}, + Meta: PeerSystemMeta{}, + Name: "peer name", + Status: &PeerStatus{Connected: true, LastSeen: time.Now()}, + } + + // SaveAccount should trigger persist + err := store.SaveAccount(account) + if err != nil { + return + } + + if store.Accounts[account.Id] == nil { + t.Errorf("expecting Account to be stored after SaveAccount()") + } + + if store.PeerKeyId2AccountId["peerkey"] == "" { + t.Errorf("expecting PeerKeyId2AccountId index updated after SaveAccount()") + } + + if store.UserId2AccountId["testuser"] == "" { + t.Errorf("expecting UserId2AccountId index updated after SaveAccount()") + } + + if store.SetupKeyId2AccountId[setupKey.Key] == "" { + t.Errorf("expecting SetupKeyId2AccountId index updated after SaveAccount()") + } + +} + +func TestStore(t *testing.T) { + store := newStore(t) + + account, _ := newAccount("testuser") + account.Users["testuser"] = NewAdminUser("testuser") + account.Peers["testpeer"] = &Peer{ + Key: "peerkey", + SetupKey: "peerkeysetupkey", + IP: net.IP{127, 0, 0, 1}, + Meta: PeerSystemMeta{}, + Name: "peer name", + Status: &PeerStatus{Connected: true, LastSeen: time.Now()}, + } + + // SaveAccount should trigger persist + err := store.SaveAccount(account) + if err != nil { + return + } + + restored, err := NewStore(store.storeFile) + if err != nil { + return + } + + restoredAccount := restored.Accounts[account.Id] + if restoredAccount == nil { + t.Errorf("failed to restore a FileStore file - missing Account %s", account.Id) + } + + if restoredAccount != nil && restoredAccount.Peers["testpeer"] == nil { + t.Errorf("failed to restore a FileStore file - missing Peer testpeer") + } + + if restoredAccount != nil && restoredAccount.CreatedBy != "testuser" { + t.Errorf("failed to restore a FileStore file - missing Account CreatedBy") + } + + if restoredAccount != nil && restoredAccount.Users["testuser"] == nil { + t.Errorf("failed to restore a FileStore file - missing User testuser") + } + + if restoredAccount != nil && restoredAccount.Network == nil { + t.Errorf("failed to restore a FileStore file - missing Network") + } + +} + +func TestRestore(t *testing.T) { + storeDir := t.TempDir() + + err := util.CopyFileContents("testdata/store.json", filepath.Join(storeDir, "store.json")) + if err != nil { + t.Fatal(err) + } + + store, err := NewStore(storeDir) + if err != nil { + return + } + + account := store.Accounts["bf1c8084-ba50-4ce7-9439-34653001fc3b"] + if account == nil { + t.Errorf("failed to restore a FileStore file - missing account bf1c8084-ba50-4ce7-9439-34653001fc3b") + } + + if account != nil && account.Users["edafee4e-63fb-11ec-90d6-0242ac120003"] == nil { + t.Errorf("failed to restore a FileStore file - missing Account User edafee4e-63fb-11ec-90d6-0242ac120003") + } + + if account != nil && account.Users["f4f6d672-63fb-11ec-90d6-0242ac120003"] == nil { + t.Errorf("failed to restore a FileStore file - missing Account User f4f6d672-63fb-11ec-90d6-0242ac120003") + } + + if account != nil && account.Network == nil { + t.Errorf("failed to restore a FileStore file - missing Account Network") + } + + if account != nil && account.SetupKeys["A2C8E62B-38F5-4553-B31E-DD66C696CEBB"] == nil { + t.Errorf("failed to restore a FileStore file - missing Account SetupKey A2C8E62B-38F5-4553-B31E-DD66C696CEBB") + } + + if len(store.UserId2AccountId) != 2 { + t.Errorf("failed to restore a FileStore wrong UserId2AccountId mapping") + } + + if len(store.SetupKeyId2AccountId) != 1 { + t.Errorf("failed to restore a FileStore wrong SetupKeyId2AccountId mapping") + } + +} + +func newStore(t *testing.T) *FileStore { + store, err := NewStore(t.TempDir()) + if err != nil { + t.Errorf("failed creating a new store") + } + + return store +} diff --git a/management/server/http/handler/peers.go b/management/server/http/handler/peers.go index 8b27adb09..40af4d227 100644 --- a/management/server/http/handler/peers.go +++ b/management/server/http/handler/peers.go @@ -62,7 +62,13 @@ func (h *Peers) deletePeer(accountId string, peer *server.Peer, w http.ResponseW } func (h *Peers) HandlePeer(w http.ResponseWriter, r *http.Request) { - accountId := extractAccountIdFromRequestContext(r) + userId := extractUserIdFromRequestContext(r) + account, err := h.accountManager.GetOrCreateAccountByUser(userId) + if err != nil { + log.Errorf("failed getting account of a user %s: %v", userId, err) + http.Redirect(w, r, "/", http.StatusInternalServerError) + return + } vars := mux.Vars(r) peerId := vars["id"] //effectively peer IP address if len(peerId) == 0 { @@ -70,7 +76,7 @@ func (h *Peers) HandlePeer(w http.ResponseWriter, r *http.Request) { return } - peer, err := h.accountManager.GetPeerByIP(accountId, peerId) + peer, err := h.accountManager.GetPeerByIP(account.Id, peerId) if err != nil { http.Error(w, "peer not found", http.StatusNotFound) return @@ -78,10 +84,10 @@ func (h *Peers) HandlePeer(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodDelete: - h.deletePeer(accountId, peer, w, r) + h.deletePeer(account.Id, peer, w, r) return case http.MethodPut: - h.updatePeer(accountId, peer, w, r) + h.updatePeer(account.Id, peer, w, r) return case http.MethodGet: writeJSONObject(w, toPeerResponse(peer)) @@ -96,11 +102,11 @@ func (h *Peers) HandlePeer(w http.ResponseWriter, r *http.Request) { func (h *Peers) GetPeers(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: - accountId := extractAccountIdFromRequestContext(r) + userId := extractUserIdFromRequestContext(r) //new user -> create a new account - account, err := h.accountManager.GetOrCreateAccount(accountId) + account, err := h.accountManager.GetOrCreateAccountByUser(userId) if err != nil { - log.Errorf("failed getting user account %s: %v", accountId, err) + log.Errorf("failed getting account of a user %s: %v", userId, err) http.Redirect(w, r, "/", http.StatusInternalServerError) return } diff --git a/management/server/http/handler/setupkeys.go b/management/server/http/handler/setupkeys.go index bbd8bbe9a..1877a627b 100644 --- a/management/server/http/handler/setupkeys.go +++ b/management/server/http/handler/setupkeys.go @@ -118,7 +118,14 @@ func (h *SetupKeys) createKey(accountId string, w http.ResponseWriter, r *http.R } func (h *SetupKeys) HandleKey(w http.ResponseWriter, r *http.Request) { - accountId := extractAccountIdFromRequestContext(r) + userId := extractUserIdFromRequestContext(r) + account, err := h.accountManager.GetOrCreateAccountByUser(userId) + if err != nil { + log.Errorf("failed getting account of a user %s: %v", userId, err) + http.Redirect(w, r, "/", http.StatusInternalServerError) + return + } + vars := mux.Vars(r) keyId := vars["id"] if len(keyId) == 0 { @@ -128,10 +135,10 @@ func (h *SetupKeys) HandleKey(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodPut: - h.updateKey(accountId, keyId, w, r) + h.updateKey(account.Id, keyId, w, r) return case http.MethodGet: - h.getKey(accountId, keyId, w, r) + h.getKey(account.Id, keyId, w, r) return default: http.Error(w, "", http.StatusNotFound) @@ -140,21 +147,20 @@ func (h *SetupKeys) HandleKey(w http.ResponseWriter, r *http.Request) { func (h *SetupKeys) GetKeys(w http.ResponseWriter, r *http.Request) { - accountId := extractAccountIdFromRequestContext(r) + userId := extractUserIdFromRequestContext(r) + //new user -> create a new account + account, err := h.accountManager.GetOrCreateAccountByUser(userId) + if err != nil { + log.Errorf("failed getting account of a user %s: %v", userId, err) + http.Redirect(w, r, "/", http.StatusInternalServerError) + return + } switch r.Method { case http.MethodPost: - h.createKey(accountId, w, r) + h.createKey(account.Id, w, r) return case http.MethodGet: - - //new user -> create a new account - account, err := h.accountManager.GetOrCreateAccount(accountId) - if err != nil { - log.Errorf("failed getting user account %s: %v", accountId, err) - http.Redirect(w, r, "/", http.StatusInternalServerError) - return - } w.WriteHeader(200) w.Header().Set("Content-Type", "application/json") @@ -165,7 +171,7 @@ func (h *SetupKeys) GetKeys(w http.ResponseWriter, r *http.Request) { err = json.NewEncoder(w).Encode(respBody) if err != nil { - log.Errorf("failed encoding account peers %s: %v", accountId, err) + log.Errorf("failed encoding account peers %s: %v", account.Id, err) http.Redirect(w, r, "/", http.StatusInternalServerError) return } diff --git a/management/server/http/handler/util.go b/management/server/http/handler/util.go index b36667706..431beda33 100644 --- a/management/server/http/handler/util.go +++ b/management/server/http/handler/util.go @@ -8,8 +8,8 @@ import ( "time" ) -// extractAccountIdFromRequestContext extracts accountId from the request context previously filled by the JWT token (after auth) -func extractAccountIdFromRequestContext(r *http.Request) string { +// extractUserIdFromRequestContext extracts accountId from the request context previously filled by the JWT token (after auth) +func extractUserIdFromRequestContext(r *http.Request) string { token := r.Context().Value("user").(*jwt.Token) claims := token.Claims.(jwt.MapClaims) diff --git a/management/server/migration/README.md b/management/server/migration/README.md new file mode 100644 index 000000000..aafe96858 --- /dev/null +++ b/management/server/migration/README.md @@ -0,0 +1,13 @@ +## Migration from Store v2 to Store v2 + +Previously Account.Id was an Auth0 user id. +Conversion moves user id to Account.CreatedBy and generates a new Account.Id using xid. +It also adds a User with id = old Account.Id with a role Admin. + +To start a conversion simply run the command below providing your current Wiretrustee Management datadir (where store.json file is located) +and a new data directory location (where a converted store.js will be stored): +```shell + ./migration --oldDir /var/wiretrustee/datadir --newDir /var/wiretrustee/newdatadir/ +``` + +Afterwards you can run the Management service providing ```/var/wiretrustee/newdatadir/ ``` as a datadir. \ No newline at end of file diff --git a/management/server/migration/convert_accounts.go b/management/server/migration/convert_accounts.go new file mode 100644 index 000000000..b912ee06d --- /dev/null +++ b/management/server/migration/convert_accounts.go @@ -0,0 +1,56 @@ +package main + +import ( + "flag" + "fmt" + "github.com/rs/xid" + "github.com/wiretrustee/wiretrustee/management/server" +) + +func main() { + + oldDir := flag.String("oldDir", "old store directory", "/var/wiretrustee/datadir") + newDir := flag.String("newDir", "new store directory", "/var/wiretrustee/newdatadir") + + flag.Parse() + + oldStore, err := server.NewStore(*oldDir) + if err != nil { + panic(err) + } + + newStore, err := server.NewStore(*newDir) + if err != nil { + panic(err) + } + + err = Convert(oldStore, newStore) + if err != nil { + panic(err) + } + + fmt.Println("successfully converted") +} + +// Convert converts old store ato a new store +// Previously Account.Id was an Auth0 user id +// Conversion moved user id to Account.CreatedBy and generated a new Account.Id using xid +// It also adds a User with id = old Account.Id with a role Admin +func Convert(oldStore *server.FileStore, newStore *server.FileStore) error { + for _, account := range oldStore.Accounts { + accountCopy := account.Copy() + accountCopy.Id = xid.New().String() + accountCopy.CreatedBy = account.Id + accountCopy.Users[account.Id] = &server.User{ + Id: account.Id, + Role: server.UserRoleAdmin, + } + + err := newStore.SaveAccount(accountCopy) + if err != nil { + return err + } + } + + return nil +} diff --git a/management/server/migration/convert_accounts_test.go b/management/server/migration/convert_accounts_test.go new file mode 100644 index 000000000..082a6bdd3 --- /dev/null +++ b/management/server/migration/convert_accounts_test.go @@ -0,0 +1,76 @@ +package main + +import ( + "github.com/wiretrustee/wiretrustee/management/server" + "github.com/wiretrustee/wiretrustee/util" + "path/filepath" + "testing" +) + +func TestConvertAccounts(t *testing.T) { + + storeDir := t.TempDir() + + err := util.CopyFileContents("../testdata/storev1.json", filepath.Join(storeDir, "store.json")) + if err != nil { + t.Fatal(err) + } + + store, err := server.NewStore(storeDir) + if err != nil { + t.Fatal(err) + } + + convertedStore, err := server.NewStore(filepath.Join(storeDir, "converted")) + if err != nil { + t.Fatal(err) + } + + err = Convert(store, convertedStore) + if err != nil { + t.Fatal(err) + } + + if len(store.Accounts) != len(convertedStore.Accounts) { + t.Errorf("expecting the same number of accounts after conversion") + } + + for _, account := range store.Accounts { + convertedAccount, err := convertedStore.GetUserAccount(account.Id) + if err != nil || convertedAccount == nil { + t.Errorf("expecting Account %s to be converted", account.Id) + return + } + if convertedAccount.CreatedBy != account.Id { + t.Errorf("expecting converted Account.CreatedBy field to be equal to the old Account.Id") + return + } + if convertedAccount.Id == account.Id { + t.Errorf("expecting converted Account.Id to be different from Account.Id") + return + } + if len(convertedAccount.Users) != 1 { + t.Errorf("expecting converted Account.Users to be of size 1") + return + } + user := convertedAccount.Users[account.Id] + if user == nil { + t.Errorf("expecting to find a user in converted Account.Users") + return + } + if user.Role != server.UserRoleAdmin { + t.Errorf("expecting to find a user in converted Account.Users with a role Admin") + return + } + + for peerId := range account.Peers { + convertedPeer := convertedAccount.Peers[peerId] + if convertedPeer == nil { + t.Errorf("expecting Account Peer of StoreV1 to be found in StoreV2") + return + } + } + + } + +} diff --git a/management/server/network.go b/management/server/network.go index b24dc8556..d08fa94a6 100644 --- a/management/server/network.go +++ b/management/server/network.go @@ -17,6 +17,14 @@ type Network struct { Dns string } +func (n *Network) Copy() *Network { + return &Network{ + Id: n.Id, + Net: n.Net, + Dns: n.Dns, + } +} + // AllocatePeerIP pics an available IP from an net.IPNet. // This method considers already taken IPs and reuses IPs if there are gaps in takenIps // E.g. if ipNet=100.30.0.0/16 and takenIps=[100.30.0.1, 100.30.0.5] then the result would be 100.30.0.2 diff --git a/management/server/peer.go b/management/server/peer.go index f7750c165..b5007e7f6 100644 --- a/management/server/peer.go +++ b/management/server/peer.go @@ -206,7 +206,6 @@ func (am *AccountManager) GetPeersForAPeer(peerKey string) ([]*Peer, error) { // Each Account has a list of pre-authorised SetupKey and if no Account has a given key err wit ha code codes.Unauthenticated // will be returned, meaning the key is invalid // Each new Peer will be assigned a new next net.IP from the Account.Network and Account.Network.LastIP will be updated (IP's are not reused). -// If the specified setupKey is empty then a new Account will be created //todo remove this part // The peer property is just a placeholder for the Peer properties to pass further func (am *AccountManager) AddPeer(setupKey string, peer Peer) (*Peer, error) { am.mux.Lock() @@ -218,8 +217,8 @@ func (am *AccountManager) AddPeer(setupKey string, peer Peer) (*Peer, error) { var err error var sk *SetupKey if len(upperKey) == 0 { - // Empty setup key, create a new account for it. - account, sk = newAccount() + // Empty setup key, fail + return nil, status.Errorf(codes.InvalidArgument, "empty setupKey %s", setupKey) } else { account, err = am.Store.GetAccountBySetupKey(upperKey) if err != nil { diff --git a/management/server/store.go b/management/server/store.go index e292a0f43..5581de9d2 100644 --- a/management/server/store.go +++ b/management/server/store.go @@ -5,6 +5,7 @@ type Store interface { DeletePeer(accountId string, peerKey string) (*Peer, error) SavePeer(accountId string, peer *Peer) error GetAccount(accountId string) (*Account, error) + GetUserAccount(userId string) (*Account, error) GetAccountPeers(accountId string) ([]*Peer, error) GetPeerAccount(peerKey string) (*Account, error) GetAccountBySetupKey(setupKey string) (*Account, error) diff --git a/management/server/testdata/store.json b/management/server/testdata/store.json index c567aaf4b..d2c4743b0 100644 --- a/management/server/testdata/store.json +++ b/management/server/testdata/store.json @@ -22,7 +22,17 @@ }, "Dns": null }, - "Peers": {} + "Peers": {}, + "Users": { + "edafee4e-63fb-11ec-90d6-0242ac120003": { + "Id": "edafee4e-63fb-11ec-90d6-0242ac120003", + "Role": "admin" + }, + "f4f6d672-63fb-11ec-90d6-0242ac120003": { + "Id": "f4f6d672-63fb-11ec-90d6-0242ac120003", + "Role": "user" + } + } } } } \ No newline at end of file diff --git a/management/server/testdata/storev1.json b/management/server/testdata/storev1.json new file mode 100644 index 000000000..674b2b87a --- /dev/null +++ b/management/server/testdata/storev1.json @@ -0,0 +1,154 @@ +{ + "Accounts": { + "auth0|61bf82ddeab084006aa1bccd": { + "Id": "auth0|61bf82ddeab084006aa1bccd", + "SetupKeys": { + "1B2B50B0-B3E8-4B0C-A426-525EDB8481BD": { + "Id": "831727121", + "Key": "1B2B50B0-B3E8-4B0C-A426-525EDB8481BD", + "Name": "One-off key", + "Type": "one-off", + "CreatedAt": "2021-12-24T16:09:45.926075752+01:00", + "ExpiresAt": "2022-01-23T16:09:45.926075752+01:00", + "Revoked": false, + "UsedTimes": 1, + "LastUsed": "2021-12-24T16:12:45.763424077+01:00" + }, + "EB51E9EB-A11F-4F6E-8E49-C982891B405A": { + "Id": "1769568301", + "Key": "EB51E9EB-A11F-4F6E-8E49-C982891B405A", + "Name": "Default key", + "Type": "reusable", + "CreatedAt": "2021-12-24T16:09:45.926073628+01:00", + "ExpiresAt": "2022-01-23T16:09:45.926073628+01:00", + "Revoked": false, + "UsedTimes": 1, + "LastUsed": "2021-12-24T16:13:06.236748538+01:00" + } + }, + "Network": { + "Id": "a443c07a-5765-4a78-97fc-390d9c1d0e49", + "Net": { + "IP": "100.64.0.0", + "Mask": "/8AAAA==" + }, + "Dns": "" + }, + "Peers": { + "oMNaI8qWi0CyclSuwGR++SurxJyM3pQEiPEHwX8IREo=": { + "Key": "oMNaI8qWi0CyclSuwGR++SurxJyM3pQEiPEHwX8IREo=", + "SetupKey": "EB51E9EB-A11F-4F6E-8E49-C982891B405A", + "IP": "100.64.0.2", + "Meta": { + "Hostname": "braginini", + "GoOS": "linux", + "Kernel": "Linux", + "Core": "21.04", + "Platform": "x86_64", + "OS": "Ubuntu", + "WtVersion": "" + }, + "Name": "braginini", + "Status": { + "LastSeen": "2021-12-24T16:13:11.244342541+01:00", + "Connected": false + } + }, + "xlx9/9D8+ibnRiIIB8nHGMxGOzxV17r8ShPHgi4aYSM=": { + "Key": "xlx9/9D8+ibnRiIIB8nHGMxGOzxV17r8ShPHgi4aYSM=", + "SetupKey": "1B2B50B0-B3E8-4B0C-A426-525EDB8481BD", + "IP": "100.64.0.1", + "Meta": { + "Hostname": "braginini", + "GoOS": "linux", + "Kernel": "Linux", + "Core": "21.04", + "Platform": "x86_64", + "OS": "Ubuntu", + "WtVersion": "" + }, + "Name": "braginini", + "Status": { + "LastSeen": "2021-12-24T16:12:49.089339333+01:00", + "Connected": false + } + } + } + }, + "google-oauth2|103201118415301331038": { + "Id": "google-oauth2|103201118415301331038", + "SetupKeys": { + "5AFB60DB-61F2-4251-8E11-494847EE88E9": { + "Id": "2485964613", + "Key": "5AFB60DB-61F2-4251-8E11-494847EE88E9", + "Name": "Default key", + "Type": "reusable", + "CreatedAt": "2021-12-24T16:10:02.238476+01:00", + "ExpiresAt": "2022-01-23T16:10:02.238476+01:00", + "Revoked": false, + "UsedTimes": 1, + "LastUsed": "2021-12-24T16:12:05.994307717+01:00" + }, + "A72E4DC2-00DE-4542-8A24-62945438104E": { + "Id": "3504804807", + "Key": "A72E4DC2-00DE-4542-8A24-62945438104E", + "Name": "One-off key", + "Type": "one-off", + "CreatedAt": "2021-12-24T16:10:02.238478209+01:00", + "ExpiresAt": "2022-01-23T16:10:02.238478209+01:00", + "Revoked": false, + "UsedTimes": 1, + "LastUsed": "2021-12-24T16:11:27.015741738+01:00" + } + }, + "Network": { + "Id": "b6d0b152-364e-40c1-a8a1-fa7bcac2267f", + "Net": { + "IP": "100.64.0.0", + "Mask": "/8AAAA==" + }, + "Dns": "" + }, + "Peers": { + "6kjbmVq1hmucVzvBXo5OucY5OYv+jSsB1jUTLq291Dw=": { + "Key": "6kjbmVq1hmucVzvBXo5OucY5OYv+jSsB1jUTLq291Dw=", + "SetupKey": "5AFB60DB-61F2-4251-8E11-494847EE88E9", + "IP": "100.64.0.2", + "Meta": { + "Hostname": "braginini", + "GoOS": "linux", + "Kernel": "Linux", + "Core": "21.04", + "Platform": "x86_64", + "OS": "Ubuntu", + "WtVersion": "" + }, + "Name": "braginini", + "Status": { + "LastSeen": "2021-12-24T16:12:05.994305438+01:00", + "Connected": false + } + }, + "Ok+5QMdt/UjoktNOvicGYj+IX2g98p+0N2PJ3vJ45RI=": { + "Key": "Ok+5QMdt/UjoktNOvicGYj+IX2g98p+0N2PJ3vJ45RI=", + "SetupKey": "A72E4DC2-00DE-4542-8A24-62945438104E", + "IP": "100.64.0.1", + "Meta": { + "Hostname": "braginini", + "GoOS": "linux", + "Kernel": "Linux", + "Core": "21.04", + "Platform": "x86_64", + "OS": "Ubuntu", + "WtVersion": "" + }, + "Name": "braginini", + "Status": { + "LastSeen": "2021-12-24T16:11:27.015739803+01:00", + "Connected": false + } + } + } + } + } +} \ No newline at end of file diff --git a/management/server/user.go b/management/server/user.go new file mode 100644 index 000000000..26b2b5cb6 --- /dev/null +++ b/management/server/user.go @@ -0,0 +1,71 @@ +package server + +import ( + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +const ( + UserRoleAdmin UserRole = "admin" + UserRoleUser UserRole = "user" +) + +// UserRole is the role of the User +type UserRole string + +// User represents a user of the system +type User struct { + Id string + Role UserRole +} + +func (u *User) Copy() *User { + return &User{ + Id: u.Id, + Role: u.Role, + } +} + +// NewUser creates a new user +func NewUser(id string, role UserRole) *User { + return &User{ + Id: id, + Role: role, + } +} + +// NewAdminUser creates a new user with role UserRoleAdmin +func NewAdminUser(id string) *User { + return NewUser(id, UserRoleAdmin) +} + +// GetOrCreateAccountByUser returns an existing account for a given user id or creates a new one if doesn't exist +func (am *AccountManager) GetOrCreateAccountByUser(userId string) (*Account, error) { + am.mux.Lock() + defer am.mux.Unlock() + + account, err := am.Store.GetUserAccount(userId) + if err != nil { + if s, ok := status.FromError(err); ok && s.Code() == codes.NotFound { + account, _ = newAccount(userId) + account.Users[userId] = NewAdminUser(userId) + err = am.Store.SaveAccount(account) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed creating account") + } + } else { + // other error + return nil, err + } + } + + return account, nil +} + +// GetAccountByUser returns an existing account for a given user id, NotFound if account couldn't be found +func (am *AccountManager) GetAccountByUser(userId string) (*Account, error) { + am.mux.Lock() + defer am.mux.Unlock() + + return am.Store.GetUserAccount(userId) +}