From fe63a64b6ee45618e6de5a48a5a6cef03be4a23c Mon Sep 17 00:00:00 2001 From: Misha Bragin Date: Thu, 16 Feb 2023 12:00:41 +0100 Subject: [PATCH] Add Account HTTP API (#691) Extend HTTP API with Account endpoints to configure global peer login expiration. GET /api/accounts PUT /api/account/{id}/ The GET endpoint returns an array of accounts with always one account in the list. No exceptions. The PUT endpoint updates account settings: PeerLoginExpiration and PeerLoginExpirationEnabled. PeerLoginExpiration is a duration in seconds after which peers' logins will expire. --- management/server/account.go | 62 ++++++ management/server/account_test.go | 42 ++++ management/server/activity/codes.go | 24 +++ management/server/http/accounts.go | 96 ++++++++++ management/server/http/accounts_test.go | 181 ++++++++++++++++++ management/server/http/api/openapi.yml | 85 ++++++++ management/server/http/api/types.gen.go | 24 +++ management/server/http/handler.go | 4 + management/server/mock_server/account_mock.go | 9 + 9 files changed, 527 insertions(+) create mode 100644 management/server/http/accounts.go create mode 100644 management/server/http/accounts_test.go diff --git a/management/server/account.go b/management/server/account.go index cf1767a31..e2293e508 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -97,6 +97,7 @@ type AccountManager interface { SaveDNSSettings(accountID string, userID string, dnsSettingsToSave *DNSSettings) error GetPeer(accountID, peerID, userID string) (*Peer, error) UpdatePeerLastLogin(peerID string) error + UpdateAccountSettings(accountID, userID string, newSettings *Settings) (*Account, error) } type DefaultAccountManager struct { @@ -315,6 +316,12 @@ func (a *Account) GetPeers() []*Peer { return peers } +// UpdateSettings saves new account settings +func (a *Account) UpdateSettings(update *Settings) *Account { + a.Settings = update.Copy() + return a +} + // UpdatePeer saves new or replaces existing peer func (a *Account) UpdatePeer(update *Peer) { a.Peers[update.ID] = update @@ -596,6 +603,61 @@ func BuildManager(store Store, peersUpdateManager *PeersUpdateManager, idpManage return am, nil } +// UpdateAccountSettings updates Account settings. +// Only users with role UserRoleAdmin can update the account. +// User that performs the update has to belong to the account. +// Returns an updated Account +func (am *DefaultAccountManager) UpdateAccountSettings(accountID, userID string, newSettings *Settings) (*Account, error) { + + halfYearLimit := 180 * 24 * time.Hour + if newSettings.PeerLoginExpiration > halfYearLimit { + return nil, status.Errorf(status.InvalidArgument, "peer login expiration can't be larger than 180 days") + } + + if newSettings.PeerLoginExpiration < time.Hour { + return nil, status.Errorf(status.InvalidArgument, "peer login expiration can't be smaller than one hour") + } + + unlock := am.Store.AcquireAccountLock(accountID) + defer unlock() + + account, err := am.Store.GetAccountByUser(userID) + if err != nil { + return nil, err + } + + user, err := account.FindUser(userID) + if err != nil { + return nil, err + } + + if !user.IsAdmin() { + return nil, status.Errorf(status.PermissionDenied, "user is not allowed to update account") + } + + oldSettings := account.Settings + if oldSettings.PeerLoginExpirationEnabled != newSettings.PeerLoginExpirationEnabled { + event := activity.AccountPeerLoginExpirationEnabled + if !newSettings.PeerLoginExpirationEnabled { + event = activity.AccountPeerLoginExpirationDisabled + } + am.storeEvent(userID, accountID, accountID, event, nil) + } + + if oldSettings.PeerLoginExpiration != newSettings.PeerLoginExpiration { + am.storeEvent(userID, accountID, accountID, activity.AccountPeerLoginExpirationDurationUpdated, nil) + } + + updatedAccount := account.UpdateSettings(newSettings) + + err = am.Store.SaveAccount(account) + if err != nil { + return nil, err + } + + return updatedAccount, nil +} + // newAccount creates a new Account with a generated ID and generated default setup keys. // If ID is already in use (due to collision) we try one more time before returning error func (am *DefaultAccountManager) newAccount(userID, domain string) (*Account, error) { diff --git a/management/server/account_test.go b/management/server/account_test.go index 3ca4f68fd..979c41c86 100644 --- a/management/server/account_test.go +++ b/management/server/account_test.go @@ -1283,6 +1283,48 @@ func hasNilField(x interface{}) error { } return nil } +func TestDefaultAccountManager_DefaultAccountSettings(t *testing.T) { + manager, err := createManager(t) + require.NoError(t, err, "unable to create account manager") + + account, err := manager.GetAccountByUserOrAccountID(userID, "", "") + require.NoError(t, err, "unable to create an account") + + assert.NotNil(t, account.Settings) + assert.Equal(t, account.Settings.PeerLoginExpirationEnabled, true) + assert.Equal(t, account.Settings.PeerLoginExpiration, 24*time.Hour) +} + +func TestDefaultAccountManager_UpdateAccountSettings(t *testing.T) { + manager, err := createManager(t) + require.NoError(t, err, "unable to create account manager") + + account, err := manager.GetAccountByUserOrAccountID(userID, "", "") + require.NoError(t, err, "unable to create an account") + + updated, err := manager.UpdateAccountSettings(account.Id, userID, &Settings{ + PeerLoginExpiration: time.Hour, + PeerLoginExpirationEnabled: false}) + require.NoError(t, err, "expecting to update account settings successfully but got error") + assert.False(t, updated.Settings.PeerLoginExpirationEnabled) + assert.Equal(t, updated.Settings.PeerLoginExpiration, time.Hour) + + account, err = manager.GetAccountByUserOrAccountID("", account.Id, "") + require.NoError(t, err, "unable to get account by ID") + + assert.False(t, account.Settings.PeerLoginExpirationEnabled) + assert.Equal(t, account.Settings.PeerLoginExpiration, time.Hour) + + _, err = manager.UpdateAccountSettings(account.Id, userID, &Settings{ + PeerLoginExpiration: time.Second, + PeerLoginExpirationEnabled: false}) + require.Error(t, err, "expecting to fail when providing PeerLoginExpiration less than one hour") + + _, err = manager.UpdateAccountSettings(account.Id, userID, &Settings{ + PeerLoginExpiration: time.Hour * 24 * 181, + PeerLoginExpirationEnabled: false}) + require.Error(t, err, "expecting to fail when providing PeerLoginExpiration more than 180 days") +} func createManager(t *testing.T) (*DefaultAccountManager, error) { store, err := createStore(t) diff --git a/management/server/activity/codes.go b/management/server/activity/codes.go index 9517aa2e3..0a9d7e50f 100644 --- a/management/server/activity/codes.go +++ b/management/server/activity/codes.go @@ -71,6 +71,12 @@ const ( NameserverGroupDeleted // NameserverGroupUpdated indicates that a user updated a nameservers group NameserverGroupUpdated + // AccountPeerLoginExpirationEnabled indicates that a user enabled peer login expiration for the account + AccountPeerLoginExpirationEnabled + // AccountPeerLoginExpirationDisabled indicates that a user disabled peer login expiration for the account + AccountPeerLoginExpirationDisabled + // AccountPeerLoginExpirationDurationUpdated indicates that a user updated peer login expiration duration for the account + AccountPeerLoginExpirationDurationUpdated ) const ( @@ -144,6 +150,12 @@ const ( NameserverGroupDeletedMessage string = "Nameserver group deleted" // NameserverGroupUpdatedMessage is a human-readable text message of the NameserverGroupUpdated activity NameserverGroupUpdatedMessage string = "Nameserver group updated" + // AccountPeerLoginExpirationEnabledMessage is a human-readable text message of the AccountPeerLoginExpirationEnabled activity + AccountPeerLoginExpirationEnabledMessage string = "Peer login expiration enabled for the account" + // AccountPeerLoginExpirationDisabledMessage is a human-readable text message of the AccountPeerLoginExpirationDisabled activity + AccountPeerLoginExpirationDisabledMessage string = "Peer login expiration disabled for the account" + // AccountPeerLoginExpirationDurationUpdatedMessage is a human-readable text message of the AccountPeerLoginExpirationDurationUpdated activity + AccountPeerLoginExpirationDurationUpdatedMessage string = "Peer login expiration duration updated" ) // Activity that triggered an Event @@ -222,6 +234,12 @@ func (a Activity) Message() string { return NameserverGroupDeletedMessage case NameserverGroupUpdated: return NameserverGroupUpdatedMessage + case AccountPeerLoginExpirationEnabled: + return AccountPeerLoginExpirationEnabledMessage + case AccountPeerLoginExpirationDisabled: + return AccountPeerLoginExpirationDisabledMessage + case AccountPeerLoginExpirationDurationUpdated: + return AccountPeerLoginExpirationDurationUpdatedMessage default: return "UNKNOWN_ACTIVITY" } @@ -300,6 +318,12 @@ func (a Activity) StringCode() string { return "nameserver.group.delete" case NameserverGroupUpdated: return "nameserver.group.update" + case AccountPeerLoginExpirationDurationUpdated: + return "account.settings.peer.login.expiration.update" + case AccountPeerLoginExpirationEnabled: + return "account.setting.peer.login.expiration.enable" + case AccountPeerLoginExpirationDisabled: + return "account.setting.peer.login.expiration.disable" default: return "UNKNOWN_ACTIVITY" } diff --git a/management/server/http/accounts.go b/management/server/http/accounts.go new file mode 100644 index 000000000..6d97893a7 --- /dev/null +++ b/management/server/http/accounts.go @@ -0,0 +1,96 @@ +package http + +import ( + "encoding/json" + "github.com/gorilla/mux" + "github.com/netbirdio/netbird/management/server" + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/http/util" + "github.com/netbirdio/netbird/management/server/jwtclaims" + "github.com/netbirdio/netbird/management/server/status" + "net/http" + "time" +) + +// Accounts is a handler that handles the server.Account HTTP endpoints +type Accounts struct { + accountManager server.AccountManager + claimsExtractor *jwtclaims.ClaimsExtractor +} + +// NewAccounts creates a new Accounts HTTP handler +func NewAccounts(accountManager server.AccountManager, authCfg AuthCfg) *Accounts { + return &Accounts{ + accountManager: accountManager, + claimsExtractor: jwtclaims.NewClaimsExtractor( + jwtclaims.WithAudience(authCfg.Audience), + jwtclaims.WithUserIDClaim(authCfg.UserIDClaim), + ), + } +} + +// GetAccountsHandler is HTTP GET handler that returns a list of accounts. Effectively returns just a single account. +func (h *Accounts) GetAccountsHandler(w http.ResponseWriter, r *http.Request) { + claims := h.claimsExtractor.FromRequestContext(r) + account, user, err := h.accountManager.GetAccountFromToken(claims) + if err != nil { + util.WriteError(err, w) + return + } + + if !user.IsAdmin() { + util.WriteError(status.Errorf(status.PermissionDenied, "the user has no permission to access account data"), w) + return + } + + resp := toAccountResponse(account) + util.WriteJSONObject(w, []*api.Account{resp}) +} + +// UpdateAccountHandler is HTTP PUT handler that updates the provided account. Updates only account settings (server.Settings) +func (h *Accounts) UpdateAccountHandler(w http.ResponseWriter, r *http.Request) { + claims := h.claimsExtractor.FromRequestContext(r) + _, user, err := h.accountManager.GetAccountFromToken(claims) + if err != nil { + util.WriteError(err, w) + return + } + + vars := mux.Vars(r) + accountID := vars["id"] + if len(accountID) == 0 { + util.WriteError(status.Errorf(status.InvalidArgument, "invalid accountID ID"), w) + return + } + + var req api.PutApiAccountsIdJSONBody + err = json.NewDecoder(r.Body).Decode(&req) + if err != nil { + util.WriteErrorResponse("couldn't parse JSON request", http.StatusBadRequest, w) + return + } + + updatedAccount, err := h.accountManager.UpdateAccountSettings(accountID, user.Id, &server.Settings{ + PeerLoginExpirationEnabled: req.Settings.PeerLoginExpirationEnabled, + PeerLoginExpiration: time.Duration(float64(time.Second.Nanoseconds()) * float64(req.Settings.PeerLoginExpiration)), + }) + + if err != nil { + util.WriteError(err, w) + return + } + + resp := toAccountResponse(updatedAccount) + + util.WriteJSONObject(w, &resp) +} + +func toAccountResponse(account *server.Account) *api.Account { + return &api.Account{ + Id: account.Id, + Settings: api.AccountSettings{ + PeerLoginExpiration: int(account.Settings.PeerLoginExpiration.Seconds()), + PeerLoginExpirationEnabled: account.Settings.PeerLoginExpirationEnabled, + }, + } +} diff --git a/management/server/http/accounts_test.go b/management/server/http/accounts_test.go new file mode 100644 index 000000000..b01af860f --- /dev/null +++ b/management/server/http/accounts_test.go @@ -0,0 +1,181 @@ +package http + +import ( + "bytes" + "encoding/json" + "github.com/gorilla/mux" + "github.com/netbirdio/netbird/management/server" + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/jwtclaims" + "github.com/netbirdio/netbird/management/server/mock_server" + "github.com/netbirdio/netbird/management/server/status" + "github.com/stretchr/testify/assert" + "io" + "net/http" + "net/http/httptest" + "testing" + "time" +) + +func initAccountsTestData(account *server.Account, admin *server.User) *Accounts { + return &Accounts{ + accountManager: &mock_server.MockAccountManager{ + GetAccountFromTokenFunc: func(claims jwtclaims.AuthorizationClaims) (*server.Account, *server.User, error) { + return account, admin, nil + }, + UpdateAccountSettingsFunc: func(accountID, userID string, newSettings *server.Settings) (*server.Account, error) { + halfYearLimit := 180 * 24 * time.Hour + if newSettings.PeerLoginExpiration > halfYearLimit { + return nil, status.Errorf(status.InvalidArgument, "peer login expiration can't be larger than 180 days") + } + + if newSettings.PeerLoginExpiration < time.Hour { + return nil, status.Errorf(status.InvalidArgument, "peer login expiration can't be smaller than one hour") + } + + accCopy := account.Copy() + accCopy.UpdateSettings(newSettings) + return accCopy, nil + + }, + }, + claimsExtractor: jwtclaims.NewClaimsExtractor( + jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims { + return jwtclaims.AuthorizationClaims{ + UserId: "test_user", + Domain: "hotmail.com", + AccountId: "test_account", + } + }), + ), + } +} + +func TestAccounts_AccountsHandler(t *testing.T) { + + accountID := "test_account" + adminUser := server.NewAdminUser("test_user") + + handler := initAccountsTestData(&server.Account{ + Id: accountID, + Domain: "hotmail.com", + Network: server.NewNetwork(), + Users: map[string]*server.User{ + adminUser.Id: adminUser, + }, + Settings: &server.Settings{ + PeerLoginExpirationEnabled: false, + PeerLoginExpiration: time.Hour, + }, + }, adminUser) + + tt := []struct { + name string + expectedStatus int + expectedBody bool + expectedID string + expectedArray bool + expectedSettings api.AccountSettings + requestType string + requestPath string + requestBody io.Reader + }{ + { + name: "GetAccounts OK", + expectedBody: true, + requestType: http.MethodGet, + requestPath: "/api/accounts", + expectedStatus: http.StatusOK, + expectedSettings: api.AccountSettings{ + PeerLoginExpiration: int(time.Hour.Seconds()), + PeerLoginExpirationEnabled: false, + }, + expectedArray: true, + expectedID: accountID, + }, + { + name: "PutAccount OK", + expectedBody: true, + requestType: http.MethodPut, + requestPath: "/api/accounts/" + accountID, + requestBody: bytes.NewBufferString("{\"settings\": {\"peer_login_expiration\": 15552000,\"peer_login_expiration_enabled\": true}}"), + expectedStatus: http.StatusOK, + expectedSettings: api.AccountSettings{ + PeerLoginExpiration: 15552000, + PeerLoginExpirationEnabled: true, + }, + expectedArray: false, + expectedID: accountID, + }, + { + name: "Update account failure with high peer_login_expiration more than 180 days", + expectedBody: true, + requestType: http.MethodPut, + requestPath: "/api/accounts/" + accountID, + requestBody: bytes.NewBufferString("{\"settings\": {\"peer_login_expiration\": 15552001,\"peer_login_expiration_enabled\": true}}"), + expectedStatus: http.StatusUnprocessableEntity, + expectedArray: false, + }, + { + name: "Update account failure with peer_login_expiration less than an hour", + expectedBody: true, + requestType: http.MethodPut, + requestPath: "/api/accounts/" + accountID, + requestBody: bytes.NewBufferString("{\"settings\": {\"peer_login_expiration\": 3599,\"peer_login_expiration_enabled\": true}}"), + expectedStatus: http.StatusUnprocessableEntity, + expectedArray: false, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + recorder := httptest.NewRecorder() + req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody) + + router := mux.NewRouter() + router.HandleFunc("/api/accounts", handler.GetAccountsHandler).Methods("GET") + router.HandleFunc("/api/accounts/{id}", handler.UpdateAccountHandler).Methods("PUT") + router.ServeHTTP(recorder, req) + + res := recorder.Result() + defer res.Body.Close() + + if status := recorder.Code; status != tc.expectedStatus { + t.Errorf("handler returned wrong status code: got %v want %v", + status, tc.expectedStatus) + return + } + + if tc.expectedStatus != http.StatusOK { + return + } + + if !tc.expectedBody { + return + } + + content, err := io.ReadAll(res.Body) + if err != nil { + t.Fatalf("I don't know what I expected; %v", err) + } + + var actual *api.Account + if tc.expectedArray { + var got []*api.Account + if err = json.Unmarshal(content, &got); err != nil { + t.Fatalf("Sent content is not in correct json format; %v", err) + } + + assert.Len(t, got, 1) + actual = got[0] + } else { + if err = json.Unmarshal(content, &actual); err != nil { + t.Fatalf("Sent content is not in correct json format; %v", err) + } + } + + assert.Equal(t, tc.expectedID, actual.Id) + assert.Equal(t, tc.expectedSettings, actual.Settings) + }) + } +} diff --git a/management/server/http/api/openapi.yml b/management/server/http/api/openapi.yml index cbe61275f..a2101551f 100644 --- a/management/server/http/api/openapi.yml +++ b/management/server/http/api/openapi.yml @@ -20,8 +20,31 @@ tags: description: Interact with and view information about DNS configuration. - name: Events description: View information about the account and network events. + - name: Accounts + description: View information about the accounts. components: schemas: + Account: + properties: + id: + description: Account ID + type: string + settings: + $ref: '#/components/schemas/AccountSettings' + required: + - id + - settings + AccountSettings: + properties: + peer_login_expiration_enabled: + description: Enables or disables peer login expiration globally. After peer's login has expired the user has to log in (authenticate). Applies only to peers that were added by a user (interactive SSO login). + type: boolean + peer_login_expiration: + description: Period of time after which peer login expires (seconds). + type: integer + required: + - peer_login_expiration_enabled + - peer_login_expiration User: type: object properties: @@ -606,6 +629,68 @@ components: security: - BearerAuth: [ ] paths: + /api/accounts: + get: + summary: Returns a list of accounts of a user. Always returns a list of one account. Only available for admin users. + tags: [ Accounts ] + security: + - BearerAuth: [ ] + responses: + '200': + description: A JSON array of accounts + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Account' + '400': + "$ref": "#/components/responses/bad_request" + '401': + "$ref": "#/components/responses/requires_authentication" + '403': + "$ref": "#/components/responses/forbidden" + '500': + "$ref": "#/components/responses/internal_error" + /api/accounts/{id}: + put: + summary: Update information about an account + tags: [ Accounts ] + security: + - BearerAuth: [ ] + parameters: + - in: path + name: id + required: true + schema: + type: string + description: The Account ID + requestBody: + description: update an account + content: + 'application/json': + schema: + type: object + properties: + settings: + $ref: '#/components/schemas/AccountSettings' + required: + - settings + responses: + '200': + description: An Account object + content: + application/json: + schema: + $ref: '#/components/schemas/Account' + '400': + "$ref": "#/components/responses/bad_request" + '401': + "$ref": "#/components/responses/requires_authentication" + '403': + "$ref": "#/components/responses/forbidden" + '500': + "$ref": "#/components/responses/internal_error" /api/users: get: summary: Returns a list of all users diff --git a/management/server/http/api/types.gen.go b/management/server/http/api/types.gen.go index f41cfcd10..438a0dbba 100644 --- a/management/server/http/api/types.gen.go +++ b/management/server/http/api/types.gen.go @@ -134,6 +134,22 @@ const ( UserStatusInvited UserStatus = "invited" ) +// Account defines model for Account. +type Account struct { + // Id Account ID + Id string `json:"id"` + Settings AccountSettings `json:"settings"` +} + +// AccountSettings defines model for AccountSettings. +type AccountSettings struct { + // PeerLoginExpiration Period of time after which peer login expires (seconds). + PeerLoginExpiration int `json:"peer_login_expiration"` + + // PeerLoginExpirationEnabled Enables or disables peer login expiration globally. After peer's login has expired the user has to log in (authenticate). Applies only to peers that were added by a user (interactive SSO login). + PeerLoginExpirationEnabled bool `json:"peer_login_expiration_enabled"` +} + // DNSSettings defines model for DNSSettings. type DNSSettings struct { // DisabledManagementGroups Groups whose DNS management is disabled @@ -617,6 +633,11 @@ type UserRequest struct { Role string `json:"role"` } +// PutApiAccountsIdJSONBody defines parameters for PutApiAccountsId. +type PutApiAccountsIdJSONBody struct { + Settings AccountSettings `json:"settings"` +} + // PatchApiDnsNameserversIdJSONBody defines parameters for PatchApiDnsNameserversId. type PatchApiDnsNameserversIdJSONBody = []NameserverGroupPatchOperation @@ -682,6 +703,9 @@ type PutApiRulesIdJSONBody struct { Sources *[]string `json:"sources,omitempty"` } +// PutApiAccountsIdJSONRequestBody defines body for PutApiAccountsId for application/json ContentType. +type PutApiAccountsIdJSONRequestBody PutApiAccountsIdJSONBody + // PostApiDnsNameserversJSONRequestBody defines body for PostApiDnsNameservers for application/json ContentType. type PostApiDnsNameserversJSONRequestBody = NameserverGroupRequest diff --git a/management/server/http/handler.go b/management/server/http/handler.go index a56df91ee..069068106 100644 --- a/management/server/http/handler.go +++ b/management/server/http/handler.go @@ -50,6 +50,10 @@ func APIHandler(accountManager s.AccountManager, appMetrics telemetry.AppMetrics nameserversHandler := NewNameservers(accountManager, authCfg) eventsHandler := NewEvents(accountManager, authCfg) dnsSettingsHandler := NewDNSSettings(accountManager, authCfg) + accountsHandler := NewAccounts(accountManager, authCfg) + + apiHandler.HandleFunc("/accounts/{id}", accountsHandler.UpdateAccountHandler).Methods("PUT", "OPTIONS") + apiHandler.HandleFunc("/accounts", accountsHandler.GetAccountsHandler).Methods("GET", "OPTIONS") apiHandler.HandleFunc("/peers", peersHandler.GetPeers).Methods("GET", "OPTIONS") apiHandler.HandleFunc("/peers/{id}", peersHandler.HandlePeer). diff --git a/management/server/mock_server/account_mock.go b/management/server/mock_server/account_mock.go index 30cf9ec30..90b6f1d07 100644 --- a/management/server/mock_server/account_mock.go +++ b/management/server/mock_server/account_mock.go @@ -70,6 +70,7 @@ type MockAccountManager struct { GetPeerFunc func(accountID, peerID, userID string) (*server.Peer, error) GetAccountByPeerIDFunc func(peerID string) (*server.Account, error) UpdatePeerLastLoginFunc func(peerID string) error + UpdateAccountSettingsFunc func(accountID, userID string, newSettings *server.Settings) (*server.Account, error) } // GetUsersFromAccount mock implementation of GetUsersFromAccount from server.AccountManager interface @@ -553,3 +554,11 @@ func (am *MockAccountManager) UpdatePeerLastLogin(peerID string) error { } return status.Errorf(codes.Unimplemented, "method UpdatePeerLastLogin is not implemented") } + +// UpdateAccountSettings mocks UpdateAccountSettings of the AccountManager interface +func (am *MockAccountManager) UpdateAccountSettings(accountID, userID string, newSettings *server.Settings) (*server.Account, error) { + if am.UpdateAccountSettingsFunc != nil { + return am.UpdateAccountSettingsFunc(accountID, userID, newSettings) + } + return nil, status.Errorf(codes.Unimplemented, "method UpdateAccountSettings is not implemented") +}