From e30def175b5bdf525e6dbcc28c4ebcb5e457ac00 Mon Sep 17 00:00:00 2001
From: Pascal Fischer <pascal@netbird.io>
Date: Mon, 20 Mar 2023 16:14:55 +0100
Subject: [PATCH] switch PATs to map and add deletion

---
 management/server/account.go          |  2 +
 management/server/account_test.go     | 25 ++++++------
 management/server/file_store.go       | 18 ++++++++
 management/server/file_store_test.go  | 26 ++++++++++--
 management/server/store.go            |  2 +
 management/server/testdata/store.json |  9 ++--
 management/server/user.go             | 47 ++++++++++++++++++---
 management/server/user_test.go        | 59 ++++++++++++++++++++++++++-
 8 files changed, 162 insertions(+), 26 deletions(-)

diff --git a/management/server/account.go b/management/server/account.go
index d3f7df028..e998b8d5c 100644
--- a/management/server/account.go
+++ b/management/server/account.go
@@ -66,6 +66,8 @@ type AccountManager interface {
 	GetNetworkMap(peerID string) (*NetworkMap, error)
 	GetPeerNetwork(peerID string) (*Network, error)
 	AddPeer(setupKey, userID string, peer *Peer) (*Peer, *NetworkMap, error)
+	AddPATToUser(accountID string, userID string, pat *PersonalAccessToken) error
+	DeletePAT(accountID string, userID string, tokenID string) error
 	UpdatePeerSSHKey(peerID string, sshKey string) error
 	GetUsersFromAccount(accountID, userID string) ([]*UserInfo, error)
 	GetGroup(accountId, groupID string) (*Group, error)
diff --git a/management/server/account_test.go b/management/server/account_test.go
index fa932023a..d211973c2 100644
--- a/management/server/account_test.go
+++ b/management/server/account_test.go
@@ -473,20 +473,21 @@ func TestAccountManager_GetAccountFromPAT(t *testing.T) {
 
 	token := "nbp_9999EUDNdkeusjentDLSJEn1902u84390W6W"
 	hashedToken := sha256.Sum256([]byte(token))
-	pat := PersonalAccessToken{
-		ID:             "tokenId",
-		Description:    "some Description",
-		HashedToken:    string(hashedToken[:]),
-		ExpirationDate: time.Time{},
-		CreatedBy:      "testuser",
-		CreatedAt:      time.Time{},
-		LastUsed:       time.Time{},
-	}
 	account.Users["someUser"] = &User{
 		Id:         "someUser",
 		Role:       "",
 		AutoGroups: nil,
-		PATs:       []PersonalAccessToken{pat},
+		PATs: map[string]*PersonalAccessToken{
+			"pat1": {
+				ID:             "tokenId",
+				Description:    "some Description",
+				HashedToken:    string(hashedToken[:]),
+				ExpirationDate: time.Time{},
+				CreatedBy:      "testuser",
+				CreatedAt:      time.Time{},
+				LastUsed:       time.Time{},
+			},
+		},
 	}
 	err := store.SaveAccount(account)
 	if err != nil {
@@ -1267,8 +1268,8 @@ func TestAccount_Copy(t *testing.T) {
 				Id:         "user1",
 				Role:       UserRoleAdmin,
 				AutoGroups: []string{"group1"},
-				PATs: []PersonalAccessToken{
-					{
+				PATs: map[string]*PersonalAccessToken{
+					"pat1": {
 						ID:             "pat1",
 						Description:    "First PAT",
 						HashedToken:    "SoMeHaShEdToKeN",
diff --git a/management/server/file_store.go b/management/server/file_store.go
index 50bfec1d6..cd98ca6b8 100644
--- a/management/server/file_store.go
+++ b/management/server/file_store.go
@@ -288,6 +288,24 @@ func (s *FileStore) SaveAccount(account *Account) error {
 	return s.persist(s.storeFile)
 }
 
+func (s *FileStore) DeleteHashedPAT2TokenIDIndex(hashedToken string) error {
+	s.mux.Lock()
+	defer s.mux.Unlock()
+
+	delete(s.HashedPAT2TokenID, hashedToken)
+
+	return s.persist(s.storeFile)
+}
+
+func (s *FileStore) DeleteTokenID2UserIDIndex(tokenID string) error {
+	s.mux.Lock()
+	defer s.mux.Unlock()
+
+	delete(s.TokenID2UserID, tokenID)
+
+	return s.persist(s.storeFile)
+}
+
 // GetAccountByPrivateDomain returns account by private domain
 func (s *FileStore) GetAccountByPrivateDomain(domain string) (*Account, error) {
 	s.mux.Lock()
diff --git a/management/server/file_store_test.go b/management/server/file_store_test.go
index 838af50fb..c2ccbf546 100644
--- a/management/server/file_store_test.go
+++ b/management/server/file_store_test.go
@@ -249,7 +249,7 @@ func TestRestore(t *testing.T) {
 
 	require.NotNil(t, account.SetupKeys["A2C8E62B-38F5-4553-B31E-DD66C696CEBB"], "failed to restore a FileStore file - missing Account SetupKey A2C8E62B-38F5-4553-B31E-DD66C696CEBB")
 
-	require.Len(t, account.Users["f4f6d672-63fb-11ec-90d6-0242ac120003"].PATs, 1, "failed to restore a FileStore wrong PATs length")
+	require.NotNil(t, account.Users["f4f6d672-63fb-11ec-90d6-0242ac120003"].PATs["9dj38s35-63fb-11ec-90d6-0242ac120003"], "failed to restore a FileStore wrong PATs length")
 
 	require.Len(t, store.UserID2AccountID, 2, "failed to restore a FileStore wrong UserID2AccountID mapping length")
 
@@ -383,16 +383,34 @@ func TestFileStore_GetTokenIDByHashedToken(t *testing.T) {
 		t.Fatal(err)
 	}
 
-	hashedToken := accounts.Accounts["bf1c8084-ba50-4ce7-9439-34653001fc3b"].Users["f4f6d672-63fb-11ec-90d6-0242ac120003"].PATs[0].HashedToken
+	hashedToken := accounts.Accounts["bf1c8084-ba50-4ce7-9439-34653001fc3b"].Users["f4f6d672-63fb-11ec-90d6-0242ac120003"].PATs["9dj38s35-63fb-11ec-90d6-0242ac120003"].HashedToken
 	tokenID, err := store.GetTokenIDByHashedToken(hashedToken)
 	if err != nil {
 		t.Fatal(err)
 	}
 
-	expectedTokenID := accounts.Accounts["bf1c8084-ba50-4ce7-9439-34653001fc3b"].Users["f4f6d672-63fb-11ec-90d6-0242ac120003"].PATs[0].ID
+	expectedTokenID := accounts.Accounts["bf1c8084-ba50-4ce7-9439-34653001fc3b"].Users["f4f6d672-63fb-11ec-90d6-0242ac120003"].PATs["9dj38s35-63fb-11ec-90d6-0242ac120003"].ID
 	assert.Equal(t, expectedTokenID, tokenID)
 }
 
+func TestFileStore_DeleteHashedPAT2TokenIDIndex(t *testing.T) {
+	store := newStore(t)
+	store.HashedPAT2TokenID["someHashedToken"] = "someTokenId"
+
+	store.DeleteHashedPAT2TokenIDIndex("someHashedToken")
+
+	assert.Empty(t, store.HashedPAT2TokenID["someHashedToken"])
+}
+
+func TestFileStore_DeleteTokenID2UserIDIndex(t *testing.T) {
+	store := newStore(t)
+	store.TokenID2UserID["someTokenId"] = "someUserId"
+
+	store.DeleteTokenID2UserIDIndex("someTokenId")
+
+	assert.Empty(t, store.TokenID2UserID["someTokenId"])
+}
+
 func TestFileStore_GetTokenIDByHashedToken_Failure(t *testing.T) {
 	storeDir := t.TempDir()
 	storeFile := filepath.Join(storeDir, "store.json")
@@ -437,7 +455,7 @@ func TestFileStore_GetUserByTokenID(t *testing.T) {
 		t.Fatal(err)
 	}
 
-	tokenID := accounts.Accounts["bf1c8084-ba50-4ce7-9439-34653001fc3b"].Users["f4f6d672-63fb-11ec-90d6-0242ac120003"].PATs[0].ID
+	tokenID := accounts.Accounts["bf1c8084-ba50-4ce7-9439-34653001fc3b"].Users["f4f6d672-63fb-11ec-90d6-0242ac120003"].PATs["9dj38s35-63fb-11ec-90d6-0242ac120003"].ID
 	user, err := store.GetUserByTokenID(tokenID)
 	if err != nil {
 		t.Fatal(err)
diff --git a/management/server/store.go b/management/server/store.go
index 3577a9f59..daad30eaa 100644
--- a/management/server/store.go
+++ b/management/server/store.go
@@ -11,6 +11,8 @@ type Store interface {
 	GetTokenIDByHashedToken(secret string) (string, error)
 	GetUserByTokenID(tokenID string) (*User, error)
 	SaveAccount(account *Account) error
+	DeleteHashedPAT2TokenIDIndex(hashedToken string) error
+	DeleteTokenID2UserIDIndex(tokenID string) error
 	GetInstallationID() string
 	SaveInstallationID(ID string) error
 	// AcquireAccountLock should attempt to acquire account lock and return a function that releases the lock
diff --git a/management/server/testdata/store.json b/management/server/testdata/store.json
index 7eed7f4c0..ecde766c3 100644
--- a/management/server/testdata/store.json
+++ b/management/server/testdata/store.json
@@ -29,13 +29,14 @@
             "Users": {
                 "edafee4e-63fb-11ec-90d6-0242ac120003": {
                     "Id": "edafee4e-63fb-11ec-90d6-0242ac120003",
-                    "Role": "admin"
+                    "Role": "admin",
+                    "PATs": {}
                 },
                 "f4f6d672-63fb-11ec-90d6-0242ac120003": {
                     "Id": "f4f6d672-63fb-11ec-90d6-0242ac120003",
                     "Role": "user",
-                    "PATs":[
-                        {
+                    "PATs": {
+                        "9dj38s35-63fb-11ec-90d6-0242ac120003": {
                             "ID":"9dj38s35-63fb-11ec-90d6-0242ac120003",
                             "Description":"some Description",
                             "HashedToken":"SoMeHaShEdToKeN",
@@ -44,7 +45,7 @@
                             "CreatedAt":"2023-01-01T00:00:00Z",
                             "LastUsed":"2023-02-01T00:00:00Z"
                         }
-                    ]
+                    }
                 }
             }
         }
diff --git a/management/server/user.go b/management/server/user.go
index fcebe006d..8047c624c 100644
--- a/management/server/user.go
+++ b/management/server/user.go
@@ -46,7 +46,7 @@ type User struct {
 	Role UserRole
 	// AutoGroups is a list of Group IDs to auto-assign to peers registered by this user
 	AutoGroups []string
-	PATs       []PersonalAccessToken
+	PATs       map[string]*PersonalAccessToken
 }
 
 // IsAdmin returns true if user is an admin, false otherwise
@@ -94,8 +94,12 @@ func (u *User) toUserInfo(userData *idp.UserData) (*UserInfo, error) {
 func (u *User) Copy() *User {
 	autoGroups := make([]string, len(u.AutoGroups))
 	copy(autoGroups, u.AutoGroups)
-	pats := make([]PersonalAccessToken, len(u.PATs))
-	copy(pats, u.PATs)
+	pats := make(map[string]*PersonalAccessToken, len(u.PATs))
+	for k, v := range u.PATs {
+		patCopy := new(PersonalAccessToken)
+		*patCopy = *v
+		pats[k] = patCopy
+	}
 	return &User{
 		Id:         u.Id,
 		Role:       u.Role,
@@ -190,7 +194,7 @@ func (am *DefaultAccountManager) CreateUser(accountID, userID string, invite *Us
 }
 
 // AddPATToUser takes the userID and the accountID the user belongs to and assigns a provided PersonalAccessToken to that user
-func (am *DefaultAccountManager) AddPATToUser(accountID string, userID string, pat PersonalAccessToken) error {
+func (am *DefaultAccountManager) AddPATToUser(accountID string, userID string, pat *PersonalAccessToken) error {
 	unlock := am.Store.AcquireAccountLock(accountID)
 	defer unlock()
 
@@ -204,7 +208,40 @@ func (am *DefaultAccountManager) AddPATToUser(accountID string, userID string, p
 		return status.Errorf(status.NotFound, "user not found")
 	}
 
-	user.PATs = append(user.PATs, pat)
+	user.PATs[pat.ID] = pat
+
+	err = am.Store.SaveAccount(account)
+	return err
+}
+
+func (am *DefaultAccountManager) DeletePAT(accountID string, userID string, tokenID string) error {
+	unlock := am.Store.AcquireAccountLock(accountID)
+	defer unlock()
+
+	account, err := am.Store.GetAccount(accountID)
+	if err != nil {
+		return err
+	}
+
+	user := account.Users[userID]
+	if user == nil {
+		return status.Errorf(status.NotFound, "user not found")
+	}
+
+	pat := user.PATs["tokenID"]
+	if pat == nil {
+		return status.Errorf(status.NotFound, "PAT not found")
+	}
+
+	err = am.Store.DeleteTokenID2UserIDIndex(pat.ID)
+	if err != nil {
+		return err
+	}
+	err = am.Store.DeleteHashedPAT2TokenIDIndex(pat.HashedToken)
+	if err != nil {
+		return err
+	}
+	delete(user.PATs, tokenID)
 
 	err = am.Store.SaveAccount(account)
 	return err
diff --git a/management/server/user_test.go b/management/server/user_test.go
index 1c81827ac..b3cd1db9a 100644
--- a/management/server/user_test.go
+++ b/management/server/user_test.go
@@ -51,7 +51,7 @@ func TestUser_AddPATToUser(t *testing.T) {
 		LastUsed:       time.Time{},
 	}
 
-	err = am.AddPATToUser("account_id", "testuser", pat)
+	err = am.AddPATToUser("account_id", "testuser", &pat)
 	if err != nil {
 		t.Fatalf("Error when adding PAT to user: %s", err)
 	}
@@ -71,3 +71,60 @@ func TestUser_AddPATToUser(t *testing.T) {
 	}
 	assert.Equal(t, "testuser", userID)
 }
+
+func TestUser_DeletePAT(t *testing.T) {
+	store := newStore(t)
+	account := newAccountWithId("account_id", "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()},
+	}
+	account.Users["user1"] = &User{
+		Id:         "user1",
+		Role:       "admin",
+		AutoGroups: nil,
+		PATs: map[string]*PersonalAccessToken{
+			"tokenID": {
+				ID:             "tokenID",
+				Description:    "some Description",
+				HashedToken:    "SoMeHaShEdToKeN",
+				ExpirationDate: time.Now().AddDate(0, 0, 7),
+				CreatedBy:      "user1",
+				CreatedAt:      time.Now(),
+				LastUsed:       time.Now(),
+			},
+		},
+	}
+	err := store.SaveAccount(account)
+	if err != nil {
+		t.Fatalf("Error when saving account: %s", err)
+	}
+
+	am := DefaultAccountManager{
+		Store:                   store,
+		cacheMux:                sync.Mutex{},
+		cacheLoading:            nil,
+		peersUpdateManager:      nil,
+		idpManager:              nil,
+		cacheManager:            nil,
+		ctx:                     nil,
+		eventStore:              nil,
+		singleAccountMode:       false,
+		singleAccountModeDomain: "",
+		dnsDomain:               "",
+		peerLoginExpiry:         nil,
+	}
+
+	err = am.DeletePAT("account_id", "user1", "tokenID")
+	if err != nil {
+		t.Fatalf("Error when adding PAT to user: %s", err)
+	}
+
+	assert.Nil(t, store.Accounts["account_id"].Users["user1"].PATs["tokenID"])
+	assert.Empty(t, store.HashedPAT2TokenID["SoMeHaShEdToKeN"])
+	assert.Empty(t, store.TokenID2UserID["tokenID"])
+}