From c69df13515b49e570241900b0682a88be8b36621 Mon Sep 17 00:00:00 2001 From: Misha Bragin Date: Wed, 23 Apr 2025 18:44:22 +0200 Subject: [PATCH] [management] Add account meta (#3724) --- management/server/account.go | 13 ++++++++++ management/server/account/manager.go | 1 + management/server/http/api/openapi.yml | 21 +++++++++++++++ management/server/http/api/types.gen.go | 12 +++++++++ .../handlers/accounts/accounts_handler.go | 26 +++++++++++++++---- .../accounts/accounts_handler_test.go | 6 +++++ management/server/mock_server/account_mock.go | 9 +++++++ management/server/store/sql_store.go | 15 +++++++++++ management/server/store/sql_store_test.go | 16 ++++++++++++ management/server/store/store.go | 1 + management/server/testdata/extended-store.sql | 2 +- management/server/types/account.go | 21 +++++++++++++++ 12 files changed, 137 insertions(+), 6 deletions(-) diff --git a/management/server/account.go b/management/server/account.go index d7f108dfe..fb0a9b65e 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -1057,6 +1057,19 @@ func (am *DefaultAccountManager) GetAccountByID(ctx context.Context, accountID s return am.Store.GetAccount(ctx, accountID) } +// GetAccountMeta returns the account metadata associated with this account ID. +func (am *DefaultAccountManager) GetAccountMeta(ctx context.Context, accountID string, userID string) (*types.AccountMeta, error) { + allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Accounts, operations.Read) + if err != nil { + return nil, status.NewPermissionValidationError(err) + } + if !allowed { + return nil, status.NewPermissionDeniedError() + } + + return am.Store.GetAccountMeta(ctx, store.LockingStrengthShare, accountID) +} + func (am *DefaultAccountManager) GetAccountIDFromUserAuth(ctx context.Context, userAuth nbcontext.UserAuth) (string, string, error) { if userAuth.UserId == "" { return "", "", errors.New(emptyUserID) diff --git a/management/server/account/manager.go b/management/server/account/manager.go index ea664d10e..b6eb7de05 100644 --- a/management/server/account/manager.go +++ b/management/server/account/manager.go @@ -37,6 +37,7 @@ type Manager interface { SaveOrAddUsers(ctx context.Context, accountID, initiatorUserID string, updates []*types.User, addIfNotExists bool) ([]*types.UserInfo, error) GetSetupKey(ctx context.Context, accountID, userID, keyID string) (*types.SetupKey, error) GetAccountByID(ctx context.Context, accountID string, userID string) (*types.Account, error) + GetAccountMeta(ctx context.Context, accountID string, userID string) (*types.AccountMeta, error) AccountExists(ctx context.Context, accountID string) (bool, error) GetAccountIDByUserID(ctx context.Context, userID, domain string) (string, error) GetAccountIDFromUserAuth(ctx context.Context, userAuth nbcontext.UserAuth) (string, string, error) diff --git a/management/server/http/api/openapi.yml b/management/server/http/api/openapi.yml index c699e9eef..1717c89ac 100644 --- a/management/server/http/api/openapi.yml +++ b/management/server/http/api/openapi.yml @@ -43,9 +43,30 @@ components: example: ch8i4ug6lnn4g9hqv7l0 settings: $ref: '#/components/schemas/AccountSettings' + domain: + description: Account domain + type: string + example: netbird.io + domain_category: + description: Account domain category + type: string + example: private + created_at: + description: Account creation date (UTC) + type: string + format: date-time + example: "2023-05-05T09:00:35.477782Z" + created_by: + description: Account creator + type: string + example: google-oauth2|277474792786460067937 required: - id - settings + - domain + - domain_category + - created_at + - created_by AccountSettings: type: object properties: diff --git a/management/server/http/api/types.gen.go b/management/server/http/api/types.gen.go index 9bdb3e4ac..3fca40366 100644 --- a/management/server/http/api/types.gen.go +++ b/management/server/http/api/types.gen.go @@ -223,6 +223,18 @@ type AccessiblePeer struct { // Account defines model for Account. type Account struct { + // CreatedAt Account creation date (UTC) + CreatedAt time.Time `json:"created_at"` + + // CreatedBy Account creator + CreatedBy string `json:"created_by"` + + // Domain Account domain + Domain string `json:"domain"` + + // DomainCategory Account domain category + DomainCategory string `json:"domain_category"` + // Id Account ID Id string `json:"id"` Settings AccountSettings `json:"settings"` diff --git a/management/server/http/handlers/accounts/accounts_handler.go b/management/server/http/handlers/accounts/accounts_handler.go index 6c8f8028a..c0851102f 100644 --- a/management/server/http/handlers/accounts/accounts_handler.go +++ b/management/server/http/handlers/accounts/accounts_handler.go @@ -47,13 +47,19 @@ func (h *handler) getAllAccounts(w http.ResponseWriter, r *http.Request) { accountID, userID := userAuth.AccountId, userAuth.UserId + meta, err := h.accountManager.GetAccountMeta(r.Context(), accountID, userID) + if err != nil { + util.WriteError(r.Context(), err, w) + return + } + settings, err := h.settingsManager.GetSettings(r.Context(), accountID, userID) if err != nil { util.WriteError(r.Context(), err, w) return } - resp := toAccountResponse(accountID, settings) + resp := toAccountResponse(accountID, settings, meta) util.WriteJSONObject(r.Context(), w, []*api.Account{resp}) } @@ -120,7 +126,13 @@ func (h *handler) updateAccount(w http.ResponseWriter, r *http.Request) { return } - resp := toAccountResponse(updatedAccount.Id, updatedAccount.Settings) + meta, err := h.accountManager.GetAccountMeta(r.Context(), accountID, userID) + if err != nil { + util.WriteError(r.Context(), err, w) + return + } + + resp := toAccountResponse(updatedAccount.Id, updatedAccount.Settings, meta) util.WriteJSONObject(r.Context(), w, &resp) } @@ -149,7 +161,7 @@ func (h *handler) deleteAccount(w http.ResponseWriter, r *http.Request) { util.WriteJSONObject(r.Context(), w, util.EmptyObject{}) } -func toAccountResponse(accountID string, settings *types.Settings) *api.Account { +func toAccountResponse(accountID string, settings *types.Settings, meta *types.AccountMeta) *api.Account { jwtAllowGroups := settings.JWTAllowGroups if jwtAllowGroups == nil { jwtAllowGroups = []string{} @@ -177,7 +189,11 @@ func toAccountResponse(accountID string, settings *types.Settings) *api.Account } return &api.Account{ - Id: accountID, - Settings: apiSettings, + Id: accountID, + Settings: apiSettings, + CreatedAt: meta.CreatedAt, + CreatedBy: meta.CreatedBy, + Domain: meta.Domain, + DomainCategory: meta.DomainCategory, } } diff --git a/management/server/http/handlers/accounts/accounts_handler_test.go b/management/server/http/handlers/accounts/accounts_handler_test.go index e971a6514..2acca4f49 100644 --- a/management/server/http/handlers/accounts/accounts_handler_test.go +++ b/management/server/http/handlers/accounts/accounts_handler_test.go @@ -50,6 +50,12 @@ func initAccountsTestData(t *testing.T, account *types.Account) *handler { accCopy.UpdateSettings(newSettings) return accCopy, nil }, + GetAccountByIDFunc: func(ctx context.Context, accountID string, userID string) (*types.Account, error) { + return account.Copy(), nil + }, + GetAccountMetaFunc: func(ctx context.Context, accountID string, userID string) (*types.AccountMeta, error) { + return account.GetMeta(), nil + }, }, settingsManager: settingsMockManager, } diff --git a/management/server/mock_server/account_mock.go b/management/server/mock_server/account_mock.go index 870fe3219..804877a66 100644 --- a/management/server/mock_server/account_mock.go +++ b/management/server/mock_server/account_mock.go @@ -116,6 +116,7 @@ type MockAccountManager struct { UpdateToPrimaryAccountFunc func(ctx context.Context, accountId string) (*types.Account, error) GetOwnerInfoFunc func(ctx context.Context, accountID string) (*types.UserInfo, error) GetCurrentUserInfoFunc func(ctx context.Context, accountID, userID string) (*types.UserInfo, error) + GetAccountMetaFunc func(ctx context.Context, accountID, userID string) (*types.AccountMeta, error) } func (am *MockAccountManager) UpdateAccountPeers(ctx context.Context, accountID string) { @@ -803,6 +804,14 @@ func (am *MockAccountManager) GetAccountByID(ctx context.Context, accountID stri return nil, status.Errorf(codes.Unimplemented, "method GetAccountByID is not implemented") } +// GetAccountByID mocks GetAccountByID of the AccountManager interface +func (am *MockAccountManager) GetAccountMeta(ctx context.Context, accountID string, userID string) (*types.AccountMeta, error) { + if am.GetAccountMetaFunc != nil { + return am.GetAccountMetaFunc(ctx, accountID, userID) + } + return nil, status.Errorf(codes.Unimplemented, "method GetAccountMeta is not implemented") +} + // GetUserByID mocks GetUserByID of the AccountManager interface func (am *MockAccountManager) GetUserByID(ctx context.Context, id string) (*types.User, error) { if am.GetUserByIDFunc != nil { diff --git a/management/server/store/sql_store.go b/management/server/store/sql_store.go index aacb56ab8..b73c372ae 100644 --- a/management/server/store/sql_store.go +++ b/management/server/store/sql_store.go @@ -658,6 +658,21 @@ func (s *SqlStore) GetAllAccounts(ctx context.Context) (all []*types.Account) { return all } +func (s *SqlStore) GetAccountMeta(ctx context.Context, lockStrength LockingStrength, accountID string) (*types.AccountMeta, error) { + var accountMeta types.AccountMeta + result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Model(&types.Account{}). + First(&accountMeta, idQueryCondition, accountID) + if result.Error != nil { + log.WithContext(ctx).Errorf("error when getting account meta %s from the store: %s", accountID, result.Error) + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return nil, status.NewAccountNotFoundError(accountID) + } + return nil, status.NewGetAccountFromStoreError(result.Error) + } + + return &accountMeta, nil +} + func (s *SqlStore) GetAccount(ctx context.Context, accountID string) (*types.Account, error) { start := time.Now() defer func() { diff --git a/management/server/store/sql_store_test.go b/management/server/store/sql_store_test.go index 589e727e9..c16a50108 100644 --- a/management/server/store/sql_store_test.go +++ b/management/server/store/sql_store_test.go @@ -3247,3 +3247,19 @@ func TestSqlStore_SaveGroups_LargeBatch(t *testing.T) { require.NoError(t, err) require.Equal(t, 8003, len(accountGroups)) } + +func TestSqlStore_GetAccountMeta(t *testing.T) { + store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir()) + t.Cleanup(cleanup) + require.NoError(t, err) + + accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" + accountMeta, err := store.GetAccountMeta(context.Background(), LockingStrengthShare, accountID) + require.NoError(t, err) + require.NotNil(t, accountMeta) + require.Equal(t, accountID, accountMeta.AccountID) + require.Equal(t, "edafee4e-63fb-11ec-90d6-0242ac120003", accountMeta.CreatedBy) + require.Equal(t, "test.com", accountMeta.Domain) + require.Equal(t, "private", accountMeta.DomainCategory) + require.Equal(t, time.Date(2024, time.October, 2, 14, 1, 38, 210000000, time.UTC), accountMeta.CreatedAt.UTC()) +} diff --git a/management/server/store/store.go b/management/server/store/store.go index c13a8dfe6..4a26bf5c3 100644 --- a/management/server/store/store.go +++ b/management/server/store/store.go @@ -50,6 +50,7 @@ type Store interface { GetAccountsCounter(ctx context.Context) (int64, error) GetAllAccounts(ctx context.Context) []*types.Account GetAccount(ctx context.Context, accountID string) (*types.Account, error) + GetAccountMeta(ctx context.Context, lockStrength LockingStrength, accountID string) (*types.AccountMeta, error) AccountExists(ctx context.Context, lockStrength LockingStrength, id string) (bool, error) GetAccountDomainAndCategory(ctx context.Context, lockStrength LockingStrength, accountID string) (string, string, error) GetAccountByUser(ctx context.Context, userID string) (*types.Account, error) diff --git a/management/server/testdata/extended-store.sql b/management/server/testdata/extended-store.sql index 2859e82c8..7900dabf5 100644 --- a/management/server/testdata/extended-store.sql +++ b/management/server/testdata/extended-store.sql @@ -25,7 +25,7 @@ CREATE INDEX `idx_routes_account_id` ON `routes`(`account_id`); CREATE INDEX `idx_name_server_groups_account_id` ON `name_server_groups`(`account_id`); CREATE INDEX `idx_posture_checks_account_id` ON `posture_checks`(`account_id`); -INSERT INTO accounts VALUES('bf1c8084-ba50-4ce7-9439-34653001fc3b','','2024-10-02 16:01:38.210014+02:00','test.com','private',1,'af1c8024-ha40-4ce2-9418-34653101fc3c','{"IP":"100.64.0.0","Mask":"//8AAA=="}','',0,'[]',0,86400000000000,0,0,0,'',NULL,NULL,NULL); +INSERT INTO accounts VALUES('bf1c8084-ba50-4ce7-9439-34653001fc3b','edafee4e-63fb-11ec-90d6-0242ac120003','2024-10-02 16:01:38.210000+02:00','test.com','private',1,'af1c8024-ha40-4ce2-9418-34653101fc3c','{"IP":"100.64.0.0","Mask":"//8AAA=="}','',0,'[]',0,86400000000000,0,0,0,'',NULL,NULL,NULL); INSERT INTO setup_keys VALUES('A2C8E62B-38F5-4553-B31E-DD66C696CEBB','bf1c8084-ba50-4ce7-9439-34653001fc3b','A2C8E62B-38F5-4553-B31E-DD66C696CEBB','Default key','reusable','2021-08-19 20:46:20.005936822+02:00','2321-09-18 20:46:20.005936822+02:00','2021-08-19 20:46:20.005936822+02:00',0,0,NULL,'["cfefqs706sqkneg59g2g"]',0,0); INSERT INTO setup_keys VALUES('A2C8E62B-38F5-4553-B31E-DD66C696CEBC','bf1c8084-ba50-4ce7-9439-34653001fc3b','A2C8E62B-38F5-4553-B31E-DD66C696CEBC','Faulty key with non existing group','reusable','2021-08-19 20:46:20.005936822+02:00','2321-09-18 20:46:20.005936822+02:00','2021-08-19 20:46:20.005936822+02:00',0,0,NULL,'["abcd"]',0,0); INSERT INTO users VALUES('edafee4e-63fb-11ec-90d6-0242ac120003','bf1c8084-ba50-4ce7-9439-34653001fc3b','admin',0,0,'','["cfefqs706sqkneg59g3g"]',0,NULL,'2024-10-02 16:01:38.210678+02:00','api',0,''); diff --git a/management/server/types/account.go b/management/server/types/account.go index 687709991..ea5f50001 100644 --- a/management/server/types/account.go +++ b/management/server/types/account.go @@ -40,6 +40,17 @@ const ( type LookupMap map[string]struct{} +// AccountMeta is a struct that contains a stripped down version of the Account object. +// It doesn't carry any peers, groups, policies, or routes, etc. Just some metadata (e.g. ID, created by, created at, etc). +type AccountMeta struct { + // AccountId is the unique identifier of the account + AccountID string `gorm:"column:id"` + CreatedAt time.Time + CreatedBy string + Domain string + DomainCategory string +} + // Account represents a unique account of the system type Account struct { // we have to name column to aid as it collides with Network.Id when work with associations @@ -855,6 +866,16 @@ func (a *Account) Copy() *Account { } } +func (a *Account) GetMeta() *AccountMeta { + return &AccountMeta{ + AccountID: a.Id, + CreatedBy: a.CreatedBy, + CreatedAt: a.CreatedAt, + Domain: a.Domain, + DomainCategory: a.DomainCategory, + } +} + func (a *Account) GetGroupAll() (*Group, error) { for _, g := range a.Groups { if g.Name == "All" {