[management] user info with role permissions (#3728)

This commit is contained in:
Pedro Maia Costa 2025-05-01 11:24:55 +01:00 committed by GitHub
parent 9bc7d788f0
commit 7b64953eed
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 446 additions and 291 deletions

View File

@ -30,9 +30,6 @@ var (
Issued: ptr("api"), Issued: ptr("api"),
LastLogin: &time.Time{}, LastLogin: &time.Time{},
Name: "M. Essam", Name: "M. Essam",
Permissions: &api.UserPermissions{
DashboardView: ptr(api.UserPermissionsDashboardViewFull),
},
Role: "user", Role: "user",
Status: api.UserStatusActive, Status: api.UserStatusActive,
} }

View File

@ -16,6 +16,7 @@ import (
"github.com/netbirdio/netbird/management/server/posture" "github.com/netbirdio/netbird/management/server/posture"
"github.com/netbirdio/netbird/management/server/store" "github.com/netbirdio/netbird/management/server/store"
"github.com/netbirdio/netbird/management/server/types" "github.com/netbirdio/netbird/management/server/types"
"github.com/netbirdio/netbird/management/server/users"
"github.com/netbirdio/netbird/route" "github.com/netbirdio/netbird/route"
) )
@ -115,5 +116,5 @@ type Manager interface {
CreateAccountByPrivateDomain(ctx context.Context, initiatorId, domain string) (*types.Account, error) CreateAccountByPrivateDomain(ctx context.Context, initiatorId, domain string) (*types.Account, error)
UpdateToPrimaryAccount(ctx context.Context, accountId string) (*types.Account, error) UpdateToPrimaryAccount(ctx context.Context, accountId string) (*types.Account, error)
GetOwnerInfo(ctx context.Context, accountId string) (*types.UserInfo, error) GetOwnerInfo(ctx context.Context, accountId string) (*types.UserInfo, error)
GetCurrentUserInfo(ctx context.Context, accountID, userID string) (*types.UserInfo, error) GetCurrentUserInfo(ctx context.Context, userAuth nbcontext.UserAuth) (*users.UserInfoWithPermissions, error)
} }

View File

@ -216,11 +216,25 @@ components:
UserPermissions: UserPermissions:
type: object type: object
properties: properties:
dashboard_view: is_restricted:
description: User's permission to view the dashboard type: boolean
description: Indicates whether this User's Peers view is restricted
modules:
type: object
additionalProperties:
type: object
additionalProperties:
type: boolean
propertyNames:
type: string type: string
enum: [ "limited", "blocked", "full" ] description: The operation type
example: limited propertyNames:
type: string
description: The module name
example: {"networks": { "read": true, "create": false, "update": false, "delete": false}, "peers": { "read": false, "create": false, "update": false, "delete": false} }
required:
- modules
- is_restricted
UserRequest: UserRequest:
type: object type: object
properties: properties:

View File

@ -178,13 +178,6 @@ const (
UserStatusInvited UserStatus = "invited" UserStatusInvited UserStatus = "invited"
) )
// Defines values for UserPermissionsDashboardView.
const (
UserPermissionsDashboardViewBlocked UserPermissionsDashboardView = "blocked"
UserPermissionsDashboardViewFull UserPermissionsDashboardView = "full"
UserPermissionsDashboardViewLimited UserPermissionsDashboardView = "limited"
)
// Defines values for GetApiEventsNetworkTrafficParamsType. // Defines values for GetApiEventsNetworkTrafficParamsType.
const ( const (
GetApiEventsNetworkTrafficParamsTypeTYPEDROP GetApiEventsNetworkTrafficParamsType = "TYPE_DROP" GetApiEventsNetworkTrafficParamsTypeTYPEDROP GetApiEventsNetworkTrafficParamsType = "TYPE_DROP"
@ -1757,13 +1750,11 @@ type UserCreateRequest struct {
// UserPermissions defines model for UserPermissions. // UserPermissions defines model for UserPermissions.
type UserPermissions struct { type UserPermissions struct {
// DashboardView User's permission to view the dashboard // IsRestricted Indicates whether this User's Peers view is restricted
DashboardView *UserPermissionsDashboardView `json:"dashboard_view,omitempty"` IsRestricted bool `json:"is_restricted"`
Modules map[string]map[string]bool `json:"modules"`
} }
// UserPermissionsDashboardView User's permission to view the dashboard
type UserPermissionsDashboardView string
// UserRequest defines model for UserRequest. // UserRequest defines model for UserRequest.
type UserRequest struct { type UserRequest struct {
// AutoGroups Group IDs to auto-assign to peers registered by this user // AutoGroups Group IDs to auto-assign to peers registered by this user

View File

@ -13,6 +13,7 @@ import (
"github.com/netbirdio/netbird/management/server/http/util" "github.com/netbirdio/netbird/management/server/http/util"
"github.com/netbirdio/netbird/management/server/status" "github.com/netbirdio/netbird/management/server/status"
"github.com/netbirdio/netbird/management/server/types" "github.com/netbirdio/netbird/management/server/types"
"github.com/netbirdio/netbird/management/server/users"
nbcontext "github.com/netbirdio/netbird/management/server/context" nbcontext "github.com/netbirdio/netbird/management/server/context"
) )
@ -272,15 +273,33 @@ func (h *handler) getCurrentUser(w http.ResponseWriter, r *http.Request) {
return return
} }
accountID, userID := userAuth.AccountId, userAuth.UserId user, err := h.accountManager.GetCurrentUserInfo(ctx, userAuth)
user, err := h.accountManager.GetCurrentUserInfo(ctx, accountID, userID)
if err != nil { if err != nil {
util.WriteError(r.Context(), err, w) util.WriteError(r.Context(), err, w)
return return
} }
util.WriteJSONObject(r.Context(), w, toUserResponse(user, userID)) util.WriteJSONObject(r.Context(), w, toUserWithPermissionsResponse(user, userAuth.UserId))
}
func toUserWithPermissionsResponse(user *users.UserInfoWithPermissions, userID string) *api.User {
response := toUserResponse(user.UserInfo, userID)
// stringify modules and operations keys
modules := make(map[string]map[string]bool)
for module, operations := range user.Permissions {
modules[string(module)] = make(map[string]bool)
for op, val := range operations {
modules[string(module)][string(op)] = val
}
}
response.Permissions = &api.UserPermissions{
IsRestricted: user.Restricted,
Modules: modules,
}
return response
} }
func toUserResponse(user *types.UserInfo, currenUserID string) *api.User { func toUserResponse(user *types.UserInfo, currenUserID string) *api.User {
@ -316,8 +335,5 @@ func toUserResponse(user *types.UserInfo, currenUserID string) *api.User {
IsBlocked: user.IsBlocked, IsBlocked: user.IsBlocked,
LastLogin: &user.LastLogin, LastLogin: &user.LastLogin,
Issued: &user.Issued, Issued: &user.Issued,
Permissions: &api.UserPermissions{
DashboardView: (*api.UserPermissionsDashboardView)(&user.Permissions.DashboardView),
},
} }
} }

View File

@ -13,12 +13,16 @@ import (
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
nbcontext "github.com/netbirdio/netbird/management/server/context" nbcontext "github.com/netbirdio/netbird/management/server/context"
"github.com/netbirdio/netbird/management/server/http/api" "github.com/netbirdio/netbird/management/server/http/api"
"github.com/netbirdio/netbird/management/server/mock_server" "github.com/netbirdio/netbird/management/server/mock_server"
"github.com/netbirdio/netbird/management/server/permissions/modules"
"github.com/netbirdio/netbird/management/server/permissions/roles"
"github.com/netbirdio/netbird/management/server/status" "github.com/netbirdio/netbird/management/server/status"
"github.com/netbirdio/netbird/management/server/types" "github.com/netbirdio/netbird/management/server/types"
"github.com/netbirdio/netbird/management/server/users"
) )
const ( const (
@ -107,7 +111,7 @@ func initUsersTestData() *handler {
return nil, status.Errorf(status.NotFound, "user with ID %s does not exists", userID) return nil, status.Errorf(status.NotFound, "user with ID %s does not exists", userID)
} }
info, err := update.Copy().ToUserInfo(nil, &types.Settings{RegularUsersViewBlocked: false}) info, err := update.Copy().ToUserInfo(nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -124,8 +128,8 @@ func initUsersTestData() *handler {
return nil return nil
}, },
GetCurrentUserInfoFunc: func(ctx context.Context, accountID, userID string) (*types.UserInfo, error) { GetCurrentUserInfoFunc: func(ctx context.Context, userAuth nbcontext.UserAuth) (*users.UserInfoWithPermissions, error) {
switch userID { switch userAuth.UserId {
case "not-found": case "not-found":
return nil, status.NewUserNotFoundError("not-found") return nil, status.NewUserNotFoundError("not-found")
case "not-of-account": case "not-of-account":
@ -135,7 +139,8 @@ func initUsersTestData() *handler {
case "service-user": case "service-user":
return nil, status.NewPermissionDeniedError() return nil, status.NewPermissionDeniedError()
case "owner": case "owner":
return &types.UserInfo{ return &users.UserInfoWithPermissions{
UserInfo: &types.UserInfo{
ID: "owner", ID: "owner",
Name: "", Name: "",
Role: "owner", Role: "owner",
@ -144,12 +149,12 @@ func initUsersTestData() *handler {
IsBlocked: false, IsBlocked: false,
NonDeletable: false, NonDeletable: false,
Issued: "api", Issued: "api",
Permissions: types.UserPermissions{
DashboardView: "full",
}, },
Permissions: mergeRolePermissions(roles.Owner),
}, nil }, nil
case "regular-user": case "regular-user":
return &types.UserInfo{ return &users.UserInfoWithPermissions{
UserInfo: &types.UserInfo{
ID: "regular-user", ID: "regular-user",
Name: "", Name: "",
Role: "user", Role: "user",
@ -158,13 +163,13 @@ func initUsersTestData() *handler {
IsBlocked: false, IsBlocked: false,
NonDeletable: false, NonDeletable: false,
Issued: "api", Issued: "api",
Permissions: types.UserPermissions{
DashboardView: "limited",
}, },
Permissions: mergeRolePermissions(roles.User),
}, nil }, nil
case "admin-user": case "admin-user":
return &types.UserInfo{ return &users.UserInfoWithPermissions{
UserInfo: &types.UserInfo{
ID: "admin-user", ID: "admin-user",
Name: "", Name: "",
Role: "admin", Role: "admin",
@ -174,13 +179,28 @@ func initUsersTestData() *handler {
NonDeletable: false, NonDeletable: false,
LastLogin: time.Time{}, LastLogin: time.Time{},
Issued: "api", Issued: "api",
Permissions: types.UserPermissions{
DashboardView: "full",
}, },
Permissions: mergeRolePermissions(roles.Admin),
}, nil
case "restricted-user":
return &users.UserInfoWithPermissions{
UserInfo: &types.UserInfo{
ID: "restricted-user",
Name: "",
Role: "user",
Status: "active",
IsServiceUser: false,
IsBlocked: false,
NonDeletable: false,
LastLogin: time.Time{},
Issued: "api",
},
Permissions: mergeRolePermissions(roles.User),
Restricted: true,
}, nil }, nil
} }
return nil, fmt.Errorf("user id %s not handled", userID) return nil, fmt.Errorf("user id %s not handled", userAuth.UserId)
}, },
}, },
} }
@ -546,6 +566,7 @@ func TestCurrentUser(t *testing.T) {
name string name string
expectedStatus int expectedStatus int
requestAuth nbcontext.UserAuth requestAuth nbcontext.UserAuth
expectedResult *api.User
}{ }{
{ {
name: "without auth", name: "without auth",
@ -575,16 +596,78 @@ func TestCurrentUser(t *testing.T) {
name: "owner", name: "owner",
requestAuth: nbcontext.UserAuth{UserId: "owner"}, requestAuth: nbcontext.UserAuth{UserId: "owner"},
expectedStatus: http.StatusOK, expectedStatus: http.StatusOK,
expectedResult: &api.User{
Id: "owner",
Role: "owner",
Status: "active",
IsBlocked: false,
IsCurrent: ptr(true),
IsServiceUser: ptr(false),
AutoGroups: []string{},
Issued: ptr("api"),
LastLogin: ptr(time.Time{}),
Permissions: &api.UserPermissions{
Modules: stringifyPermissionsKeys(mergeRolePermissions(roles.Owner)),
},
},
}, },
{ {
name: "regular user", name: "regular user",
requestAuth: nbcontext.UserAuth{UserId: "regular-user"}, requestAuth: nbcontext.UserAuth{UserId: "regular-user"},
expectedStatus: http.StatusOK, expectedStatus: http.StatusOK,
expectedResult: &api.User{
Id: "regular-user",
Role: "user",
Status: "active",
IsBlocked: false,
IsCurrent: ptr(true),
IsServiceUser: ptr(false),
AutoGroups: []string{},
Issued: ptr("api"),
LastLogin: ptr(time.Time{}),
Permissions: &api.UserPermissions{
Modules: stringifyPermissionsKeys(mergeRolePermissions(roles.User)),
},
},
}, },
{ {
name: "admin user", name: "admin user",
requestAuth: nbcontext.UserAuth{UserId: "admin-user"}, requestAuth: nbcontext.UserAuth{UserId: "admin-user"},
expectedStatus: http.StatusOK, expectedStatus: http.StatusOK,
expectedResult: &api.User{
Id: "admin-user",
Role: "admin",
Status: "active",
IsBlocked: false,
IsCurrent: ptr(true),
IsServiceUser: ptr(false),
AutoGroups: []string{},
Issued: ptr("api"),
LastLogin: ptr(time.Time{}),
Permissions: &api.UserPermissions{
Modules: stringifyPermissionsKeys(mergeRolePermissions(roles.Admin)),
},
},
},
{
name: "restricted user",
requestAuth: nbcontext.UserAuth{UserId: "restricted-user"},
expectedStatus: http.StatusOK,
expectedResult: &api.User{
Id: "restricted-user",
Role: "user",
Status: "active",
IsBlocked: false,
IsCurrent: ptr(true),
IsServiceUser: ptr(false),
AutoGroups: []string{},
Issued: ptr("api"),
LastLogin: ptr(time.Time{}),
Permissions: &api.UserPermissions{
IsRestricted: true,
Modules: stringifyPermissionsKeys(mergeRolePermissions(roles.User)),
},
},
}, },
} }
@ -603,10 +686,42 @@ func TestCurrentUser(t *testing.T) {
res := rr.Result() res := rr.Result()
defer res.Body.Close() defer res.Body.Close()
if status := rr.Code; status != tc.expectedStatus { assert.Equal(t, tc.expectedStatus, rr.Code, "handler returned wrong status code")
t.Fatalf("handler returned wrong status code: got %v want %v",
status, tc.expectedStatus) if tc.expectedResult != nil {
var result api.User
require.NoError(t, json.NewDecoder(res.Body).Decode(&result))
assert.EqualValues(t, *tc.expectedResult, result)
} }
}) })
} }
} }
func ptr[T any, PT *T](x T) PT {
return &x
}
func mergeRolePermissions(role roles.RolePermissions) roles.Permissions {
permissions := roles.Permissions{}
for k := range modules.All {
if rolePermissions, ok := role.Permissions[k]; ok {
permissions[k] = rolePermissions
continue
}
permissions[k] = role.AutoAllowNew
}
return permissions
}
func stringifyPermissionsKeys(permissions roles.Permissions) map[string]map[string]bool {
modules := make(map[string]map[string]bool)
for module, operations := range permissions {
modules[string(module)] = make(map[string]bool)
for op, val := range operations {
modules[string(module)][string(op)] = val
}
}
return modules
}

View File

@ -19,6 +19,7 @@ import (
"github.com/netbirdio/netbird/management/server/posture" "github.com/netbirdio/netbird/management/server/posture"
"github.com/netbirdio/netbird/management/server/store" "github.com/netbirdio/netbird/management/server/store"
"github.com/netbirdio/netbird/management/server/types" "github.com/netbirdio/netbird/management/server/types"
"github.com/netbirdio/netbird/management/server/users"
"github.com/netbirdio/netbird/route" "github.com/netbirdio/netbird/route"
) )
@ -115,7 +116,7 @@ type MockAccountManager struct {
CreateAccountByPrivateDomainFunc func(ctx context.Context, initiatorId, domain string) (*types.Account, error) CreateAccountByPrivateDomainFunc func(ctx context.Context, initiatorId, domain string) (*types.Account, error)
UpdateToPrimaryAccountFunc func(ctx context.Context, accountId string) (*types.Account, error) UpdateToPrimaryAccountFunc func(ctx context.Context, accountId string) (*types.Account, error)
GetOwnerInfoFunc func(ctx context.Context, accountID string) (*types.UserInfo, error) GetOwnerInfoFunc func(ctx context.Context, accountID string) (*types.UserInfo, error)
GetCurrentUserInfoFunc func(ctx context.Context, accountID, userID string) (*types.UserInfo, error) GetCurrentUserInfoFunc func(ctx context.Context, userAuth nbcontext.UserAuth) (*users.UserInfoWithPermissions, error)
GetAccountMetaFunc func(ctx context.Context, accountID, userID string) (*types.AccountMeta, error) GetAccountMetaFunc func(ctx context.Context, accountID, userID string) (*types.AccountMeta, error)
} }
@ -882,9 +883,9 @@ func (am *MockAccountManager) GetOwnerInfo(ctx context.Context, accountId string
return nil, status.Errorf(codes.Unimplemented, "method GetOwnerInfo is not implemented") return nil, status.Errorf(codes.Unimplemented, "method GetOwnerInfo is not implemented")
} }
func (am *MockAccountManager) GetCurrentUserInfo(ctx context.Context, accountID, userID string) (*types.UserInfo, error) { func (am *MockAccountManager) GetCurrentUserInfo(ctx context.Context, userAuth nbcontext.UserAuth) (*users.UserInfoWithPermissions, error) {
if am.GetCurrentUserInfoFunc != nil { if am.GetCurrentUserInfoFunc != nil {
return am.GetCurrentUserInfoFunc(ctx, accountID, userID) return am.GetCurrentUserInfoFunc(ctx, userAuth)
} }
return nil, status.Errorf(codes.Unimplemented, "method GetCurrentUserInfo is not implemented") return nil, status.Errorf(codes.Unimplemented, "method GetCurrentUserInfo is not implemented")
} }

View File

@ -59,7 +59,7 @@ func (am *DefaultAccountManager) GetPeers(ctx context.Context, accountID, userID
return nil, fmt.Errorf("failed to get account settings: %w", err) return nil, fmt.Errorf("failed to get account settings: %w", err)
} }
if settings.RegularUsersViewBlocked { if user.IsRestrictable() && settings.RegularUsersViewBlocked {
return []*nbpeer.Peer{}, nil return []*nbpeer.Peer{}, nil
} }

View File

@ -20,6 +20,8 @@ type Manager interface {
ValidateUserPermissions(ctx context.Context, accountID, userID string, module modules.Module, operation operations.Operation) (bool, error) ValidateUserPermissions(ctx context.Context, accountID, userID string, module modules.Module, operation operations.Operation) (bool, error)
ValidateRoleModuleAccess(ctx context.Context, accountID string, role roles.RolePermissions, module modules.Module, operation operations.Operation) bool ValidateRoleModuleAccess(ctx context.Context, accountID string, role roles.RolePermissions, module modules.Module, operation operations.Operation) bool
ValidateAccountAccess(ctx context.Context, accountID string, user *types.User, allowOwnerAndAdmin bool) error ValidateAccountAccess(ctx context.Context, accountID string, user *types.User, allowOwnerAndAdmin bool) error
GetPermissionsByRole(ctx context.Context, role types.UserRole) (roles.Permissions, error)
} }
type managerImpl struct { type managerImpl struct {
@ -96,3 +98,22 @@ func (m *managerImpl) ValidateAccountAccess(ctx context.Context, accountID strin
} }
return nil return nil
} }
func (m *managerImpl) GetPermissionsByRole(ctx context.Context, role types.UserRole) (roles.Permissions, error) {
roleMap, ok := roles.RolesMap[role]
if !ok {
return roles.Permissions{}, status.NewUserRoleNotFoundError(string(role))
}
permissions := roles.Permissions{}
for k := range modules.All {
if rolePermissions, ok := roleMap.Permissions[k]; ok {
permissions[k] = rolePermissions
continue
}
permissions[k] = roleMap.AutoAllowNew
}
return permissions, nil
}

View File

@ -38,6 +38,21 @@ func (m *MockManager) EXPECT() *MockManagerMockRecorder {
return m.recorder return m.recorder
} }
// GetPermissionsByRole mocks base method.
func (m *MockManager) GetPermissionsByRole(ctx context.Context, role types.UserRole) (roles.Permissions, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetPermissionsByRole", ctx, role)
ret0, _ := ret[0].(roles.Permissions)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetPermissionsByRole indicates an expected call of GetPermissionsByRole.
func (mr *MockManagerMockRecorder) GetPermissionsByRole(ctx, role interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPermissionsByRole", reflect.TypeOf((*MockManager)(nil).GetPermissionsByRole), ctx, role)
}
// ValidateAccountAccess mocks base method. // ValidateAccountAccess mocks base method.
func (m *MockManager) ValidateAccountAccess(ctx context.Context, accountID string, user *types.User, allowOwnerAndAdmin bool) error { func (m *MockManager) ValidateAccountAccess(ctx context.Context, accountID string, user *types.User, allowOwnerAndAdmin bool) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()

View File

@ -17,3 +17,19 @@ const (
SetupKeys Module = "setup_keys" SetupKeys Module = "setup_keys"
Pats Module = "pats" Pats Module = "pats"
) )
var All = map[Module]struct{}{
Networks: {},
Peers: {},
Groups: {},
Settings: {},
Accounts: {},
Dns: {},
Nameservers: {},
Events: {},
Policies: {},
Routes: {},
Users: {},
SetupKeys: {},
Pats: {},
}

View File

@ -23,9 +23,9 @@ var NetworkAdmin = RolePermissions{
}, },
modules.Groups: { modules.Groups: {
operations.Read: true, operations.Read: true,
operations.Create: false, operations.Create: true,
operations.Update: false, operations.Update: true,
operations.Delete: false, operations.Delete: true,
}, },
modules.Settings: { modules.Settings: {
operations.Read: true, operations.Read: true,
@ -87,5 +87,11 @@ var NetworkAdmin = RolePermissions{
operations.Update: true, operations.Update: true,
operations.Delete: true, operations.Delete: true,
}, },
modules.Peers: {
operations.Read: true,
operations.Create: false,
operations.Update: false,
operations.Delete: false,
},
}, },
} }

View File

@ -65,11 +65,6 @@ type UserInfo struct {
LastLogin time.Time `json:"last_login"` LastLogin time.Time `json:"last_login"`
Issued string `json:"issued"` Issued string `json:"issued"`
IntegrationReference integration_reference.IntegrationReference `json:"-"` IntegrationReference integration_reference.IntegrationReference `json:"-"`
Permissions UserPermissions `json:"permissions"`
}
type UserPermissions struct {
DashboardView string `json:"dashboard_view"`
} }
// User represents a user of the system // User represents a user of the system
@ -132,21 +127,18 @@ func (u *User) IsRegularUser() bool {
return !u.HasAdminPower() && !u.IsServiceUser return !u.HasAdminPower() && !u.IsServiceUser
} }
// IsRestrictable checks whether a user is in a restrictable role.
func (u *User) IsRestrictable() bool {
return u.Role == UserRoleUser || u.Role == UserRoleBillingAdmin
}
// ToUserInfo converts a User object to a UserInfo object. // ToUserInfo converts a User object to a UserInfo object.
func (u *User) ToUserInfo(userData *idp.UserData, settings *Settings) (*UserInfo, error) { func (u *User) ToUserInfo(userData *idp.UserData) (*UserInfo, error) {
autoGroups := u.AutoGroups autoGroups := u.AutoGroups
if autoGroups == nil { if autoGroups == nil {
autoGroups = []string{} autoGroups = []string{}
} }
dashboardViewPermissions := "full"
if !u.HasAdminPower() {
dashboardViewPermissions = "limited"
if settings.RegularUsersViewBlocked {
dashboardViewPermissions = "blocked"
}
}
if userData == nil { if userData == nil {
return &UserInfo{ return &UserInfo{
ID: u.Id, ID: u.Id,
@ -159,9 +151,6 @@ func (u *User) ToUserInfo(userData *idp.UserData, settings *Settings) (*UserInfo
IsBlocked: u.Blocked, IsBlocked: u.Blocked,
LastLogin: u.GetLastLogin(), LastLogin: u.GetLastLogin(),
Issued: u.Issued, Issued: u.Issued,
Permissions: UserPermissions{
DashboardView: dashboardViewPermissions,
},
}, nil }, nil
} }
if userData.ID != u.Id { if userData.ID != u.Id {
@ -184,9 +173,6 @@ func (u *User) ToUserInfo(userData *idp.UserData, settings *Settings) (*UserInfo
IsBlocked: u.Blocked, IsBlocked: u.Blocked,
LastLogin: u.GetLastLogin(), LastLogin: u.GetLastLogin(),
Issued: u.Issued, Issued: u.Issued,
Permissions: UserPermissions{
DashboardView: dashboardViewPermissions,
},
}, nil }, nil
} }

View File

@ -12,6 +12,7 @@ import (
"github.com/netbirdio/netbird/management/server/activity" "github.com/netbirdio/netbird/management/server/activity"
nbContext "github.com/netbirdio/netbird/management/server/context" nbContext "github.com/netbirdio/netbird/management/server/context"
nbcontext "github.com/netbirdio/netbird/management/server/context"
"github.com/netbirdio/netbird/management/server/idp" "github.com/netbirdio/netbird/management/server/idp"
nbpeer "github.com/netbirdio/netbird/management/server/peer" nbpeer "github.com/netbirdio/netbird/management/server/peer"
"github.com/netbirdio/netbird/management/server/permissions/modules" "github.com/netbirdio/netbird/management/server/permissions/modules"
@ -19,6 +20,7 @@ import (
"github.com/netbirdio/netbird/management/server/status" "github.com/netbirdio/netbird/management/server/status"
"github.com/netbirdio/netbird/management/server/store" "github.com/netbirdio/netbird/management/server/store"
"github.com/netbirdio/netbird/management/server/types" "github.com/netbirdio/netbird/management/server/types"
"github.com/netbirdio/netbird/management/server/users"
"github.com/netbirdio/netbird/management/server/util" "github.com/netbirdio/netbird/management/server/util"
) )
@ -122,11 +124,6 @@ func (am *DefaultAccountManager) inviteNewUser(ctx context.Context, accountID, u
CreatedAt: time.Now().UTC(), CreatedAt: time.Now().UTC(),
} }
settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID)
if err != nil {
return nil, err
}
if err = am.Store.SaveUser(ctx, store.LockingStrengthUpdate, newUser); err != nil { if err = am.Store.SaveUser(ctx, store.LockingStrengthUpdate, newUser); err != nil {
return nil, err return nil, err
} }
@ -138,7 +135,7 @@ func (am *DefaultAccountManager) inviteNewUser(ctx context.Context, accountID, u
am.StoreEvent(ctx, userID, newUser.Id, accountID, activity.UserInvited, nil) am.StoreEvent(ctx, userID, newUser.Id, accountID, activity.UserInvited, nil)
return newUser.ToUserInfo(idpUser, settings) return newUser.ToUserInfo(idpUser)
} }
// createNewIdpUser validates the invite and creates a new user in the IdP // createNewIdpUser validates the invite and creates a new user in the IdP
@ -360,6 +357,7 @@ func (am *DefaultAccountManager) CreatePAT(ctx context.Context, accountID string
return nil, err return nil, err
} }
// @note this is essential to prevent non admin users with Pats create permission frpm creating one for a service user
if initiatorUserID != targetUserID && !(initiatorUser.HasAdminPower() && targetUser.IsServiceUser) { if initiatorUserID != targetUserID && !(initiatorUser.HasAdminPower() && targetUser.IsServiceUser) {
return nil, status.NewAdminPermissionError() return nil, status.NewAdminPermissionError()
} }
@ -727,19 +725,14 @@ func handleOwnerRoleTransfer(ctx context.Context, transaction store.Store, initi
// If the AccountManager has a non-nil idpManager and the User is not a service user, // If the AccountManager has a non-nil idpManager and the User is not a service user,
// it will attempt to look up the UserData from the cache. // it will attempt to look up the UserData from the cache.
func (am *DefaultAccountManager) getUserInfo(ctx context.Context, user *types.User, accountID string) (*types.UserInfo, error) { func (am *DefaultAccountManager) getUserInfo(ctx context.Context, user *types.User, accountID string) (*types.UserInfo, error) {
settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID)
if err != nil {
return nil, err
}
if !isNil(am.idpManager) && !user.IsServiceUser { if !isNil(am.idpManager) && !user.IsServiceUser {
userData, err := am.lookupUserInCache(ctx, user.Id, accountID) userData, err := am.lookupUserInCache(ctx, user.Id, accountID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return user.ToUserInfo(userData, settings) return user.ToUserInfo(userData)
} }
return user.ToUserInfo(nil, settings) return user.ToUserInfo(nil)
} }
// validateUserUpdate validates the update operation for a user. // validateUserUpdate validates the update operation for a user.
@ -879,17 +872,12 @@ func (am *DefaultAccountManager) BuildUserInfosForAccount(ctx context.Context, a
queriedUsers = append(queriedUsers, usersFromIntegration...) queriedUsers = append(queriedUsers, usersFromIntegration...)
} }
settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID)
if err != nil {
return nil, err
}
userInfosMap := make(map[string]*types.UserInfo) userInfosMap := make(map[string]*types.UserInfo)
// 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 accountUsers { for _, accountUser := range accountUsers {
info, err := accountUser.ToUserInfo(nil, settings) info, err := accountUser.ToUserInfo(nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -902,7 +890,7 @@ func (am *DefaultAccountManager) BuildUserInfosForAccount(ctx context.Context, a
for _, localUser := range accountUsers { for _, localUser := range accountUsers {
var info *types.UserInfo var info *types.UserInfo
if queriedUser, contains := findUserInIDPUserdata(localUser.Id, queriedUsers); contains { if queriedUser, contains := findUserInIDPUserdata(localUser.Id, queriedUsers); contains {
info, err = localUser.ToUserInfo(queriedUser, settings) info, err = localUser.ToUserInfo(queriedUser)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -912,14 +900,6 @@ func (am *DefaultAccountManager) BuildUserInfosForAccount(ctx context.Context, a
name = localUser.ServiceUserName name = localUser.ServiceUserName
} }
dashboardViewPermissions := "full"
if !localUser.HasAdminPower() {
dashboardViewPermissions = "limited"
if settings.RegularUsersViewBlocked {
dashboardViewPermissions = "blocked"
}
}
info = &types.UserInfo{ info = &types.UserInfo{
ID: localUser.Id, ID: localUser.Id,
Email: "", Email: "",
@ -929,7 +909,6 @@ func (am *DefaultAccountManager) BuildUserInfosForAccount(ctx context.Context, a
Status: string(types.UserStatusActive), Status: string(types.UserStatusActive),
IsServiceUser: localUser.IsServiceUser, IsServiceUser: localUser.IsServiceUser,
NonDeletable: localUser.NonDeletable, NonDeletable: localUser.NonDeletable,
Permissions: types.UserPermissions{DashboardView: dashboardViewPermissions},
} }
} }
userInfosMap[info.ID] = info userInfosMap[info.ID] = info
@ -1239,8 +1218,10 @@ func validateUserInvite(invite *types.UserInfo) error {
return nil return nil
} }
// GetCurrentUserInfo retrieves the account's current user info // GetCurrentUserInfo retrieves the account's current user info and permissions
func (am *DefaultAccountManager) GetCurrentUserInfo(ctx context.Context, accountID, userID string) (*types.UserInfo, error) { func (am *DefaultAccountManager) GetCurrentUserInfo(ctx context.Context, userAuth nbcontext.UserAuth) (*users.UserInfoWithPermissions, error) {
accountID, userID := userAuth.AccountId, userAuth.UserId
user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, userID) user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, userID)
if err != nil { if err != nil {
return nil, err return nil, err
@ -1258,10 +1239,25 @@ func (am *DefaultAccountManager) GetCurrentUserInfo(ctx context.Context, account
return nil, err return nil, err
} }
settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID)
if err != nil {
return nil, err
}
userInfo, err := am.getUserInfo(ctx, user, accountID) userInfo, err := am.getUserInfo(ctx, user, accountID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return userInfo, nil userWithPermissions := &users.UserInfoWithPermissions{
UserInfo: userInfo,
Restricted: !userAuth.IsChild && user.IsRestrictable() && settings.RegularUsersViewBlocked,
}
permissions, err := am.permissionsManager.GetPermissionsByRole(ctx, user.Role)
if err == nil {
userWithPermissions.Permissions = permissions
}
return userWithPermissions, nil
} }

View File

@ -13,7 +13,10 @@ import (
nbcache "github.com/netbirdio/netbird/management/server/cache" nbcache "github.com/netbirdio/netbird/management/server/cache"
nbcontext "github.com/netbirdio/netbird/management/server/context" nbcontext "github.com/netbirdio/netbird/management/server/context"
"github.com/netbirdio/netbird/management/server/permissions" "github.com/netbirdio/netbird/management/server/permissions"
"github.com/netbirdio/netbird/management/server/permissions/modules"
"github.com/netbirdio/netbird/management/server/permissions/roles"
"github.com/netbirdio/netbird/management/server/status" "github.com/netbirdio/netbird/management/server/status"
"github.com/netbirdio/netbird/management/server/users"
"github.com/netbirdio/netbird/management/server/util" "github.com/netbirdio/netbird/management/server/util"
nbpeer "github.com/netbirdio/netbird/management/server/peer" nbpeer "github.com/netbirdio/netbird/management/server/peer"
@ -1020,90 +1023,6 @@ func TestDefaultAccountManager_ListUsers(t *testing.T) {
assert.Equal(t, 2, regular) assert.Equal(t, 2, regular)
} }
func TestDefaultAccountManager_ListUsers_DashboardPermissions(t *testing.T) {
testCases := []struct {
name string
role types.UserRole
limitedViewSettings bool
expectedDashboardPermissions string
}{
{
name: "Regular user, no limited view settings",
role: types.UserRoleUser,
limitedViewSettings: false,
expectedDashboardPermissions: "limited",
},
{
name: "Admin user, no limited view settings",
role: types.UserRoleAdmin,
limitedViewSettings: false,
expectedDashboardPermissions: "full",
},
{
name: "Owner, no limited view settings",
role: types.UserRoleOwner,
limitedViewSettings: false,
expectedDashboardPermissions: "full",
},
{
name: "Regular user, limited view settings",
role: types.UserRoleUser,
limitedViewSettings: true,
expectedDashboardPermissions: "blocked",
},
{
name: "Admin user, limited view settings",
role: types.UserRoleAdmin,
limitedViewSettings: true,
expectedDashboardPermissions: "full",
},
{
name: "Owner, limited view settings",
role: types.UserRoleOwner,
limitedViewSettings: true,
expectedDashboardPermissions: "full",
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
store, cleanup, err := store.NewTestStoreFromSQL(context.Background(), "", t.TempDir())
if err != nil {
t.Fatalf("Error when creating store: %s", err)
}
t.Cleanup(cleanup)
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "")
account.Users["normal_user1"] = types.NewUser("normal_user1", testCase.role, false, false, "", []string{}, types.UserIssuedAPI)
account.Settings.RegularUsersViewBlocked = testCase.limitedViewSettings
delete(account.Users, mockUserID)
err = store.SaveAccount(context.Background(), account)
if err != nil {
t.Fatalf("Error when saving account: %s", err)
}
permissionsManager := permissions.NewManager(store)
am := DefaultAccountManager{
Store: store,
eventStore: &activity.InMemoryEventStore{},
permissionsManager: permissionsManager,
}
users, err := am.ListUsers(context.Background(), mockAccountID)
if err != nil {
t.Fatalf("Error when checking user role: %s", err)
}
assert.Equal(t, 1, len(users))
userInfo, _ := users[0].ToUserInfo(nil, account.Settings)
assert.Equal(t, testCase.expectedDashboardPermissions, userInfo.Permissions.DashboardView)
})
}
}
func TestDefaultAccountManager_ExternalCache(t *testing.T) { func TestDefaultAccountManager_ExternalCache(t *testing.T) {
store, cleanup, err := store.NewTestStoreFromSQL(context.Background(), "", t.TempDir()) store, cleanup, err := store.NewTestStoreFromSQL(context.Background(), "", t.TempDir())
if err != nil { if err != nil {
@ -1654,40 +1573,35 @@ func TestDefaultAccountManager_GetCurrentUserInfo(t *testing.T) {
tt := []struct { tt := []struct {
name string name string
accountId string userAuth nbcontext.UserAuth
userId string
expectedErr error expectedErr error
expectedResult *types.UserInfo expectedResult *users.UserInfoWithPermissions
}{ }{
{ {
name: "not found", name: "not found",
accountId: account1.Id, userAuth: nbcontext.UserAuth{AccountId: account1.Id, UserId: "not-found"},
userId: "not-found",
expectedErr: status.NewUserNotFoundError("not-found"), expectedErr: status.NewUserNotFoundError("not-found"),
}, },
{ {
name: "not part of account", name: "not part of account",
accountId: account1.Id, userAuth: nbcontext.UserAuth{AccountId: account1.Id, UserId: "account2Owner"},
userId: "account2Owner",
expectedErr: status.NewUserNotPartOfAccountError(), expectedErr: status.NewUserNotPartOfAccountError(),
}, },
{ {
name: "blocked", name: "blocked",
accountId: account1.Id, userAuth: nbcontext.UserAuth{AccountId: account1.Id, UserId: "blocked-user"},
userId: "blocked-user",
expectedErr: status.NewUserBlockedError(), expectedErr: status.NewUserBlockedError(),
}, },
{ {
name: "service user", name: "service user",
accountId: account1.Id, userAuth: nbcontext.UserAuth{AccountId: account1.Id, UserId: "service-user"},
userId: "service-user",
expectedErr: status.NewPermissionDeniedError(), expectedErr: status.NewPermissionDeniedError(),
}, },
{ {
name: "owner user", name: "owner user",
accountId: account1.Id, userAuth: nbcontext.UserAuth{AccountId: account1.Id, UserId: "account1Owner"},
userId: "account1Owner", expectedResult: &users.UserInfoWithPermissions{
expectedResult: &types.UserInfo{ UserInfo: &types.UserInfo{
ID: "account1Owner", ID: "account1Owner",
Name: "", Name: "",
Role: "owner", Role: "owner",
@ -1699,16 +1613,15 @@ func TestDefaultAccountManager_GetCurrentUserInfo(t *testing.T) {
LastLogin: time.Time{}, LastLogin: time.Time{},
Issued: "api", Issued: "api",
IntegrationReference: integration_reference.IntegrationReference{}, IntegrationReference: integration_reference.IntegrationReference{},
Permissions: types.UserPermissions{
DashboardView: "full",
}, },
Permissions: mergeRolePermissions(roles.Owner),
}, },
}, },
{ {
name: "regular user", name: "regular user",
accountId: account1.Id, userAuth: nbcontext.UserAuth{AccountId: account1.Id, UserId: "regular-user"},
userId: "regular-user", expectedResult: &users.UserInfoWithPermissions{
expectedResult: &types.UserInfo{ UserInfo: &types.UserInfo{
ID: "regular-user", ID: "regular-user",
Name: "", Name: "",
Role: "user", Role: "user",
@ -1719,16 +1632,15 @@ func TestDefaultAccountManager_GetCurrentUserInfo(t *testing.T) {
LastLogin: time.Time{}, LastLogin: time.Time{},
Issued: "api", Issued: "api",
IntegrationReference: integration_reference.IntegrationReference{}, IntegrationReference: integration_reference.IntegrationReference{},
Permissions: types.UserPermissions{
DashboardView: "limited",
}, },
Permissions: mergeRolePermissions(roles.User),
}, },
}, },
{ {
name: "admin user", name: "admin user",
accountId: account1.Id, userAuth: nbcontext.UserAuth{AccountId: account1.Id, UserId: "admin-user"},
userId: "admin-user", expectedResult: &users.UserInfoWithPermissions{
expectedResult: &types.UserInfo{ UserInfo: &types.UserInfo{
ID: "admin-user", ID: "admin-user",
Name: "", Name: "",
Role: "admin", Role: "admin",
@ -1739,16 +1651,15 @@ func TestDefaultAccountManager_GetCurrentUserInfo(t *testing.T) {
LastLogin: time.Time{}, LastLogin: time.Time{},
Issued: "api", Issued: "api",
IntegrationReference: integration_reference.IntegrationReference{}, IntegrationReference: integration_reference.IntegrationReference{},
Permissions: types.UserPermissions{
DashboardView: "full",
}, },
Permissions: mergeRolePermissions(roles.Admin),
}, },
}, },
{ {
name: "settings blocked regular user", name: "settings blocked regular user",
accountId: account2.Id, userAuth: nbcontext.UserAuth{AccountId: account2.Id, UserId: "settings-blocked-user"},
userId: "settings-blocked-user", expectedResult: &users.UserInfoWithPermissions{
expectedResult: &types.UserInfo{ UserInfo: &types.UserInfo{
ID: "settings-blocked-user", ID: "settings-blocked-user",
Name: "", Name: "",
Role: "user", Role: "user",
@ -1759,16 +1670,57 @@ func TestDefaultAccountManager_GetCurrentUserInfo(t *testing.T) {
LastLogin: time.Time{}, LastLogin: time.Time{},
Issued: "api", Issued: "api",
IntegrationReference: integration_reference.IntegrationReference{}, IntegrationReference: integration_reference.IntegrationReference{},
Permissions: types.UserPermissions{
DashboardView: "blocked",
}, },
Permissions: mergeRolePermissions(roles.User),
Restricted: true,
},
},
{
name: "settings blocked regular user child account",
userAuth: nbcontext.UserAuth{AccountId: account2.Id, UserId: "settings-blocked-user", IsChild: true},
expectedResult: &users.UserInfoWithPermissions{
UserInfo: &types.UserInfo{
ID: "settings-blocked-user",
Name: "",
Role: "user",
Status: "active",
IsServiceUser: false,
IsBlocked: false,
NonDeletable: false,
LastLogin: time.Time{},
Issued: "api",
IntegrationReference: integration_reference.IntegrationReference{},
},
Permissions: mergeRolePermissions(roles.User),
Restricted: false,
},
},
{
name: "settings blocked owner user",
userAuth: nbcontext.UserAuth{AccountId: account2.Id, UserId: "account2Owner"},
expectedResult: &users.UserInfoWithPermissions{
UserInfo: &types.UserInfo{
ID: "account2Owner",
Name: "",
Role: "owner",
AutoGroups: []string{},
Status: "active",
IsServiceUser: false,
IsBlocked: false,
NonDeletable: false,
LastLogin: time.Time{},
Issued: "api",
IntegrationReference: integration_reference.IntegrationReference{},
},
Permissions: mergeRolePermissions(roles.Owner),
}, },
}, },
} }
for _, tc := range tt { for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
result, err := am.GetCurrentUserInfo(context.Background(), tc.accountId, tc.userId) result, err := am.GetCurrentUserInfo(context.Background(), tc.userAuth)
if tc.expectedErr != nil { if tc.expectedErr != nil {
assert.Equal(t, err, tc.expectedErr) assert.Equal(t, err, tc.expectedErr)
@ -1780,3 +1732,17 @@ func TestDefaultAccountManager_GetCurrentUserInfo(t *testing.T) {
}) })
} }
} }
func mergeRolePermissions(role roles.RolePermissions) roles.Permissions {
permissions := roles.Permissions{}
for k := range modules.All {
if rolePermissions, ok := role.Permissions[k]; ok {
permissions[k] = rolePermissions
continue
}
permissions[k] = role.AutoAllowNew
}
return permissions
}

View File

@ -0,0 +1,14 @@
package users
import (
"github.com/netbirdio/netbird/management/server/permissions/roles"
"github.com/netbirdio/netbird/management/server/types"
)
// Wrapped UserInfo with Role Permissions
type UserInfoWithPermissions struct {
*types.UserInfo
Permissions roles.Permissions
Restricted bool
}