diff --git a/management/server/account.go b/management/server/account.go index 8588cf343..d9030007d 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -85,7 +85,8 @@ type AccountManager interface { GetAllPATs(accountID string, initiatorUserID string, targetUserID string) ([]*PersonalAccessToken, error) UpdatePeerSSHKey(peerID string, sshKey string) 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) SaveGroup(accountID, userID string, group *Group) 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. 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 bool @@ -188,6 +192,7 @@ func (s *Settings) Copy() *Settings { JWTGroupsClaimName: s.JWTGroupsClaimName, GroupsPropagationEnabled: s.GroupsPropagationEnabled, JWTAllowGroups: s.JWTAllowGroups, + RegularUsersViewBlocked: s.RegularUsersViewBlocked, } if s.Extra != nil { settings.Extra = s.Extra.Copy() @@ -226,6 +231,10 @@ type Account struct { Settings *Settings `gorm:"embedded;embeddedPrefix:settings_"` } +type UserPermissions struct { + DashboardView string `json:"dashboard_view"` +} + type UserInfo struct { ID string `json:"id"` Email string `json:"email"` @@ -239,6 +248,7 @@ type UserInfo struct { LastLogin time.Time `json:"last_login"` Issued string `json:"issued"` IntegrationReference IntegrationReference `json:"-"` + Permissions UserPermissions `json:"permissions"` } // 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, PeerLoginExpiration: DefaultPeerLoginExpiration, GroupsPropagationEnabled: true, + RegularUsersViewBlocked: true, }, } diff --git a/management/server/group.go b/management/server/group.go index 43d48e622..59f05a354 100644 --- a/management/server/group.go +++ b/management/server/group.go @@ -63,7 +63,7 @@ func (g *Group) Copy() *Group { } // 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) defer unlock() @@ -72,6 +72,15 @@ func (am *DefaultAccountManager) GetGroup(accountID, groupID string) (*Group, er 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] if ok { 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) } +// 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 func (am *DefaultAccountManager) GetGroupByName(groupName, accountID string) (*Group, error) { unlock := am.Store.AcquireAccountLock(accountID) diff --git a/management/server/http/accounts_handler.go b/management/server/http/accounts_handler.go index 71088cfaf..d3c9954d3 100644 --- a/management/server/http/accounts_handler.go +++ b/management/server/http/accounts_handler.go @@ -76,6 +76,7 @@ func (h *AccountsHandler) UpdateAccount(w http.ResponseWriter, r *http.Request) settings := &server.Settings{ PeerLoginExpirationEnabled: req.Settings.PeerLoginExpirationEnabled, PeerLoginExpiration: time.Duration(float64(time.Second.Nanoseconds()) * float64(req.Settings.PeerLoginExpiration)), + RegularUsersViewBlocked: req.Settings.RegularUsersViewBlocked, } if req.Settings.Extra != nil { @@ -143,6 +144,7 @@ func toAccountResponse(account *server.Account) *api.Account { JwtGroupsEnabled: &account.Settings.JWTGroupsEnabled, JwtGroupsClaimName: &account.Settings.JWTGroupsClaimName, JwtAllowGroups: &jwtAllowGroups, + RegularUsersViewBlocked: account.Settings.RegularUsersViewBlocked, } if account.Settings.Extra != nil { diff --git a/management/server/http/accounts_handler_test.go b/management/server/http/accounts_handler_test.go index fd2c4bfcd..9d174d0be 100644 --- a/management/server/http/accounts_handler_test.go +++ b/management/server/http/accounts_handler_test.go @@ -69,6 +69,7 @@ func TestAccounts_AccountsHandler(t *testing.T) { Settings: &server.Settings{ PeerLoginExpirationEnabled: false, PeerLoginExpiration: time.Hour, + RegularUsersViewBlocked: true, }, }, adminUser) @@ -96,6 +97,7 @@ func TestAccounts_AccountsHandler(t *testing.T) { JwtGroupsClaimName: sr(""), JwtGroupsEnabled: br(false), JwtAllowGroups: &[]string{}, + RegularUsersViewBlocked: true, }, expectedArray: true, expectedID: accountID, @@ -114,6 +116,7 @@ func TestAccounts_AccountsHandler(t *testing.T) { JwtGroupsClaimName: sr(""), JwtGroupsEnabled: br(false), JwtAllowGroups: &[]string{}, + RegularUsersViewBlocked: false, }, expectedArray: false, expectedID: accountID, @@ -123,7 +126,7 @@ func TestAccounts_AccountsHandler(t *testing.T) { expectedBody: true, requestType: http.MethodPut, 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, expectedSettings: api.AccountSettings{ PeerLoginExpiration: 15552000, @@ -132,6 +135,7 @@ func TestAccounts_AccountsHandler(t *testing.T) { JwtGroupsClaimName: sr("roles"), JwtGroupsEnabled: br(true), JwtAllowGroups: &[]string{"test"}, + RegularUsersViewBlocked: true, }, expectedArray: false, expectedID: accountID, @@ -141,7 +145,7 @@ func TestAccounts_AccountsHandler(t *testing.T) { expectedBody: true, requestType: http.MethodPut, 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, expectedSettings: api.AccountSettings{ PeerLoginExpiration: 554400, @@ -150,6 +154,7 @@ func TestAccounts_AccountsHandler(t *testing.T) { JwtGroupsClaimName: sr("groups"), JwtGroupsEnabled: br(true), JwtAllowGroups: &[]string{}, + RegularUsersViewBlocked: true, }, expectedArray: false, expectedID: accountID, diff --git a/management/server/http/api/openapi.yml b/management/server/http/api/openapi.yml index 7ec2310af..281089396 100644 --- a/management/server/http/api/openapi.yml +++ b/management/server/http/api/openapi.yml @@ -54,6 +54,10 @@ components: description: Period of time after which peer login expires (seconds). type: integer example: 43200 + regular_users_view_blocked: + description: Allows blocking regular users from viewing parts of the system. + type: boolean + example: true groups_propagation_enabled: description: Allows propagate the new user auto groups to peers that belongs to the user type: boolean @@ -77,6 +81,7 @@ components: required: - peer_login_expiration_enabled - peer_login_expiration + - regular_users_view_blocked AccountExtraSettings: type: object properties: @@ -144,6 +149,8 @@ components: description: How user was issued by API or Integration type: string example: api + permissions: + $ref: '#/components/schemas/UserPermissions' required: - id - email @@ -152,6 +159,14 @@ components: - auto_groups - status - 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: type: object properties: @@ -589,8 +604,6 @@ components: type: string enum: ["api", "integration", "jwt"] example: api - type: string - example: api required: - id - name diff --git a/management/server/http/api/types.gen.go b/management/server/http/api/types.gen.go index a4c492bb8..78cd83a27 100644 --- a/management/server/http/api/types.gen.go +++ b/management/server/http/api/types.gen.go @@ -69,6 +69,20 @@ const ( 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. const ( NameserverNsTypeUdp NameserverNsType = "udp" @@ -129,6 +143,13 @@ const ( UserStatusInvited UserStatus = "invited" ) +// Defines values for UserPermissionsDashboardView. +const ( + UserPermissionsDashboardViewBlocked UserPermissionsDashboardView = "blocked" + UserPermissionsDashboardViewFull UserPermissionsDashboardView = "full" + UserPermissionsDashboardViewLimited UserPermissionsDashboardView = "limited" +) + // AccessiblePeer defines model for AccessiblePeer. 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 @@ -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 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 @@ -283,8 +307,8 @@ type Group struct { // Id Group ID Id string `json:"id"` - // Issued How group was issued by API or from JWT token - Issued *string `json:"issued,omitempty"` + // Issued How the group was issued (api, integration, jwt) + Issued *GroupIssued `json:"issued,omitempty"` // Name Group Name identifier Name string `json:"name"` @@ -296,13 +320,16 @@ type Group struct { PeersCount int `json:"peers_count"` } +// GroupIssued How the group was issued (api, integration, jwt) +type GroupIssued string + // GroupMinimum defines model for GroupMinimum. type GroupMinimum struct { // Id Group ID Id string `json:"id"` - // Issued How group was issued by API or from JWT token - Issued *string `json:"issued,omitempty"` + // Issued How the group was issued (api, integration, jwt) + Issued *GroupMinimumIssued `json:"issued,omitempty"` // Name Group Name identifier Name string `json:"name"` @@ -311,6 +338,9 @@ type GroupMinimum struct { PeersCount int `json:"peers_count"` } +// GroupMinimumIssued How the group was issued (api, integration, jwt) +type GroupMinimumIssued string + // GroupRequest defines model for GroupRequest. type GroupRequest struct { // Name Group name identifier @@ -1072,7 +1102,8 @@ type User struct { LastLogin *time.Time `json:"last_login,omitempty"` // 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 string `json:"role"` @@ -1102,6 +1133,15 @@ type UserCreateRequest struct { 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. type UserRequest struct { // AutoGroups Group IDs to auto-assign to peers registered by this user diff --git a/management/server/http/groups_handler.go b/management/server/http/groups_handler.go index b37f4fd2f..56d06595f 100644 --- a/management/server/http/groups_handler.go +++ b/management/server/http/groups_handler.go @@ -35,19 +35,25 @@ func NewGroupsHandler(accountManager server.AccountManager, authCfg AuthCfg) *Gr // GetAllGroups list for the account func (h *GroupsHandler) GetAllGroups(w http.ResponseWriter, r *http.Request) { claims := h.claimsExtractor.FromRequestContext(r) - account, _, err := h.accountManager.GetAccountFromToken(claims) + account, user, err := h.accountManager.GetAccountFromToken(claims) if err != nil { log.Error(err) http.Redirect(w, r, "/", http.StatusInternalServerError) return } - var groups []*api.Group - for _, g := range account.Groups { - groups = append(groups, toGroupResponse(account, g)) + groups, err := h.accountManager.GetAllGroups(account.Id, user.Id) + if err != nil { + 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 @@ -207,7 +213,7 @@ func (h *GroupsHandler) DeleteGroup(w http.ResponseWriter, r *http.Request) { // GetGroup returns a group func (h *GroupsHandler) GetGroup(w http.ResponseWriter, r *http.Request) { claims := h.claimsExtractor.FromRequestContext(r) - account, _, err := h.accountManager.GetAccountFromToken(claims) + account, user, err := h.accountManager.GetAccountFromToken(claims) if err != nil { util.WriteError(err, w) return @@ -221,7 +227,7 @@ func (h *GroupsHandler) GetGroup(w http.ResponseWriter, r *http.Request) { return } - group, err := h.accountManager.GetGroup(account.Id, groupID) + group, err := h.accountManager.GetGroup(account.Id, groupID, user.Id) if err != nil { util.WriteError(err, w) return @@ -239,7 +245,7 @@ func toGroupResponse(account *server.Account, group *server.Group) *api.Group { gr := api.Group{ Id: group.ID, Name: group.Name, - Issued: &group.Issued, + Issued: (*api.GroupIssued)(&group.Issued), } for _, pid := range group.Peers { diff --git a/management/server/http/groups_handler_test.go b/management/server/http/groups_handler_test.go index 5b47b1208..303efc9d7 100644 --- a/management/server/http/groups_handler_test.go +++ b/management/server/http/groups_handler_test.go @@ -37,7 +37,7 @@ func initGroupTestData(user *server.User, groups ...*server.Group) *GroupsHandle } return nil }, - GetGroupFunc: func(_, groupID string) (*server.Group, error) { + GetGroupFunc: func(_, groupID, _ string) (*server.Group, error) { if groupID != "idofthegroup" { return nil, status.Errorf(status.NotFound, "not found") } @@ -187,7 +187,7 @@ func TestWriteGroup(t *testing.T) { expectedGroup: &api.Group{ Id: "id-was-set", Name: "Default POSTed Group", - Issued: &groupIssuedAPI, + Issued: (*api.GroupIssued)(&groupIssuedAPI), }, }, { @@ -209,7 +209,7 @@ func TestWriteGroup(t *testing.T) { expectedGroup: &api.Group{ Id: "id-existed", Name: "Default POSTed Group", - Issued: &groupIssuedAPI, + Issued: (*api.GroupIssued)(&groupIssuedAPI), }, }, { @@ -240,7 +240,7 @@ func TestWriteGroup(t *testing.T) { expectedGroup: &api.Group{ Id: "id-jwt-group", Name: "changed", - Issued: &groupIssuedJWT, + Issued: (*api.GroupIssued)(&groupIssuedJWT), }, }, } diff --git a/management/server/http/users_handler.go b/management/server/http/users_handler.go index 5d92b65e5..ed8a3f543 100644 --- a/management/server/http/users_handler.go +++ b/management/server/http/users_handler.go @@ -288,5 +288,8 @@ func toUserResponse(user *server.UserInfo, currenUserID string) *api.User { IsBlocked: user.IsBlocked, LastLogin: &user.LastLogin, Issued: &user.Issued, + Permissions: &api.UserPermissions{ + DashboardView: (*api.UserPermissionsDashboardView)(&user.Permissions.DashboardView), + }, } } diff --git a/management/server/http/users_handler_test.go b/management/server/http/users_handler_test.go index ff886ca9f..91f19d8d8 100644 --- a/management/server/http/users_handler_test.go +++ b/management/server/http/users_handler_test.go @@ -105,7 +105,7 @@ func initUsersTestData() *UsersHandler { 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 { return nil, err } diff --git a/management/server/mock_server/account_mock.go b/management/server/mock_server/account_mock.go index f518372ed..9463498cf 100644 --- a/management/server/mock_server/account_mock.go +++ b/management/server/mock_server/account_mock.go @@ -31,7 +31,8 @@ type MockAccountManager struct { GetNetworkMapFunc func(peerKey string) (*server.NetworkMap, error) GetPeerNetworkFunc func(peerKey string) (*server.Network, 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) SaveGroupFunc func(accountID, userID string, group *server.Group) error DeleteGroupFunc func(accountID, userId, groupID string) error @@ -92,6 +93,22 @@ type MockAccountManager struct { 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 func (am *MockAccountManager) GetUsersFromAccount(accountID string, userID string) ([]*server.UserInfo, error) { if am.GetUsersFromAccountFunc != nil { @@ -243,14 +260,6 @@ func (am *MockAccountManager) AddPeer( 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 func (am *MockAccountManager) GetGroupByName(accountID, groupName string) (*server.Group, error) { if am.GetGroupFunc != nil { diff --git a/management/server/peer.go b/management/server/peer.go index 53b86e9b3..7de1b6542 100644 --- a/management/server/peer.go +++ b/management/server/peer.go @@ -54,6 +54,11 @@ func (am *DefaultAccountManager) GetPeers(accountID, userID string) ([]*nbpeer.P peers := make([]*nbpeer.Peer, 0) peersMap := make(map[string]*nbpeer.Peer) + + if !user.HasAdminPower() && !user.IsServiceUser && account.Settings.RegularUsersViewBlocked { + return peers, nil + } + for _, peer := range account.Peers { 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 @@ -738,6 +743,10 @@ func (am *DefaultAccountManager) GetPeer(accountID, peerID, userID string) (*nbp 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) if peer == nil { return nil, status.Errorf(status.NotFound, "peer with %s not found under account %s", peerID, accountID) diff --git a/management/server/peer_test.go b/management/server/peer_test.go index ee84ea47d..7f6d440bb 100644 --- a/management/server/peer_test.go +++ b/management/server/peer_test.go @@ -4,9 +4,8 @@ import ( "testing" "time" - "github.com/stretchr/testify/assert" - "github.com/rs/xid" + "github.com/stretchr/testify/assert" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" nbpeer "github.com/netbirdio/netbird/management/server/peer" @@ -392,6 +391,8 @@ func TestDefaultAccountManager_GetPeer(t *testing.T) { Id: someUser, Role: UserRoleUser, } + account.Settings.RegularUsersViewBlocked = false + err = manager.Store.SaveAccount(account) if err != nil { t.Fatal(err) @@ -480,3 +481,153 @@ func TestDefaultAccountManager_GetPeer(t *testing.T) { } 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) + + }) + } + +} diff --git a/management/server/setupkey.go b/management/server/setupkey.go index 972665527..ff6fb3204 100644 --- a/management/server/setupkey.go +++ b/management/server/setupkey.go @@ -339,6 +339,10 @@ func (am *DefaultAccountManager) ListSetupKeys(accountID, userID string) ([]*Set 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)) for _, key := range account.SetupKeys { var k *SetupKey @@ -368,6 +372,10 @@ func (am *DefaultAccountManager) GetSetupKey(accountID, userID, keyID string) (* 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 for _, key := range account.SetupKeys { if key.Id == keyID { diff --git a/management/server/setupkey_test.go b/management/server/setupkey_test.go index c22df2094..b714652f1 100644 --- a/management/server/setupkey_test.go +++ b/management/server/setupkey_test.go @@ -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) { expectedName := "Default key" expectedRevoke := false diff --git a/management/server/user.go b/management/server/user.go index f1516139b..15517db41 100644 --- a/management/server/user.go +++ b/management/server/user.go @@ -113,12 +113,20 @@ func (u *User) HasAdminPower() bool { } // 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 if autoGroups == nil { autoGroups = []string{} } + dashboardViewPermissions := "full" + if !u.HasAdminPower() { + dashboardViewPermissions = "limited" + if settings.RegularUsersViewBlocked { + dashboardViewPermissions = "blocked" + } + } + if userData == nil { return &UserInfo{ ID: u.Id, @@ -131,6 +139,9 @@ func (u *User) ToUserInfo(userData *idp.UserData) (*UserInfo, error) { IsBlocked: u.Blocked, LastLogin: u.LastLogin, Issued: u.Issued, + Permissions: UserPermissions{ + DashboardView: dashboardViewPermissions, + }, }, nil } if userData.ID != u.Id { @@ -153,6 +164,9 @@ func (u *User) ToUserInfo(userData *idp.UserData) (*UserInfo, error) { IsBlocked: u.Blocked, LastLogin: u.LastLogin, Issued: u.Issued, + Permissions: UserPermissions{ + DashboardView: dashboardViewPermissions, + }, }, nil } @@ -358,7 +372,7 @@ func (am *DefaultAccountManager) inviteNewUser(accountID, userID string, invite 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. @@ -905,9 +919,9 @@ func (am *DefaultAccountManager) SaveOrAddUser(accountID, initiatorUserID string if err != nil { 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 @@ -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 continue } - info, err := accountUser.ToUserInfo(nil) + info, err := accountUser.ToUserInfo(nil, account.Settings) if err != nil { return nil, err } @@ -1015,7 +1029,7 @@ func (am *DefaultAccountManager) GetUsersFromAccount(accountID, userID string) ( var info *UserInfo if queriedUser, contains := findUserInIDPUserdata(localUser.Id, queriedUsers); contains { - info, err = localUser.ToUserInfo(queriedUser) + info, err = localUser.ToUserInfo(queriedUser, account.Settings) if err != nil { return nil, err } @@ -1024,6 +1038,15 @@ func (am *DefaultAccountManager) GetUsersFromAccount(accountID, userID string) ( if localUser.IsServiceUser { name = localUser.ServiceUserName } + + dashboardViewPermissions := "full" + if !localUser.HasAdminPower() { + dashboardViewPermissions = "limited" + if account.Settings.RegularUsersViewBlocked { + dashboardViewPermissions = "blocked" + } + } + info = &UserInfo{ ID: localUser.Id, Email: "", @@ -1033,6 +1056,7 @@ func (am *DefaultAccountManager) GetUsersFromAccount(accountID, userID string) ( Status: string(UserStatusActive), IsServiceUser: localUser.IsServiceUser, NonDeletable: localUser.NonDeletable, + Permissions: UserPermissions{DashboardView: dashboardViewPermissions}, } } userInfos = append(userInfos, info) diff --git a/management/server/user_test.go b/management/server/user_test.go index 50cd726ef..e34aa406d 100644 --- a/management/server/user_test.go +++ b/management/server/user_test.go @@ -709,6 +709,83 @@ func TestDefaultAccountManager_ListUsers(t *testing.T) { 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) { store := newStore(t) account := newAccountWithId(mockAccountID, mockUserID, "")