Add limited dashboard view (#1738)

This commit is contained in:
pascal-fischer 2024-03-27 16:11:45 +01:00 committed by GitHub
parent 68b377a28c
commit ea2d060f93
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 466 additions and 41 deletions

View File

@ -85,7 +85,8 @@ type AccountManager interface {
GetAllPATs(accountID string, initiatorUserID string, targetUserID string) ([]*PersonalAccessToken, error) GetAllPATs(accountID string, initiatorUserID string, targetUserID string) ([]*PersonalAccessToken, error)
UpdatePeerSSHKey(peerID string, sshKey string) error UpdatePeerSSHKey(peerID string, sshKey string) error
GetUsersFromAccount(accountID, userID string) ([]*UserInfo, error) GetUsersFromAccount(accountID, userID string) ([]*UserInfo, error)
GetGroup(accountId, groupID string) (*Group, error) GetGroup(accountId, groupID, userID string) (*Group, error)
GetAllGroups(accountID, userID string) ([]*Group, error)
GetGroupByName(groupName, accountID string) (*Group, error) GetGroupByName(groupName, accountID string) (*Group, error)
SaveGroup(accountID, userID string, group *Group) error SaveGroup(accountID, userID string, group *Group) error
DeleteGroup(accountId, userId, groupID string) error DeleteGroup(accountId, userId, groupID string) error
@ -162,6 +163,9 @@ type Settings struct {
// Applies to all peers that have Peer.LoginExpirationEnabled set to true. // Applies to all peers that have Peer.LoginExpirationEnabled set to true.
PeerLoginExpiration time.Duration PeerLoginExpiration time.Duration
// RegularUsersViewBlocked allows to block regular users from viewing even their own peers and some UI elements
RegularUsersViewBlocked bool
// GroupsPropagationEnabled allows to propagate auto groups from the user to the peer // GroupsPropagationEnabled allows to propagate auto groups from the user to the peer
GroupsPropagationEnabled bool GroupsPropagationEnabled bool
@ -188,6 +192,7 @@ func (s *Settings) Copy() *Settings {
JWTGroupsClaimName: s.JWTGroupsClaimName, JWTGroupsClaimName: s.JWTGroupsClaimName,
GroupsPropagationEnabled: s.GroupsPropagationEnabled, GroupsPropagationEnabled: s.GroupsPropagationEnabled,
JWTAllowGroups: s.JWTAllowGroups, JWTAllowGroups: s.JWTAllowGroups,
RegularUsersViewBlocked: s.RegularUsersViewBlocked,
} }
if s.Extra != nil { if s.Extra != nil {
settings.Extra = s.Extra.Copy() settings.Extra = s.Extra.Copy()
@ -226,6 +231,10 @@ type Account struct {
Settings *Settings `gorm:"embedded;embeddedPrefix:settings_"` Settings *Settings `gorm:"embedded;embeddedPrefix:settings_"`
} }
type UserPermissions struct {
DashboardView string `json:"dashboard_view"`
}
type UserInfo struct { type UserInfo struct {
ID string `json:"id"` ID string `json:"id"`
Email string `json:"email"` Email string `json:"email"`
@ -239,6 +248,7 @@ type UserInfo struct {
LastLogin time.Time `json:"last_login"` LastLogin time.Time `json:"last_login"`
Issued string `json:"issued"` Issued string `json:"issued"`
IntegrationReference IntegrationReference `json:"-"` IntegrationReference IntegrationReference `json:"-"`
Permissions UserPermissions `json:"permissions"`
} }
// getRoutesToSync returns the enabled routes for the peer ID and the routes // getRoutesToSync returns the enabled routes for the peer ID and the routes
@ -1885,6 +1895,7 @@ func newAccountWithId(accountID, userID, domain string) *Account {
PeerLoginExpirationEnabled: true, PeerLoginExpirationEnabled: true,
PeerLoginExpiration: DefaultPeerLoginExpiration, PeerLoginExpiration: DefaultPeerLoginExpiration,
GroupsPropagationEnabled: true, GroupsPropagationEnabled: true,
RegularUsersViewBlocked: true,
}, },
} }

View File

@ -63,7 +63,7 @@ func (g *Group) Copy() *Group {
} }
// GetGroup object of the peers // GetGroup object of the peers
func (am *DefaultAccountManager) GetGroup(accountID, groupID string) (*Group, error) { func (am *DefaultAccountManager) GetGroup(accountID, groupID, userID string) (*Group, error) {
unlock := am.Store.AcquireAccountLock(accountID) unlock := am.Store.AcquireAccountLock(accountID)
defer unlock() defer unlock()
@ -72,6 +72,15 @@ func (am *DefaultAccountManager) GetGroup(accountID, groupID string) (*Group, er
return nil, err return nil, err
} }
user, err := account.FindUser(userID)
if err != nil {
return nil, err
}
if !user.HasAdminPower() && !user.IsServiceUser && account.Settings.RegularUsersViewBlocked {
return nil, status.Errorf(status.PermissionDenied, "groups are blocked for users")
}
group, ok := account.Groups[groupID] group, ok := account.Groups[groupID]
if ok { if ok {
return group, nil return group, nil
@ -80,6 +89,33 @@ func (am *DefaultAccountManager) GetGroup(accountID, groupID string) (*Group, er
return nil, status.Errorf(status.NotFound, "group with ID %s not found", groupID) return nil, status.Errorf(status.NotFound, "group with ID %s not found", groupID)
} }
// GetAllGroups returns all groups in an account
func (am *DefaultAccountManager) GetAllGroups(accountID string, userID string) ([]*Group, error) {
unlock := am.Store.AcquireAccountLock(accountID)
defer unlock()
account, err := am.Store.GetAccount(accountID)
if err != nil {
return nil, err
}
user, err := account.FindUser(userID)
if err != nil {
return nil, err
}
if !user.HasAdminPower() && !user.IsServiceUser && account.Settings.RegularUsersViewBlocked {
return nil, status.Errorf(status.PermissionDenied, "groups are blocked for users")
}
groups := make([]*Group, 0, len(account.Groups))
for _, item := range account.Groups {
groups = append(groups, item)
}
return groups, nil
}
// GetGroupByName filters all groups in an account by name and returns the one with the most peers // GetGroupByName filters all groups in an account by name and returns the one with the most peers
func (am *DefaultAccountManager) GetGroupByName(groupName, accountID string) (*Group, error) { func (am *DefaultAccountManager) GetGroupByName(groupName, accountID string) (*Group, error) {
unlock := am.Store.AcquireAccountLock(accountID) unlock := am.Store.AcquireAccountLock(accountID)

View File

@ -76,6 +76,7 @@ func (h *AccountsHandler) UpdateAccount(w http.ResponseWriter, r *http.Request)
settings := &server.Settings{ settings := &server.Settings{
PeerLoginExpirationEnabled: req.Settings.PeerLoginExpirationEnabled, PeerLoginExpirationEnabled: req.Settings.PeerLoginExpirationEnabled,
PeerLoginExpiration: time.Duration(float64(time.Second.Nanoseconds()) * float64(req.Settings.PeerLoginExpiration)), PeerLoginExpiration: time.Duration(float64(time.Second.Nanoseconds()) * float64(req.Settings.PeerLoginExpiration)),
RegularUsersViewBlocked: req.Settings.RegularUsersViewBlocked,
} }
if req.Settings.Extra != nil { if req.Settings.Extra != nil {
@ -143,6 +144,7 @@ func toAccountResponse(account *server.Account) *api.Account {
JwtGroupsEnabled: &account.Settings.JWTGroupsEnabled, JwtGroupsEnabled: &account.Settings.JWTGroupsEnabled,
JwtGroupsClaimName: &account.Settings.JWTGroupsClaimName, JwtGroupsClaimName: &account.Settings.JWTGroupsClaimName,
JwtAllowGroups: &jwtAllowGroups, JwtAllowGroups: &jwtAllowGroups,
RegularUsersViewBlocked: account.Settings.RegularUsersViewBlocked,
} }
if account.Settings.Extra != nil { if account.Settings.Extra != nil {

View File

@ -69,6 +69,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
Settings: &server.Settings{ Settings: &server.Settings{
PeerLoginExpirationEnabled: false, PeerLoginExpirationEnabled: false,
PeerLoginExpiration: time.Hour, PeerLoginExpiration: time.Hour,
RegularUsersViewBlocked: true,
}, },
}, adminUser) }, adminUser)
@ -96,6 +97,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
JwtGroupsClaimName: sr(""), JwtGroupsClaimName: sr(""),
JwtGroupsEnabled: br(false), JwtGroupsEnabled: br(false),
JwtAllowGroups: &[]string{}, JwtAllowGroups: &[]string{},
RegularUsersViewBlocked: true,
}, },
expectedArray: true, expectedArray: true,
expectedID: accountID, expectedID: accountID,
@ -114,6 +116,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
JwtGroupsClaimName: sr(""), JwtGroupsClaimName: sr(""),
JwtGroupsEnabled: br(false), JwtGroupsEnabled: br(false),
JwtAllowGroups: &[]string{}, JwtAllowGroups: &[]string{},
RegularUsersViewBlocked: false,
}, },
expectedArray: false, expectedArray: false,
expectedID: accountID, expectedID: accountID,
@ -123,7 +126,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
expectedBody: true, expectedBody: true,
requestType: http.MethodPut, requestType: http.MethodPut,
requestPath: "/api/accounts/" + accountID, requestPath: "/api/accounts/" + accountID,
requestBody: bytes.NewBufferString("{\"settings\": {\"peer_login_expiration\": 15552000,\"peer_login_expiration_enabled\": false,\"jwt_groups_enabled\":true,\"jwt_groups_claim_name\":\"roles\",\"jwt_allow_groups\":[\"test\"]}}"), requestBody: bytes.NewBufferString("{\"settings\": {\"peer_login_expiration\": 15552000,\"peer_login_expiration_enabled\": false,\"jwt_groups_enabled\":true,\"jwt_groups_claim_name\":\"roles\",\"jwt_allow_groups\":[\"test\"],\"regular_users_view_blocked\":true}}"),
expectedStatus: http.StatusOK, expectedStatus: http.StatusOK,
expectedSettings: api.AccountSettings{ expectedSettings: api.AccountSettings{
PeerLoginExpiration: 15552000, PeerLoginExpiration: 15552000,
@ -132,6 +135,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
JwtGroupsClaimName: sr("roles"), JwtGroupsClaimName: sr("roles"),
JwtGroupsEnabled: br(true), JwtGroupsEnabled: br(true),
JwtAllowGroups: &[]string{"test"}, JwtAllowGroups: &[]string{"test"},
RegularUsersViewBlocked: true,
}, },
expectedArray: false, expectedArray: false,
expectedID: accountID, expectedID: accountID,
@ -141,7 +145,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
expectedBody: true, expectedBody: true,
requestType: http.MethodPut, requestType: http.MethodPut,
requestPath: "/api/accounts/" + accountID, requestPath: "/api/accounts/" + accountID,
requestBody: bytes.NewBufferString("{\"settings\": {\"peer_login_expiration\": 554400,\"peer_login_expiration_enabled\": true,\"jwt_groups_enabled\":true,\"jwt_groups_claim_name\":\"groups\",\"groups_propagation_enabled\":true}}"), requestBody: bytes.NewBufferString("{\"settings\": {\"peer_login_expiration\": 554400,\"peer_login_expiration_enabled\": true,\"jwt_groups_enabled\":true,\"jwt_groups_claim_name\":\"groups\",\"groups_propagation_enabled\":true,\"regular_users_view_blocked\":true}}"),
expectedStatus: http.StatusOK, expectedStatus: http.StatusOK,
expectedSettings: api.AccountSettings{ expectedSettings: api.AccountSettings{
PeerLoginExpiration: 554400, PeerLoginExpiration: 554400,
@ -150,6 +154,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
JwtGroupsClaimName: sr("groups"), JwtGroupsClaimName: sr("groups"),
JwtGroupsEnabled: br(true), JwtGroupsEnabled: br(true),
JwtAllowGroups: &[]string{}, JwtAllowGroups: &[]string{},
RegularUsersViewBlocked: true,
}, },
expectedArray: false, expectedArray: false,
expectedID: accountID, expectedID: accountID,

View File

@ -54,6 +54,10 @@ components:
description: Period of time after which peer login expires (seconds). description: Period of time after which peer login expires (seconds).
type: integer type: integer
example: 43200 example: 43200
regular_users_view_blocked:
description: Allows blocking regular users from viewing parts of the system.
type: boolean
example: true
groups_propagation_enabled: groups_propagation_enabled:
description: Allows propagate the new user auto groups to peers that belongs to the user description: Allows propagate the new user auto groups to peers that belongs to the user
type: boolean type: boolean
@ -77,6 +81,7 @@ components:
required: required:
- peer_login_expiration_enabled - peer_login_expiration_enabled
- peer_login_expiration - peer_login_expiration
- regular_users_view_blocked
AccountExtraSettings: AccountExtraSettings:
type: object type: object
properties: properties:
@ -144,6 +149,8 @@ components:
description: How user was issued by API or Integration description: How user was issued by API or Integration
type: string type: string
example: api example: api
permissions:
$ref: '#/components/schemas/UserPermissions'
required: required:
- id - id
- email - email
@ -152,6 +159,14 @@ components:
- auto_groups - auto_groups
- status - status
- is_blocked - is_blocked
UserPermissions:
type: object
properties:
dashboard_view:
description: User's permission to view the dashboard
type: string
enum: [ "limited", "blocked", "full" ]
example: limited
UserRequest: UserRequest:
type: object type: object
properties: properties:
@ -589,8 +604,6 @@ components:
type: string type: string
enum: ["api", "integration", "jwt"] enum: ["api", "integration", "jwt"]
example: api example: api
type: string
example: api
required: required:
- id - id
- name - name

View File

@ -69,6 +69,20 @@ const (
GeoLocationCheckActionDeny GeoLocationCheckAction = "deny" GeoLocationCheckActionDeny GeoLocationCheckAction = "deny"
) )
// Defines values for GroupIssued.
const (
GroupIssuedApi GroupIssued = "api"
GroupIssuedIntegration GroupIssued = "integration"
GroupIssuedJwt GroupIssued = "jwt"
)
// Defines values for GroupMinimumIssued.
const (
GroupMinimumIssuedApi GroupMinimumIssued = "api"
GroupMinimumIssuedIntegration GroupMinimumIssued = "integration"
GroupMinimumIssuedJwt GroupMinimumIssued = "jwt"
)
// Defines values for NameserverNsType. // Defines values for NameserverNsType.
const ( const (
NameserverNsTypeUdp NameserverNsType = "udp" NameserverNsTypeUdp NameserverNsType = "udp"
@ -129,6 +143,13 @@ const (
UserStatusInvited UserStatus = "invited" UserStatusInvited UserStatus = "invited"
) )
// Defines values for UserPermissionsDashboardView.
const (
UserPermissionsDashboardViewBlocked UserPermissionsDashboardView = "blocked"
UserPermissionsDashboardViewFull UserPermissionsDashboardView = "full"
UserPermissionsDashboardViewLimited UserPermissionsDashboardView = "limited"
)
// AccessiblePeer defines model for AccessiblePeer. // AccessiblePeer defines model for AccessiblePeer.
type AccessiblePeer struct { type AccessiblePeer struct {
// DnsLabel Peer's DNS label is the parsed peer name for domain resolution. It is used to form an FQDN by appending the account's domain to the peer label. e.g. peer-dns-label.netbird.cloud // DnsLabel Peer's DNS label is the parsed peer name for domain resolution. It is used to form an FQDN by appending the account's domain to the peer label. e.g. peer-dns-label.netbird.cloud
@ -186,6 +207,9 @@ type AccountSettings struct {
// 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 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"` PeerLoginExpirationEnabled bool `json:"peer_login_expiration_enabled"`
// RegularUsersViewBlocked Allows blocking regular users from viewing parts of the system.
RegularUsersViewBlocked bool `json:"regular_users_view_blocked"`
} }
// Checks List of objects that perform the actual checks // Checks List of objects that perform the actual checks
@ -283,8 +307,8 @@ type Group struct {
// Id Group ID // Id Group ID
Id string `json:"id"` Id string `json:"id"`
// Issued How group was issued by API or from JWT token // Issued How the group was issued (api, integration, jwt)
Issued *string `json:"issued,omitempty"` Issued *GroupIssued `json:"issued,omitempty"`
// Name Group Name identifier // Name Group Name identifier
Name string `json:"name"` Name string `json:"name"`
@ -296,13 +320,16 @@ type Group struct {
PeersCount int `json:"peers_count"` PeersCount int `json:"peers_count"`
} }
// GroupIssued How the group was issued (api, integration, jwt)
type GroupIssued string
// GroupMinimum defines model for GroupMinimum. // GroupMinimum defines model for GroupMinimum.
type GroupMinimum struct { type GroupMinimum struct {
// Id Group ID // Id Group ID
Id string `json:"id"` Id string `json:"id"`
// Issued How group was issued by API or from JWT token // Issued How the group was issued (api, integration, jwt)
Issued *string `json:"issued,omitempty"` Issued *GroupMinimumIssued `json:"issued,omitempty"`
// Name Group Name identifier // Name Group Name identifier
Name string `json:"name"` Name string `json:"name"`
@ -311,6 +338,9 @@ type GroupMinimum struct {
PeersCount int `json:"peers_count"` PeersCount int `json:"peers_count"`
} }
// GroupMinimumIssued How the group was issued (api, integration, jwt)
type GroupMinimumIssued string
// GroupRequest defines model for GroupRequest. // GroupRequest defines model for GroupRequest.
type GroupRequest struct { type GroupRequest struct {
// Name Group name identifier // Name Group name identifier
@ -1072,7 +1102,8 @@ type User struct {
LastLogin *time.Time `json:"last_login,omitempty"` LastLogin *time.Time `json:"last_login,omitempty"`
// Name User's name from idp provider // Name User's name from idp provider
Name string `json:"name"` Name string `json:"name"`
Permissions *UserPermissions `json:"permissions,omitempty"`
// Role User's NetBird account role // Role User's NetBird account role
Role string `json:"role"` Role string `json:"role"`
@ -1102,6 +1133,15 @@ type UserCreateRequest struct {
Role string `json:"role"` Role string `json:"role"`
} }
// UserPermissions defines model for UserPermissions.
type UserPermissions struct {
// DashboardView User's permission to view the dashboard
DashboardView *UserPermissionsDashboardView `json:"dashboard_view,omitempty"`
}
// 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

@ -35,19 +35,25 @@ func NewGroupsHandler(accountManager server.AccountManager, authCfg AuthCfg) *Gr
// GetAllGroups list for the account // GetAllGroups list for the account
func (h *GroupsHandler) GetAllGroups(w http.ResponseWriter, r *http.Request) { func (h *GroupsHandler) GetAllGroups(w http.ResponseWriter, r *http.Request) {
claims := h.claimsExtractor.FromRequestContext(r) claims := h.claimsExtractor.FromRequestContext(r)
account, _, err := h.accountManager.GetAccountFromToken(claims) account, user, err := h.accountManager.GetAccountFromToken(claims)
if err != nil { if err != nil {
log.Error(err) log.Error(err)
http.Redirect(w, r, "/", http.StatusInternalServerError) http.Redirect(w, r, "/", http.StatusInternalServerError)
return return
} }
var groups []*api.Group groups, err := h.accountManager.GetAllGroups(account.Id, user.Id)
for _, g := range account.Groups { if err != nil {
groups = append(groups, toGroupResponse(account, g)) util.WriteError(err, w)
return
} }
util.WriteJSONObject(w, groups) groupsResponse := make([]*api.Group, 0, len(groups))
for _, group := range groups {
groupsResponse = append(groupsResponse, toGroupResponse(account, group))
}
util.WriteJSONObject(w, groupsResponse)
} }
// UpdateGroup handles update to a group identified by a given ID // UpdateGroup handles update to a group identified by a given ID
@ -207,7 +213,7 @@ func (h *GroupsHandler) DeleteGroup(w http.ResponseWriter, r *http.Request) {
// GetGroup returns a group // GetGroup returns a group
func (h *GroupsHandler) GetGroup(w http.ResponseWriter, r *http.Request) { func (h *GroupsHandler) GetGroup(w http.ResponseWriter, r *http.Request) {
claims := h.claimsExtractor.FromRequestContext(r) claims := h.claimsExtractor.FromRequestContext(r)
account, _, err := h.accountManager.GetAccountFromToken(claims) account, user, err := h.accountManager.GetAccountFromToken(claims)
if err != nil { if err != nil {
util.WriteError(err, w) util.WriteError(err, w)
return return
@ -221,7 +227,7 @@ func (h *GroupsHandler) GetGroup(w http.ResponseWriter, r *http.Request) {
return return
} }
group, err := h.accountManager.GetGroup(account.Id, groupID) group, err := h.accountManager.GetGroup(account.Id, groupID, user.Id)
if err != nil { if err != nil {
util.WriteError(err, w) util.WriteError(err, w)
return return
@ -239,7 +245,7 @@ func toGroupResponse(account *server.Account, group *server.Group) *api.Group {
gr := api.Group{ gr := api.Group{
Id: group.ID, Id: group.ID,
Name: group.Name, Name: group.Name,
Issued: &group.Issued, Issued: (*api.GroupIssued)(&group.Issued),
} }
for _, pid := range group.Peers { for _, pid := range group.Peers {

View File

@ -37,7 +37,7 @@ func initGroupTestData(user *server.User, groups ...*server.Group) *GroupsHandle
} }
return nil return nil
}, },
GetGroupFunc: func(_, groupID string) (*server.Group, error) { GetGroupFunc: func(_, groupID, _ string) (*server.Group, error) {
if groupID != "idofthegroup" { if groupID != "idofthegroup" {
return nil, status.Errorf(status.NotFound, "not found") return nil, status.Errorf(status.NotFound, "not found")
} }
@ -187,7 +187,7 @@ func TestWriteGroup(t *testing.T) {
expectedGroup: &api.Group{ expectedGroup: &api.Group{
Id: "id-was-set", Id: "id-was-set",
Name: "Default POSTed Group", Name: "Default POSTed Group",
Issued: &groupIssuedAPI, Issued: (*api.GroupIssued)(&groupIssuedAPI),
}, },
}, },
{ {
@ -209,7 +209,7 @@ func TestWriteGroup(t *testing.T) {
expectedGroup: &api.Group{ expectedGroup: &api.Group{
Id: "id-existed", Id: "id-existed",
Name: "Default POSTed Group", Name: "Default POSTed Group",
Issued: &groupIssuedAPI, Issued: (*api.GroupIssued)(&groupIssuedAPI),
}, },
}, },
{ {
@ -240,7 +240,7 @@ func TestWriteGroup(t *testing.T) {
expectedGroup: &api.Group{ expectedGroup: &api.Group{
Id: "id-jwt-group", Id: "id-jwt-group",
Name: "changed", Name: "changed",
Issued: &groupIssuedJWT, Issued: (*api.GroupIssued)(&groupIssuedJWT),
}, },
}, },
} }

View File

@ -288,5 +288,8 @@ func toUserResponse(user *server.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

@ -105,7 +105,7 @@ func initUsersTestData() *UsersHandler {
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) info, err := update.Copy().ToUserInfo(nil, &server.Settings{RegularUsersViewBlocked: false})
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -31,7 +31,8 @@ type MockAccountManager struct {
GetNetworkMapFunc func(peerKey string) (*server.NetworkMap, error) GetNetworkMapFunc func(peerKey string) (*server.NetworkMap, error)
GetPeerNetworkFunc func(peerKey string) (*server.Network, error) GetPeerNetworkFunc func(peerKey string) (*server.Network, error)
AddPeerFunc func(setupKey string, userId string, peer *nbpeer.Peer) (*nbpeer.Peer, *server.NetworkMap, error) AddPeerFunc func(setupKey string, userId string, peer *nbpeer.Peer) (*nbpeer.Peer, *server.NetworkMap, error)
GetGroupFunc func(accountID, groupID string) (*server.Group, error) GetGroupFunc func(accountID, groupID, userID string) (*server.Group, error)
GetAllGroupsFunc func(accountID, userID string) ([]*server.Group, error)
GetGroupByNameFunc func(accountID, groupName string) (*server.Group, error) GetGroupByNameFunc func(accountID, groupName string) (*server.Group, error)
SaveGroupFunc func(accountID, userID string, group *server.Group) error SaveGroupFunc func(accountID, userID string, group *server.Group) error
DeleteGroupFunc func(accountID, userId, groupID string) error DeleteGroupFunc func(accountID, userId, groupID string) error
@ -92,6 +93,22 @@ type MockAccountManager struct {
GetIdpManagerFunc func() idp.Manager GetIdpManagerFunc func() idp.Manager
} }
// GetGroup mock implementation of GetGroup from server.AccountManager interface
func (am *MockAccountManager) GetGroup(accountId, groupID, userID string) (*server.Group, error) {
if am.GetGroupFunc != nil {
return am.GetGroupFunc(accountId, groupID, userID)
}
return nil, status.Errorf(codes.Unimplemented, "method GetGroup is not implemented")
}
// GetAllGroups mock implementation of GetAllGroups from server.AccountManager interface
func (am *MockAccountManager) GetAllGroups(accountID, userID string) ([]*server.Group, error) {
if am.GetAllGroupsFunc != nil {
return am.GetAllGroupsFunc(accountID, userID)
}
return nil, status.Errorf(codes.Unimplemented, "method GetAllGroups is not implemented")
}
// GetUsersFromAccount mock implementation of GetUsersFromAccount from server.AccountManager interface // GetUsersFromAccount mock implementation of GetUsersFromAccount from server.AccountManager interface
func (am *MockAccountManager) GetUsersFromAccount(accountID string, userID string) ([]*server.UserInfo, error) { func (am *MockAccountManager) GetUsersFromAccount(accountID string, userID string) ([]*server.UserInfo, error) {
if am.GetUsersFromAccountFunc != nil { if am.GetUsersFromAccountFunc != nil {
@ -243,14 +260,6 @@ func (am *MockAccountManager) AddPeer(
return nil, nil, status.Errorf(codes.Unimplemented, "method AddPeer is not implemented") return nil, nil, status.Errorf(codes.Unimplemented, "method AddPeer is not implemented")
} }
// GetGroup mock implementation of GetGroup from server.AccountManager interface
func (am *MockAccountManager) GetGroup(accountID, groupID string) (*server.Group, error) {
if am.GetGroupFunc != nil {
return am.GetGroupFunc(accountID, groupID)
}
return nil, status.Errorf(codes.Unimplemented, "method GetGroup is not implemented")
}
// GetGroupByName mock implementation of GetGroupByName from server.AccountManager interface // GetGroupByName mock implementation of GetGroupByName from server.AccountManager interface
func (am *MockAccountManager) GetGroupByName(accountID, groupName string) (*server.Group, error) { func (am *MockAccountManager) GetGroupByName(accountID, groupName string) (*server.Group, error) {
if am.GetGroupFunc != nil { if am.GetGroupFunc != nil {

View File

@ -54,6 +54,11 @@ func (am *DefaultAccountManager) GetPeers(accountID, userID string) ([]*nbpeer.P
peers := make([]*nbpeer.Peer, 0) peers := make([]*nbpeer.Peer, 0)
peersMap := make(map[string]*nbpeer.Peer) peersMap := make(map[string]*nbpeer.Peer)
if !user.HasAdminPower() && !user.IsServiceUser && account.Settings.RegularUsersViewBlocked {
return peers, nil
}
for _, peer := range account.Peers { for _, peer := range account.Peers {
if !(user.HasAdminPower() || user.IsServiceUser) && user.Id != peer.UserID { if !(user.HasAdminPower() || user.IsServiceUser) && 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
@ -738,6 +743,10 @@ func (am *DefaultAccountManager) GetPeer(accountID, peerID, userID string) (*nbp
return nil, err return nil, err
} }
if !user.HasAdminPower() && !user.IsServiceUser && account.Settings.RegularUsersViewBlocked {
return nil, status.Errorf(status.Internal, "user %s has no access to his own peer %s under account %s", userID, peerID, accountID)
}
peer := account.GetPeer(peerID) peer := account.GetPeer(peerID)
if peer == nil { if peer == nil {
return nil, status.Errorf(status.NotFound, "peer with %s not found under account %s", peerID, accountID) return nil, status.Errorf(status.NotFound, "peer with %s not found under account %s", peerID, accountID)

View File

@ -4,9 +4,8 @@ import (
"testing" "testing"
"time" "time"
"github.com/stretchr/testify/assert"
"github.com/rs/xid" "github.com/rs/xid"
"github.com/stretchr/testify/assert"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes" "golang.zx2c4.com/wireguard/wgctrl/wgtypes"
nbpeer "github.com/netbirdio/netbird/management/server/peer" nbpeer "github.com/netbirdio/netbird/management/server/peer"
@ -392,6 +391,8 @@ func TestDefaultAccountManager_GetPeer(t *testing.T) {
Id: someUser, Id: someUser,
Role: UserRoleUser, Role: UserRoleUser,
} }
account.Settings.RegularUsersViewBlocked = false
err = manager.Store.SaveAccount(account) err = manager.Store.SaveAccount(account)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -480,3 +481,153 @@ func TestDefaultAccountManager_GetPeer(t *testing.T) {
} }
assert.NotNil(t, peer) assert.NotNil(t, peer)
} }
func TestDefaultAccountManager_GetPeers(t *testing.T) {
testCases := []struct {
name string
role UserRole
limitedViewSettings bool
isServiceUser bool
expectedPeerCount int
}{
{
name: "Regular user, no limited view settings, not a service user",
role: UserRoleUser,
limitedViewSettings: false,
isServiceUser: false,
expectedPeerCount: 1,
},
{
name: "Service user, no limited view settings",
role: UserRoleUser,
limitedViewSettings: false,
isServiceUser: true,
expectedPeerCount: 2,
},
{
name: "Regular user, limited view settings",
role: UserRoleUser,
limitedViewSettings: true,
isServiceUser: false,
expectedPeerCount: 0,
},
{
name: "Service user, limited view settings",
role: UserRoleUser,
limitedViewSettings: true,
isServiceUser: true,
expectedPeerCount: 2,
},
{
name: "Admin, no limited view settings, not a service user",
role: UserRoleAdmin,
limitedViewSettings: false,
isServiceUser: false,
expectedPeerCount: 2,
},
{
name: "Admin service user, no limited view settings",
role: UserRoleAdmin,
limitedViewSettings: false,
isServiceUser: true,
expectedPeerCount: 2,
},
{
name: "Admin, limited view settings",
role: UserRoleAdmin,
limitedViewSettings: true,
isServiceUser: false,
expectedPeerCount: 2,
},
{
name: "Admin Service user, limited view settings",
role: UserRoleAdmin,
limitedViewSettings: true,
isServiceUser: true,
expectedPeerCount: 2,
},
{
name: "Owner, no limited view settings",
role: UserRoleOwner,
limitedViewSettings: true,
isServiceUser: false,
expectedPeerCount: 2,
},
{
name: "Owner, limited view settings",
role: UserRoleOwner,
limitedViewSettings: true,
isServiceUser: false,
expectedPeerCount: 2,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
manager, err := createManager(t)
if err != nil {
t.Fatal(err)
return
}
// account with an admin and a regular user
accountID := "test_account"
adminUser := "account_creator"
someUser := "some_user"
account := newAccountWithId(accountID, adminUser, "")
account.Users[someUser] = &User{
Id: someUser,
Role: testCase.role,
IsServiceUser: testCase.isServiceUser,
}
account.Policies = []*Policy{}
account.Settings.RegularUsersViewBlocked = testCase.limitedViewSettings
err = manager.Store.SaveAccount(account)
if err != nil {
t.Fatal(err)
return
}
peerKey1, err := wgtypes.GeneratePrivateKey()
if err != nil {
t.Fatal(err)
return
}
peerKey2, err := wgtypes.GeneratePrivateKey()
if err != nil {
t.Fatal(err)
return
}
_, _, err = manager.AddPeer("", someUser, &nbpeer.Peer{
Key: peerKey1.PublicKey().String(),
Meta: nbpeer.PeerSystemMeta{Hostname: "test-peer-1"},
})
if err != nil {
t.Errorf("expecting peer to be added, got failure %v", err)
return
}
_, _, err = manager.AddPeer("", adminUser, &nbpeer.Peer{
Key: peerKey2.PublicKey().String(),
Meta: nbpeer.PeerSystemMeta{Hostname: "test-peer-2"},
})
if err != nil {
t.Errorf("expecting peer to be added, got failure %v", err)
return
}
peers, err := manager.GetPeers(accountID, someUser)
if err != nil {
t.Fatal(err)
return
}
assert.NotNil(t, peers)
assert.Len(t, peers, testCase.expectedPeerCount)
})
}
}

View File

@ -339,6 +339,10 @@ func (am *DefaultAccountManager) ListSetupKeys(accountID, userID string) ([]*Set
return nil, err return nil, err
} }
if !user.HasAdminPower() && !user.IsServiceUser {
return nil, status.Errorf(status.Unauthorized, "only users with admin power can view policies")
}
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
@ -368,6 +372,10 @@ func (am *DefaultAccountManager) GetSetupKey(accountID, userID, keyID string) (*
return nil, err return nil, err
} }
if !user.HasAdminPower() && !user.IsServiceUser {
return nil, status.Errorf(status.Unauthorized, "only users with admin power can view policies")
}
var foundKey *SetupKey var foundKey *SetupKey
for _, key := range account.SetupKeys { for _, key := range account.SetupKeys {
if key.Id == keyID { if key.Id == keyID {

View File

@ -166,6 +166,37 @@ func TestDefaultAccountManager_CreateSetupKey(t *testing.T) {
} }
func TestGetSetupKeys(t *testing.T) {
manager, err := createManager(t)
if err != nil {
t.Fatal(err)
}
userID := "testingUser"
account, err := manager.GetOrCreateAccountByUser(userID, "")
if err != nil {
t.Fatal(err)
}
err = manager.SaveGroup(account.Id, userID, &Group{
ID: "group_1",
Name: "group_name_1",
Peers: []string{},
})
if err != nil {
t.Fatal(err)
}
err = manager.SaveGroup(account.Id, userID, &Group{
ID: "group_2",
Name: "group_name_2",
Peers: []string{},
})
if err != nil {
t.Fatal(err)
}
}
func TestGenerateDefaultSetupKey(t *testing.T) { func TestGenerateDefaultSetupKey(t *testing.T) {
expectedName := "Default key" expectedName := "Default key"
expectedRevoke := false expectedRevoke := false

View File

@ -113,12 +113,20 @@ func (u *User) HasAdminPower() bool {
} }
// 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) (*UserInfo, error) { func (u *User) ToUserInfo(userData *idp.UserData, settings *Settings) (*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,
@ -131,6 +139,9 @@ func (u *User) ToUserInfo(userData *idp.UserData) (*UserInfo, error) {
IsBlocked: u.Blocked, IsBlocked: u.Blocked,
LastLogin: u.LastLogin, LastLogin: u.LastLogin,
Issued: u.Issued, Issued: u.Issued,
Permissions: UserPermissions{
DashboardView: dashboardViewPermissions,
},
}, nil }, nil
} }
if userData.ID != u.Id { if userData.ID != u.Id {
@ -153,6 +164,9 @@ func (u *User) ToUserInfo(userData *idp.UserData) (*UserInfo, error) {
IsBlocked: u.Blocked, IsBlocked: u.Blocked,
LastLogin: u.LastLogin, LastLogin: u.LastLogin,
Issued: u.Issued, Issued: u.Issued,
Permissions: UserPermissions{
DashboardView: dashboardViewPermissions,
},
}, nil }, nil
} }
@ -358,7 +372,7 @@ func (am *DefaultAccountManager) inviteNewUser(accountID, userID string, invite
am.StoreEvent(userID, newUser.Id, accountID, activity.UserInvited, nil) am.StoreEvent(userID, newUser.Id, accountID, activity.UserInvited, nil)
return newUser.ToUserInfo(idpUser) return newUser.ToUserInfo(idpUser, account.Settings)
} }
// GetUser looks up a user by provided authorization claims. // GetUser looks up a user by provided authorization claims.
@ -905,9 +919,9 @@ func (am *DefaultAccountManager) SaveOrAddUser(accountID, initiatorUserID string
if err != nil { if err != nil {
return nil, err return nil, err
} }
return newUser.ToUserInfo(userData) return newUser.ToUserInfo(userData, account.Settings)
} }
return newUser.ToUserInfo(nil) return newUser.ToUserInfo(nil, account.Settings)
} }
// GetOrCreateAccountByUser returns an existing account for a given user id or creates a new one if doesn't exist // GetOrCreateAccountByUser returns an existing account for a given user id or creates a new one if doesn't exist
@ -998,7 +1012,7 @@ func (am *DefaultAccountManager) GetUsersFromAccount(accountID, userID string) (
// 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
} }
info, err := accountUser.ToUserInfo(nil) info, err := accountUser.ToUserInfo(nil, account.Settings)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -1015,7 +1029,7 @@ func (am *DefaultAccountManager) GetUsersFromAccount(accountID, userID string) (
var info *UserInfo var info *UserInfo
if queriedUser, contains := findUserInIDPUserdata(localUser.Id, queriedUsers); contains { if queriedUser, contains := findUserInIDPUserdata(localUser.Id, queriedUsers); contains {
info, err = localUser.ToUserInfo(queriedUser) info, err = localUser.ToUserInfo(queriedUser, account.Settings)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -1024,6 +1038,15 @@ func (am *DefaultAccountManager) GetUsersFromAccount(accountID, userID string) (
if localUser.IsServiceUser { if localUser.IsServiceUser {
name = localUser.ServiceUserName name = localUser.ServiceUserName
} }
dashboardViewPermissions := "full"
if !localUser.HasAdminPower() {
dashboardViewPermissions = "limited"
if account.Settings.RegularUsersViewBlocked {
dashboardViewPermissions = "blocked"
}
}
info = &UserInfo{ info = &UserInfo{
ID: localUser.Id, ID: localUser.Id,
Email: "", Email: "",
@ -1033,6 +1056,7 @@ func (am *DefaultAccountManager) GetUsersFromAccount(accountID, userID string) (
Status: string(UserStatusActive), Status: string(UserStatusActive),
IsServiceUser: localUser.IsServiceUser, IsServiceUser: localUser.IsServiceUser,
NonDeletable: localUser.NonDeletable, NonDeletable: localUser.NonDeletable,
Permissions: UserPermissions{DashboardView: dashboardViewPermissions},
} }
} }
userInfos = append(userInfos, info) userInfos = append(userInfos, info)

View File

@ -709,6 +709,83 @@ 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 UserRole
limitedViewSettings bool
expectedDashboardPermissions string
}{
{
name: "Regular user, no limited view settings",
role: UserRoleUser,
limitedViewSettings: false,
expectedDashboardPermissions: "limited",
},
{
name: "Admin user, no limited view settings",
role: UserRoleAdmin,
limitedViewSettings: false,
expectedDashboardPermissions: "full",
},
{
name: "Owner, no limited view settings",
role: UserRoleOwner,
limitedViewSettings: false,
expectedDashboardPermissions: "full",
},
{
name: "Regular user, limited view settings",
role: UserRoleUser,
limitedViewSettings: true,
expectedDashboardPermissions: "blocked",
},
{
name: "Admin user, limited view settings",
role: UserRoleAdmin,
limitedViewSettings: true,
expectedDashboardPermissions: "full",
},
{
name: "Owner, limited view settings",
role: UserRoleOwner,
limitedViewSettings: true,
expectedDashboardPermissions: "full",
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
store := newStore(t)
account := newAccountWithId(mockAccountID, mockUserID, "")
account.Users["normal_user1"] = NewUser("normal_user1", testCase.role, false, false, "", []string{}, UserIssuedAPI)
account.Settings.RegularUsersViewBlocked = testCase.limitedViewSettings
delete(account.Users, mockUserID)
err := store.SaveAccount(account)
if err != nil {
t.Fatalf("Error when saving account: %s", err)
}
am := DefaultAccountManager{
Store: store,
eventStore: &activity.InMemoryEventStore{},
}
users, err := am.ListUsers(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 := newStore(t) store := newStore(t)
account := newAccountWithId(mockAccountID, mockUserID, "") account := newAccountWithId(mockAccountID, mockUserID, "")