add owner role support (#1340)

This PR adds support to Owner roles.

The owner role has a similar access level as the admin, but it has the power to delete the account.

Besides that, the role has the following constraints:

- The role can only be transferred. So, only a user with the owner role can transfer the owner role to a new user
- It can't be assigned to users being invited
- It can't be assigned to service users
This commit is contained in:
Maycon Santos 2023-12-01 17:24:57 +01:00 committed by GitHub
parent b8c46e2654
commit d7efea74b6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 397 additions and 111 deletions

View File

@ -877,7 +877,7 @@ func (am *DefaultAccountManager) UpdateAccountSettings(accountID, userID string,
return nil, err return nil, err
} }
if !user.IsAdmin() { if !user.HasAdminPower() {
return nil, status.Errorf(status.PermissionDenied, "user is not allowed to update account") return nil, status.Errorf(status.PermissionDenied, "user is not allowed to update account")
} }
@ -1019,7 +1019,7 @@ func (am *DefaultAccountManager) DeleteAccount(accountID, userID string) error {
return err return err
} }
if !user.IsAdmin() { if !user.HasAdminPower() {
return status.Errorf(status.PermissionDenied, "user is not allowed to delete account") return status.Errorf(status.PermissionDenied, "user is not allowed to delete account")
} }
@ -1703,7 +1703,7 @@ func newAccountWithId(accountID, userID, domain string) *Account {
routes := make(map[string]*route.Route) routes := make(map[string]*route.Route)
setupKeys := map[string]*SetupKey{} setupKeys := map[string]*SetupKey{}
nameServersGroups := make(map[string]*nbdns.NameServerGroup) nameServersGroups := make(map[string]*nbdns.NameServerGroup)
users[userID] = NewAdminUser(userID) users[userID] = NewOwnerUser(userID)
dnsSettings := DNSSettings{ dnsSettings := DNSSettings{
DisabledManagementGroups: make([]string, 0), DisabledManagementGroups: make([]string, 0),
} }

View File

@ -306,7 +306,7 @@ func TestDefaultAccountManager_GetAccountFromToken(t *testing.T) {
inputInitUserParams: defaultInitAccount, inputInitUserParams: defaultInitAccount,
testingFunc: require.NotEqual, testingFunc: require.NotEqual,
expectedMSG: "account IDs shouldn't match", expectedMSG: "account IDs shouldn't match",
expectedUserRole: UserRoleAdmin, expectedUserRole: UserRoleOwner,
expectedDomainCategory: "", expectedDomainCategory: "",
expectedDomain: publicDomain, expectedDomain: publicDomain,
expectedPrimaryDomainStatus: false, expectedPrimaryDomainStatus: false,
@ -328,7 +328,7 @@ func TestDefaultAccountManager_GetAccountFromToken(t *testing.T) {
inputInitUserParams: initUnknown, inputInitUserParams: initUnknown,
testingFunc: require.NotEqual, testingFunc: require.NotEqual,
expectedMSG: "account IDs shouldn't match", expectedMSG: "account IDs shouldn't match",
expectedUserRole: UserRoleAdmin, expectedUserRole: UserRoleOwner,
expectedDomain: unknownDomain, expectedDomain: unknownDomain,
expectedDomainCategory: "", expectedDomainCategory: "",
expectedPrimaryDomainStatus: false, expectedPrimaryDomainStatus: false,
@ -346,7 +346,7 @@ func TestDefaultAccountManager_GetAccountFromToken(t *testing.T) {
inputInitUserParams: defaultInitAccount, inputInitUserParams: defaultInitAccount,
testingFunc: require.NotEqual, testingFunc: require.NotEqual,
expectedMSG: "account IDs shouldn't match", expectedMSG: "account IDs shouldn't match",
expectedUserRole: UserRoleAdmin, expectedUserRole: UserRoleOwner,
expectedDomain: privateDomain, expectedDomain: privateDomain,
expectedDomainCategory: PrivateCategory, expectedDomainCategory: PrivateCategory,
expectedPrimaryDomainStatus: true, expectedPrimaryDomainStatus: true,
@ -387,7 +387,7 @@ func TestDefaultAccountManager_GetAccountFromToken(t *testing.T) {
inputInitUserParams: defaultInitAccount, inputInitUserParams: defaultInitAccount,
testingFunc: require.Equal, testingFunc: require.Equal,
expectedMSG: "account IDs should match", expectedMSG: "account IDs should match",
expectedUserRole: UserRoleAdmin, expectedUserRole: UserRoleOwner,
expectedDomain: defaultInitAccount.Domain, expectedDomain: defaultInitAccount.Domain,
expectedDomainCategory: PrivateCategory, expectedDomainCategory: PrivateCategory,
expectedPrimaryDomainStatus: true, expectedPrimaryDomainStatus: true,
@ -406,7 +406,7 @@ func TestDefaultAccountManager_GetAccountFromToken(t *testing.T) {
inputInitUserParams: defaultInitAccount, inputInitUserParams: defaultInitAccount,
testingFunc: require.Equal, testingFunc: require.Equal,
expectedMSG: "account IDs should match", expectedMSG: "account IDs should match",
expectedUserRole: UserRoleAdmin, expectedUserRole: UserRoleOwner,
expectedDomain: defaultInitAccount.Domain, expectedDomain: defaultInitAccount.Domain,
expectedDomainCategory: PrivateCategory, expectedDomainCategory: PrivateCategory,
expectedPrimaryDomainStatus: true, expectedPrimaryDomainStatus: true,
@ -424,7 +424,7 @@ func TestDefaultAccountManager_GetAccountFromToken(t *testing.T) {
inputInitUserParams: defaultInitAccount, inputInitUserParams: defaultInitAccount,
testingFunc: require.NotEqual, testingFunc: require.NotEqual,
expectedMSG: "account IDs shouldn't match", expectedMSG: "account IDs shouldn't match",
expectedUserRole: UserRoleAdmin, expectedUserRole: UserRoleOwner,
expectedDomain: "", expectedDomain: "",
expectedDomainCategory: "", expectedDomainCategory: "",
expectedPrimaryDomainStatus: false, expectedPrimaryDomainStatus: false,
@ -1183,7 +1183,7 @@ func TestGetUsersFromAccount(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
users := map[string]*User{"1": {Id: "1", Role: "admin"}, "2": {Id: "2", Role: "user"}, "3": {Id: "3", Role: "user"}} users := map[string]*User{"1": {Id: "1", Role: UserRoleOwner}, "2": {Id: "2", Role: "user"}, "3": {Id: "3", Role: "user"}}
accountId := "test_account_id" accountId := "test_account_id"
account, err := createAccount(manager, accountId, users["1"].Id, "") account, err := createAccount(manager, accountId, users["1"].Id, "")

View File

@ -120,6 +120,8 @@ const (
IntegrationUpdated IntegrationUpdated
// IntegrationDeleted indicates that the user deleted an integration // IntegrationDeleted indicates that the user deleted an integration
IntegrationDeleted IntegrationDeleted
// TransferredOwnerRole indicates that the user transferred the owner role of the account
TransferredOwnerRole
) )
var activityMap = map[Activity]Code{ var activityMap = map[Activity]Code{
@ -178,6 +180,7 @@ var activityMap = map[Activity]Code{
IntegrationCreated: {"Integration created", "integration.create"}, IntegrationCreated: {"Integration created", "integration.create"},
IntegrationUpdated: {"Integration updated", "integration.update"}, IntegrationUpdated: {"Integration updated", "integration.update"},
IntegrationDeleted: {"Integration deleted", "integration.delete"}, IntegrationDeleted: {"Integration deleted", "integration.delete"},
TransferredOwnerRole: {"Transferred owner role", "transferred.owner.role"},
} }
// StringCode returns a string code of the activity // StringCode returns a string code of the activity

View File

@ -47,8 +47,8 @@ func (am *DefaultAccountManager) GetDNSSettings(accountID string, userID string)
return nil, err return nil, err
} }
if !user.IsAdmin() { if !user.HasAdminPower() {
return nil, status.Errorf(status.PermissionDenied, "only admins are allowed to view DNS settings") return nil, status.Errorf(status.PermissionDenied, "only users with admin power are allowed to view DNS settings")
} }
dnsSettings := account.DNSSettings.Copy() dnsSettings := account.DNSSettings.Copy()
return &dnsSettings, nil return &dnsSettings, nil
@ -69,8 +69,8 @@ func (am *DefaultAccountManager) SaveDNSSettings(accountID string, userID string
return err return err
} }
if !user.IsAdmin() { if !user.HasAdminPower() {
return status.Errorf(status.PermissionDenied, "only admins are allowed to update DNS settings") return status.Errorf(status.PermissionDenied, "only users with admin power are allowed to update DNS settings")
} }
if dnsSettingsToSave == nil { if dnsSettingsToSave == nil {

View File

@ -170,7 +170,7 @@ func (am *DefaultAccountManager) DeleteGroup(accountId, userId, groupID string)
return status.Errorf(status.NotFound, "user not found") return status.Errorf(status.NotFound, "user not found")
} }
if executingUser.Role != UserRoleAdmin || !executingUser.IsServiceUser { if executingUser.Role != UserRoleAdmin || !executingUser.IsServiceUser {
return status.Errorf(status.PermissionDenied, "only admins service user can delete integration group") return status.Errorf(status.PermissionDenied, "only service users with admin power can delete integration group")
} }
} }

View File

@ -57,7 +57,7 @@ func TestDefaultAccountManager_DeleteGroup(t *testing.T) {
{ {
"integration", "integration",
"grp-for-integration", "grp-for-integration",
"only admins service user can delete integration group", "only service users with admin power can delete integration group",
}, },
} }

View File

@ -40,7 +40,7 @@ func (h *AccountsHandler) GetAllAccounts(w http.ResponseWriter, r *http.Request)
return return
} }
if !user.IsAdmin() { if !user.HasAdminPower() {
util.WriteError(status.Errorf(status.PermissionDenied, "the user has no permission to access account data"), w) util.WriteError(status.Errorf(status.PermissionDenied, "the user has no permission to access account data"), w)
return return
} }

View File

@ -53,7 +53,7 @@ func (a *AccessControl) Handler(h http.Handler) http.Handler {
return return
} }
if !user.IsAdmin() { if !user.HasAdminPower() {
switch r.Method { switch r.Method {
case http.MethodDelete, http.MethodPost, http.MethodPatch, http.MethodPut: case http.MethodDelete, http.MethodPost, http.MethodPatch, http.MethodPut:
@ -63,7 +63,7 @@ func (a *AccessControl) Handler(h http.Handler) http.Handler {
return return
} }
util.WriteError(status.Errorf(status.PermissionDenied, "only admin can perform this operation"), w) util.WriteError(status.Errorf(status.PermissionDenied, "only users with admin power can perform this operation"), w)
return return
} }
} }

View File

@ -0,0 +1,77 @@
package idp
// MockIDP is a mock implementation of the IDP interface
type MockIDP struct {
UpdateUserAppMetadataFunc func(userId string, appMetadata AppMetadata) error
GetUserDataByIDFunc func(userId string, appMetadata AppMetadata) (*UserData, error)
GetAccountFunc func(accountId string) ([]*UserData, error)
GetAllAccountsFunc func() (map[string][]*UserData, error)
CreateUserFunc func(email, name, accountID, invitedByEmail string) (*UserData, error)
GetUserByEmailFunc func(email string) ([]*UserData, error)
InviteUserByIDFunc func(userID string) error
DeleteUserFunc func(userID string) error
}
// UpdateUserAppMetadata is a mock implementation of the IDP interface UpdateUserAppMetadata method
func (m *MockIDP) UpdateUserAppMetadata(userId string, appMetadata AppMetadata) error {
if m.UpdateUserAppMetadataFunc != nil {
return m.UpdateUserAppMetadataFunc(userId, appMetadata)
}
return nil
}
// GetUserDataByID is a mock implementation of the IDP interface GetUserDataByID method
func (m *MockIDP) GetUserDataByID(userId string, appMetadata AppMetadata) (*UserData, error) {
if m.GetUserDataByIDFunc != nil {
return m.GetUserDataByIDFunc(userId, appMetadata)
}
return nil, nil
}
// GetAccount is a mock implementation of the IDP interface GetAccount method
func (m *MockIDP) GetAccount(accountId string) ([]*UserData, error) {
if m.GetAccountFunc != nil {
return m.GetAccountFunc(accountId)
}
return nil, nil
}
// GetAllAccounts is a mock implementation of the IDP interface GetAllAccounts method
func (m *MockIDP) GetAllAccounts() (map[string][]*UserData, error) {
if m.GetAllAccountsFunc != nil {
return m.GetAllAccountsFunc()
}
return nil, nil
}
// CreateUser is a mock implementation of the IDP interface CreateUser method
func (m *MockIDP) CreateUser(email, name, accountID, invitedByEmail string) (*UserData, error) {
if m.CreateUserFunc != nil {
return m.CreateUserFunc(email, name, accountID, invitedByEmail)
}
return nil, nil
}
// GetUserByEmail is a mock implementation of the IDP interface GetUserByEmail method
func (m *MockIDP) GetUserByEmail(email string) ([]*UserData, error) {
if m.GetUserByEmailFunc != nil {
return m.GetUserByEmailFunc(email)
}
return nil, nil
}
// InviteUserByID is a mock implementation of the IDP interface InviteUserByID method
func (m *MockIDP) InviteUserByID(userID string) error {
if m.InviteUserByIDFunc != nil {
return m.InviteUserByIDFunc(userID)
}
return nil
}
// DeleteUser is a mock implementation of the IDP interface DeleteUser method
func (m *MockIDP) DeleteUser(userID string) error {
if m.DeleteUserFunc != nil {
return m.DeleteUserFunc(userID)
}
return nil
}

View File

@ -214,7 +214,7 @@ func (am *DefaultAccountManager) GetPeers(accountID, userID string) ([]*Peer, er
peers := make([]*Peer, 0) peers := make([]*Peer, 0)
peersMap := make(map[string]*Peer) peersMap := make(map[string]*Peer)
for _, peer := range account.Peers { for _, peer := range account.Peers {
if !user.IsAdmin() && user.Id != peer.UserID { if !user.HasAdminPower() && user.Id != peer.UserID {
// only display peers that belong to the current user if the current user is not an admin // only display peers that belong to the current user if the current user is not an admin
continue continue
} }
@ -862,7 +862,7 @@ func (am *DefaultAccountManager) GetPeer(accountID, peerID, userID string) (*Pee
} }
// if admin or user owns this peer, return peer // if admin or user owns this peer, return peer
if user.IsAdmin() || peer.UserID == userID { if user.HasAdminPower() || peer.UserID == userID {
return peer, nil return peer, nil
} }

View File

@ -319,8 +319,8 @@ func (am *DefaultAccountManager) GetPolicy(accountID, policyID, userID string) (
return nil, err return nil, err
} }
if !user.IsAdmin() { if !user.HasAdminPower() {
return nil, status.Errorf(status.PermissionDenied, "only admins are allowed to view policies") return nil, status.Errorf(status.PermissionDenied, "only users with admin power are allowed to view policies")
} }
for _, policy := range account.Policies { for _, policy := range account.Policies {
@ -402,8 +402,8 @@ func (am *DefaultAccountManager) ListPolicies(accountID, userID string) ([]*Poli
return nil, err return nil, err
} }
if !user.IsAdmin() { if !user.HasAdminPower() {
return nil, status.Errorf(status.PermissionDenied, "Only Administrators can view policies") return nil, status.Errorf(status.PermissionDenied, "only users with admin power can view policies")
} }
return account.Policies, nil return account.Policies, nil

View File

@ -27,8 +27,8 @@ func (am *DefaultAccountManager) GetRoute(accountID, routeID, userID string) (*r
return nil, err return nil, err
} }
if !user.IsAdmin() { if !user.HasAdminPower() {
return nil, status.Errorf(status.PermissionDenied, "Only administrators can view Network Routes") return nil, status.Errorf(status.PermissionDenied, "only users with admin power can view Network Routes")
} }
wantedRoute, found := account.Routes[routeID] wantedRoute, found := account.Routes[routeID]
@ -296,8 +296,8 @@ func (am *DefaultAccountManager) ListRoutes(accountID, userID string) ([]*route.
return nil, err return nil, err
} }
if !user.IsAdmin() { if !user.HasAdminPower() {
return nil, status.Errorf(status.PermissionDenied, "Only administrators can view Network Routes") return nil, status.Errorf(status.PermissionDenied, "only users with admin power can view Network Routes")
} }
routes := make([]*route.Route, 0, len(account.Routes)) routes := make([]*route.Route, 0, len(account.Routes))

View File

@ -342,7 +342,7 @@ func (am *DefaultAccountManager) ListSetupKeys(accountID, userID string) ([]*Set
keys := make([]*SetupKey, 0, len(account.SetupKeys)) keys := make([]*SetupKey, 0, len(account.SetupKeys))
for _, key := range account.SetupKeys { for _, key := range account.SetupKeys {
var k *SetupKey var k *SetupKey
if !user.IsAdmin() { if !user.HasAdminPower() {
k = key.HiddenCopy(999) k = key.HiddenCopy(999)
} else { } else {
k = key.Copy() k = key.Copy()
@ -384,7 +384,7 @@ func (am *DefaultAccountManager) GetSetupKey(accountID, userID, keyID string) (*
foundKey.UpdatedAt = foundKey.CreatedAt foundKey.UpdatedAt = foundKey.CreatedAt
} }
if !user.IsAdmin() { if !user.HasAdminPower() {
foundKey = foundKey.HiddenCopy(999) foundKey = foundKey.HiddenCopy(999)
} }

View File

@ -15,6 +15,7 @@ import (
) )
const ( const (
UserRoleOwner UserRole = "owner"
UserRoleAdmin UserRole = "admin" UserRoleAdmin UserRole = "admin"
UserRoleUser UserRole = "user" UserRoleUser UserRole = "user"
UserRoleUnknown UserRole = "unknown" UserRoleUnknown UserRole = "unknown"
@ -30,6 +31,8 @@ const (
// StrRoleToUserRole returns UserRole for a given strRole or UserRoleUnknown if the specified role is unknown // StrRoleToUserRole returns UserRole for a given strRole or UserRoleUnknown if the specified role is unknown
func StrRoleToUserRole(strRole string) UserRole { func StrRoleToUserRole(strRole string) UserRole {
switch strings.ToLower(strRole) { switch strings.ToLower(strRole) {
case "owner":
return UserRoleOwner
case "admin": case "admin":
return UserRoleAdmin return UserRoleAdmin
case "user": case "user":
@ -97,9 +100,9 @@ func (u *User) LastDashboardLoginChanged(LastLogin time.Time) bool {
return LastLogin.After(u.LastLogin) && !u.LastLogin.IsZero() return LastLogin.After(u.LastLogin) && !u.LastLogin.IsZero()
} }
// IsAdmin returns true if the user is an admin, false otherwise // HasAdminPower returns true if the user has admin or owner roles, false otherwise
func (u *User) IsAdmin() bool { func (u *User) HasAdminPower() bool {
return u.Role == UserRoleAdmin return u.Role == UserRoleAdmin || u.Role == UserRoleOwner
} }
// ToUserInfo converts a User object to a UserInfo object. // ToUserInfo converts a User object to a UserInfo object.
@ -193,6 +196,11 @@ func NewAdminUser(id string) *User {
return NewUser(id, UserRoleAdmin, false, false, "", []string{}, UserIssuedAPI) return NewUser(id, UserRoleAdmin, false, false, "", []string{}, UserIssuedAPI)
} }
// NewOwnerUser creates a new user with role UserRoleOwner
func NewOwnerUser(id string) *User {
return NewUser(id, UserRoleOwner, false, false, "", []string{}, UserIssuedAPI)
}
// createServiceUser creates a new service user under the given account. // createServiceUser creates a new service user under the given account.
func (am *DefaultAccountManager) createServiceUser(accountID string, initiatorUserID string, role UserRole, serviceUserName string, nonDeletable bool, autoGroups []string) (*UserInfo, error) { func (am *DefaultAccountManager) createServiceUser(accountID string, initiatorUserID string, role UserRole, serviceUserName string, nonDeletable bool, autoGroups []string) (*UserInfo, error) {
unlock := am.Store.AcquireAccountLock(accountID) unlock := am.Store.AcquireAccountLock(accountID)
@ -207,8 +215,12 @@ func (am *DefaultAccountManager) createServiceUser(accountID string, initiatorUs
if executingUser == nil { if executingUser == nil {
return nil, status.Errorf(status.NotFound, "user not found") return nil, status.Errorf(status.NotFound, "user not found")
} }
if executingUser.Role != UserRoleAdmin { if !executingUser.HasAdminPower() {
return nil, status.Errorf(status.PermissionDenied, "only admins can create service users") return nil, status.Errorf(status.PermissionDenied, "only users with admin power can create service users")
}
if role == UserRoleOwner {
return nil, status.Errorf(status.InvalidArgument, "can't create a service user with owner role")
} }
newUserID := uuid.New().String() newUserID := uuid.New().String()
@ -258,11 +270,15 @@ func (am *DefaultAccountManager) inviteNewUser(accountID, userID string, invite
return nil, fmt.Errorf("provided user update is nil") return nil, fmt.Errorf("provided user update is nil")
} }
invitedRole := StrRoleToUserRole(invite.Role)
switch { switch {
case invite.Name == "": case invite.Name == "":
return nil, status.Errorf(status.InvalidArgument, "name can't be empty") return nil, status.Errorf(status.InvalidArgument, "name can't be empty")
case invite.Email == "": case invite.Email == "":
return nil, status.Errorf(status.InvalidArgument, "email can't be empty") return nil, status.Errorf(status.InvalidArgument, "email can't be empty")
case invitedRole == UserRoleOwner:
return nil, status.Errorf(status.InvalidArgument, "can't invite a user with owner role")
default: default:
} }
@ -311,10 +327,9 @@ func (am *DefaultAccountManager) inviteNewUser(accountID, userID string, invite
return nil, err return nil, err
} }
role := StrRoleToUserRole(invite.Role)
newUser := &User{ newUser := &User{
Id: idpUser.ID, Id: idpUser.ID,
Role: role, Role: invitedRole,
AutoGroups: invite.AutoGroups, AutoGroups: invite.AutoGroups,
Issued: invite.Issued, Issued: invite.Issued,
IntegrationReference: invite.IntegrationReference, IntegrationReference: invite.IntegrationReference,
@ -416,8 +431,8 @@ func (am *DefaultAccountManager) DeleteUser(accountID, initiatorUserID string, t
if executingUser == nil { if executingUser == nil {
return status.Errorf(status.NotFound, "user not found") return status.Errorf(status.NotFound, "user not found")
} }
if executingUser.Role != UserRoleAdmin { if !executingUser.HasAdminPower() {
return status.Errorf(status.PermissionDenied, "only admins can delete users") return status.Errorf(status.PermissionDenied, "only users with admin power can delete users")
} }
targetUser := account.Users[targetUserID] targetUser := account.Users[targetUserID]
@ -425,9 +440,13 @@ func (am *DefaultAccountManager) DeleteUser(accountID, initiatorUserID string, t
return status.Errorf(status.NotFound, "target user not found") return status.Errorf(status.NotFound, "target user not found")
} }
if targetUser.Role == UserRoleOwner {
return status.Errorf(status.PermissionDenied, "unable to delete a user with owner role")
}
// disable deleting integration user if the initiator is not admin service user // disable deleting integration user if the initiator is not admin service user
if targetUser.Issued == UserIssuedIntegration && !executingUser.IsServiceUser { if targetUser.Issued == UserIssuedIntegration && !executingUser.IsServiceUser {
return status.Errorf(status.PermissionDenied, "only admin service user can delete this user") return status.Errorf(status.PermissionDenied, "only integration service user can delete this user")
} }
// handle service user first and exit, no need to fetch extra data from IDP, etc // handle service user first and exit, no need to fetch extra data from IDP, etc
@ -566,7 +585,7 @@ func (am *DefaultAccountManager) CreatePAT(accountID string, initiatorUserID str
return nil, status.Errorf(status.NotFound, "user not found") return nil, status.Errorf(status.NotFound, "user not found")
} }
if !(initiatorUserID == targetUserID || (executingUser.IsAdmin() && targetUser.IsServiceUser)) { if !(initiatorUserID == targetUserID || (executingUser.HasAdminPower() && targetUser.IsServiceUser)) {
return nil, status.Errorf(status.PermissionDenied, "no permission to create PAT for this user") return nil, status.Errorf(status.PermissionDenied, "no permission to create PAT for this user")
} }
@ -608,7 +627,7 @@ func (am *DefaultAccountManager) DeletePAT(accountID string, initiatorUserID str
return status.Errorf(status.NotFound, "user not found") return status.Errorf(status.NotFound, "user not found")
} }
if !(initiatorUserID == targetUserID || (executingUser.IsAdmin() && targetUser.IsServiceUser)) { if !(initiatorUserID == targetUserID || (executingUser.HasAdminPower() && targetUser.IsServiceUser)) {
return status.Errorf(status.PermissionDenied, "no permission to delete PAT for this user") return status.Errorf(status.PermissionDenied, "no permission to delete PAT for this user")
} }
@ -658,7 +677,7 @@ func (am *DefaultAccountManager) GetPAT(accountID string, initiatorUserID string
return nil, status.Errorf(status.NotFound, "user not found") return nil, status.Errorf(status.NotFound, "user not found")
} }
if !(initiatorUserID == targetUserID || (executingUser.IsAdmin() && targetUser.IsServiceUser)) { if !(initiatorUserID == targetUserID || (executingUser.HasAdminPower() && targetUser.IsServiceUser)) {
return nil, status.Errorf(status.PermissionDenied, "no permission to get PAT for this userser") return nil, status.Errorf(status.PermissionDenied, "no permission to get PAT for this userser")
} }
@ -690,7 +709,7 @@ func (am *DefaultAccountManager) GetAllPATs(accountID string, initiatorUserID st
return nil, status.Errorf(status.NotFound, "user not found") return nil, status.Errorf(status.NotFound, "user not found")
} }
if !(initiatorUserID == targetUserID || (executingUser.IsAdmin() && targetUser.IsServiceUser)) { if !(initiatorUserID == targetUserID || (executingUser.HasAdminPower() && targetUser.IsServiceUser)) {
return nil, status.Errorf(status.PermissionDenied, "no permission to get PAT for this user") return nil, status.Errorf(status.PermissionDenied, "no permission to get PAT for this user")
} }
@ -727,8 +746,8 @@ func (am *DefaultAccountManager) SaveOrAddUser(accountID, initiatorUserID string
return nil, err return nil, err
} }
if !initiatorUser.IsAdmin() || initiatorUser.IsBlocked() { if !initiatorUser.HasAdminPower() || initiatorUser.IsBlocked() {
return nil, status.Errorf(status.PermissionDenied, "only admins are authorized to perform user update operations") return nil, status.Errorf(status.PermissionDenied, "only users with admin power are authorized to perform user update operations")
} }
oldUser := account.Users[update.Id] oldUser := account.Users[update.Id]
@ -740,14 +759,38 @@ func (am *DefaultAccountManager) SaveOrAddUser(accountID, initiatorUserID string
oldUser = update oldUser = update
} }
if initiatorUser.IsAdmin() && initiatorUserID == update.Id && oldUser.Blocked != update.Blocked { if initiatorUser.HasAdminPower() && initiatorUserID == update.Id && oldUser.Blocked != update.Blocked {
return nil, status.Errorf(status.PermissionDenied, "admins can't block or unblock themselves") return nil, status.Errorf(status.PermissionDenied, "admins can't block or unblock themselves")
} }
if initiatorUser.IsAdmin() && initiatorUserID == update.Id && update.Role != UserRoleAdmin { if initiatorUser.HasAdminPower() && initiatorUserID == update.Id && update.Role != initiatorUser.Role {
return nil, status.Errorf(status.PermissionDenied, "admins can't change their role") return nil, status.Errorf(status.PermissionDenied, "admins can't change their role")
} }
if initiatorUser.Role == UserRoleAdmin && oldUser.Role == UserRoleOwner && update.Role != oldUser.Role {
return nil, status.Errorf(status.PermissionDenied, "only owners can remove owner role from their user")
}
if initiatorUser.Role == UserRoleAdmin && oldUser.Role == UserRoleOwner && update.IsBlocked() && !oldUser.IsBlocked() {
return nil, status.Errorf(status.PermissionDenied, "unable to block owner user")
}
if initiatorUser.Role == UserRoleAdmin && update.Role == UserRoleOwner && update.Role != oldUser.Role {
return nil, status.Errorf(status.PermissionDenied, "only owners can add owner role to other users")
}
if oldUser.IsServiceUser && update.Role == UserRoleOwner {
return nil, status.Errorf(status.PermissionDenied, "can't update a service user with owner role")
}
transferedOwnerRole := false
if initiatorUser.Role == UserRoleOwner && initiatorUserID != update.Id && update.Role == UserRoleOwner {
newInitiatorUser := initiatorUser.Copy()
newInitiatorUser.Role = UserRoleAdmin
account.Users[initiatorUserID] = newInitiatorUser
transferedOwnerRole = true
}
// only auto groups, revoked status, and integration reference can be updated for now // only auto groups, revoked status, and integration reference can be updated for now
newUser := oldUser.Copy() newUser := oldUser.Copy()
newUser.Role = update.Role newUser.Role = update.Role
@ -806,9 +849,12 @@ func (am *DefaultAccountManager) SaveOrAddUser(accountID, initiatorUserID string
} }
} }
// store activity logs switch {
if oldUser.Role != newUser.Role { case transferedOwnerRole:
am.StoreEvent(initiatorUserID, oldUser.Id, accountID, activity.TransferredOwnerRole, nil)
case oldUser.Role != newUser.Role:
am.StoreEvent(initiatorUserID, oldUser.Id, accountID, activity.UserRoleUpdated, map[string]any{"role": newUser.Role}) am.StoreEvent(initiatorUserID, oldUser.Id, accountID, activity.UserRoleUpdated, map[string]any{"role": newUser.Role})
default:
} }
if update.AutoGroups != nil { if update.AutoGroups != nil {
@ -882,7 +928,7 @@ func (am *DefaultAccountManager) GetOrCreateAccountByUser(userID, domain string)
userObj := account.Users[userID] userObj := account.Users[userID]
if account.Domain != lowerDomain && userObj.Role == UserRoleAdmin { if account.Domain != lowerDomain && userObj.Role == UserRoleOwner {
account.Domain = lowerDomain account.Domain = lowerDomain
err = am.Store.SaveAccount(account) err = am.Store.SaveAccount(account)
if err != nil { if err != nil {
@ -940,7 +986,7 @@ func (am *DefaultAccountManager) GetUsersFromAccount(accountID, userID string) (
// in case of self-hosted, or IDP doesn't return anything, we will return the locally stored userInfo // in case of self-hosted, or IDP doesn't return anything, we will return the locally stored userInfo
if len(queriedUsers) == 0 { if len(queriedUsers) == 0 {
for _, accountUser := range account.Users { for _, accountUser := range account.Users {
if !user.IsAdmin() && user.Id != accountUser.Id { if !user.HasAdminPower() && user.Id != accountUser.Id {
// if user is not an admin then show only current user and do not show other users // if user is not an admin then show only current user and do not show other users
continue continue
} }
@ -954,7 +1000,7 @@ func (am *DefaultAccountManager) GetUsersFromAccount(accountID, userID string) (
} }
for _, localUser := range account.Users { for _, localUser := range account.Users {
if !user.IsAdmin() && user.Id != localUser.Id { if !user.HasAdminPower() && user.Id != localUser.Id {
// if user is not an admin then show only current user and do not show other users // if user is not an admin then show only current user and do not show other users
continue continue
} }

View File

@ -348,6 +348,11 @@ func TestUser_CreateServiceUser(t *testing.T) {
assert.Zero(t, user.Email) assert.Zero(t, user.Email)
assert.True(t, user.IsServiceUser) assert.True(t, user.IsServiceUser)
assert.Equal(t, "active", user.Status) assert.Equal(t, "active", user.Status)
_, err = am.createServiceUser(mockAccountID, mockUserID, UserRoleOwner, mockServiceUserName, false, nil)
if err == nil {
t.Fatal("should return error when creating service user with owner role")
}
} }
func TestUser_CreateUser_ServiceUser(t *testing.T) { func TestUser_CreateUser_ServiceUser(t *testing.T) {
@ -412,6 +417,75 @@ func TestUser_CreateUser_RegularUser(t *testing.T) {
assert.Errorf(t, err, "Not configured IDP will throw error but right path used") assert.Errorf(t, err, "Not configured IDP will throw error but right path used")
} }
func TestUser_InviteNewUser(t *testing.T) {
store := newStore(t)
account := newAccountWithId(mockAccountID, mockUserID, "")
err := store.SaveAccount(account)
if err != nil {
t.Fatalf("Error when saving account: %s", err)
}
am := DefaultAccountManager{
Store: store,
eventStore: &activity.InMemoryEventStore{},
cacheLoading: map[string]chan struct{}{},
}
goCacheClient := gocache.New(CacheExpirationMax, 30*time.Minute)
goCacheStore := cacheStore.NewGoCache(goCacheClient)
am.cacheManager = cache.NewLoadable[[]*idp.UserData](am.loadAccount, cache.New[[]*idp.UserData](goCacheStore))
mockData := []*idp.UserData{
{
Email: "user@test.com",
Name: "user",
ID: mockUserID,
},
}
idpMock := idp.MockIDP{
CreateUserFunc: func(email, name, accountID, invitedByEmail string) (*idp.UserData, error) {
newData := &idp.UserData{
Email: email,
Name: name,
ID: "id",
}
mockData = append(mockData, newData)
return newData, nil
},
GetAccountFunc: func(accountId string) ([]*idp.UserData, error) {
return mockData, nil
},
}
am.idpManager = &idpMock
// test if new invite with regular role works
_, err = am.inviteNewUser(mockAccountID, mockUserID, &UserInfo{
Name: mockServiceUserName,
Role: mockRole,
Email: "test@teste.com",
IsServiceUser: false,
AutoGroups: []string{"group1", "group2"},
})
assert.NoErrorf(t, err, "Invite user should not throw error")
// test if new invite with owner role fails
_, err = am.inviteNewUser(mockAccountID, mockUserID, &UserInfo{
Name: mockServiceUserName,
Role: string(UserRoleOwner),
Email: "test2@teste.com",
IsServiceUser: false,
AutoGroups: []string{"group1", "group2"},
})
assert.Errorf(t, err, "Invite user with owner role should throw error")
}
func TestUser_DeleteUser_ServiceUser(t *testing.T) { func TestUser_DeleteUser_ServiceUser(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
@ -514,6 +588,14 @@ func TestUser_DeleteUser_regularUser(t *testing.T) {
Issued: UserIssuedIntegration, Issued: UserIssuedIntegration,
} }
targetId = "user5"
account.Users[targetId] = &User{
Id: targetId,
IsServiceUser: false,
Issued: UserIssuedAPI,
Role: UserRoleOwner,
}
err := store.SaveAccount(account) err := store.SaveAccount(account)
if err != nil { if err != nil {
t.Fatalf("Error when saving account: %s", err) t.Fatalf("Error when saving account: %s", err)
@ -546,6 +628,12 @@ func TestUser_DeleteUser_regularUser(t *testing.T) {
assertErrFunc: assert.Error, assertErrFunc: assert.Error,
assertErrMessage: "only admin service user can delete this user", assertErrMessage: "only admin service user can delete this user",
}, },
{
name: "Delete user with owner role should return permission denied ",
userID: "user5",
assertErrFunc: assert.Error,
assertErrMessage: "unable to delete a user with owner role",
},
} }
for _, testCase := range testCases { for _, testCase := range testCases {
@ -581,7 +669,7 @@ func TestDefaultAccountManager_GetUser(t *testing.T) {
} }
assert.Equal(t, mockUserID, user.Id) assert.Equal(t, mockUserID, user.Id)
assert.True(t, user.IsAdmin()) assert.True(t, user.HasAdminPower())
assert.False(t, user.IsBlocked()) assert.False(t, user.IsBlocked())
} }
@ -609,7 +697,7 @@ func TestDefaultAccountManager_ListUsers(t *testing.T) {
admins := 0 admins := 0
regular := 0 regular := 0
for _, user := range users { for _, user := range users {
if user.IsAdmin() { if user.HasAdminPower() {
admins++ admins++
continue continue
} }
@ -677,10 +765,10 @@ func TestDefaultAccountManager_ExternalCache(t *testing.T) {
func TestUser_IsAdmin(t *testing.T) { func TestUser_IsAdmin(t *testing.T) {
user := NewAdminUser(mockUserID) user := NewAdminUser(mockUserID)
assert.True(t, user.IsAdmin()) assert.True(t, user.HasAdminPower())
user = NewRegularUser(mockUserID) user = NewRegularUser(mockUserID)
assert.False(t, user.IsAdmin()) assert.False(t, user.HasAdminPower())
} }
func TestUser_GetUsersFromAccount_ForAdmin(t *testing.T) { func TestUser_GetUsersFromAccount_ForAdmin(t *testing.T) {
@ -746,26 +834,39 @@ func TestDefaultAccountManager_SaveUser(t *testing.T) {
} }
regularUserID := "regularUser" regularUserID := "regularUser"
serviceUserID := "serviceUser"
adminUserID := "adminUser"
ownerUserID := "ownerUser"
tt := []struct { tt := []struct {
name string name string
adminInitiator bool initiatorID string
update *User update *User
expectedErr bool expectedErr bool
}{ }{
{ {
name: "Should_Fail_To_Update_Admin_Role", name: "Should_Fail_To_Update_Admin_Role",
expectedErr: true, expectedErr: true,
adminInitiator: true, initiatorID: adminUserID,
update: &User{ update: &User{
Id: userID, Id: adminUserID,
Role: UserRoleUser, Role: UserRoleUser,
Blocked: false, Blocked: false,
}, },
}, { }, {
name: "Should_Fail_When_Admin_Blocks_Themselves", name: "Should_Fail_When_Admin_Blocks_Themselves",
expectedErr: true, expectedErr: true,
adminInitiator: true, initiatorID: adminUserID,
update: &User{
Id: adminUserID,
Role: UserRoleAdmin,
Blocked: true,
},
},
{
name: "Should_Fail_To_Update_Non_Existing_User",
expectedErr: true,
initiatorID: adminUserID,
update: &User{ update: &User{
Id: userID, Id: userID,
Role: UserRoleAdmin, Role: UserRoleAdmin,
@ -773,66 +874,125 @@ func TestDefaultAccountManager_SaveUser(t *testing.T) {
}, },
}, },
{ {
name: "Should_Fail_To_Update_Non_Existing_User", name: "Should_Fail_To_Update_When_Initiator_Is_Not_An_Admin",
expectedErr: true, expectedErr: true,
adminInitiator: true, initiatorID: regularUserID,
update: &User{ update: &User{
Id: userID, Id: adminUserID,
Role: UserRoleAdmin, Role: UserRoleAdmin,
Blocked: true, Blocked: true,
}, },
}, },
{ {
name: "Should_Fail_To_Update_When_Initiator_Is_Not_An_Admin", name: "Should_Update_User",
expectedErr: true, expectedErr: false,
adminInitiator: false, initiatorID: adminUserID,
update: &User{
Id: userID,
Role: UserRoleAdmin,
Blocked: true,
},
},
{
name: "Should_Update_User",
expectedErr: false,
adminInitiator: true,
update: &User{ update: &User{
Id: regularUserID, Id: regularUserID,
Role: UserRoleAdmin, Role: UserRoleAdmin,
Blocked: true, Blocked: true,
}, },
}, },
{
name: "Should_Transfer_Owner_Role_To_User",
expectedErr: false,
initiatorID: ownerUserID,
update: &User{
Id: adminUserID,
Role: UserRoleAdmin,
Blocked: false,
},
},
{
name: "Should_Fail_To_Transfer_Owner_Role_To_Service_User",
expectedErr: true,
initiatorID: ownerUserID,
update: &User{
Id: serviceUserID,
Role: UserRoleOwner,
Blocked: false,
},
},
{
name: "Should_Fail_To_Update_Owner_User_Role_By_Admin",
expectedErr: true,
initiatorID: adminUserID,
update: &User{
Id: ownerUserID,
Role: UserRoleAdmin,
Blocked: false,
},
},
{
name: "Should_Fail_To_Update_Owner_User_Role_By_User",
expectedErr: true,
initiatorID: regularUserID,
update: &User{
Id: ownerUserID,
Role: UserRoleAdmin,
Blocked: false,
},
},
{
name: "Should_Fail_To_Update_Owner_User_Role_By_Service_User",
expectedErr: true,
initiatorID: serviceUserID,
update: &User{
Id: ownerUserID,
Role: UserRoleAdmin,
Blocked: false,
},
},
{
name: "Should_Fail_To_Update_Owner_Role_By_Admin",
expectedErr: true,
initiatorID: adminUserID,
update: &User{
Id: regularUserID,
Role: UserRoleOwner,
Blocked: false,
},
},
{
name: "Should_Fail_To_Block_Owner_Role_By_Admin",
expectedErr: true,
initiatorID: adminUserID,
update: &User{
Id: ownerUserID,
Role: UserRoleOwner,
Blocked: true,
},
},
} }
for _, tc := range tt { for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
// create an account and an admin user // create an account and an admin user
account, err := manager.GetOrCreateAccountByUser(userID, "netbird.io") account, err := manager.GetOrCreateAccountByUser(ownerUserID, "netbird.io")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
// create a regular user // create other users
account.Users[regularUserID] = NewRegularUser(regularUserID) account.Users[regularUserID] = NewRegularUser(regularUserID)
err = manager.Store.SaveAccount(account) account.Users[adminUserID] = NewAdminUser(adminUserID)
if err != nil { account.Users[serviceUserID] = &User{IsServiceUser: true, Id: serviceUserID, Role: UserRoleAdmin, ServiceUserName: "service"}
t.Fatal(err) err = manager.Store.SaveAccount(account)
} if err != nil {
t.Fatal(err)
}
initiatorID := userID updated, err := manager.SaveUser(account.Id, tc.initiatorID, tc.update)
if !tc.adminInitiator { if tc.expectedErr {
initiatorID = regularUserID require.Errorf(t, err, "expecting SaveUser to throw an error")
} } else {
require.NoError(t, err, "expecting SaveUser not to throw an error")
assert.NotNil(t, updated)
updated, err := manager.SaveUser(account.Id, initiatorID, tc.update) assert.Equal(t, string(tc.update.Role), updated.Role)
if tc.expectedErr { assert.Equal(t, tc.update.IsBlocked(), updated.IsBlocked)
require.Errorf(t, err, "expecting SaveUser to throw an error") }
} else { })
require.NoError(t, err, "expecting SaveUser not to throw an error")
assert.NotNil(t, updated)
assert.Equal(t, string(tc.update.Role), updated.Role)
assert.Equal(t, tc.update.IsBlocked(), updated.IsBlocked)
}
} }
} }