diff --git a/management/cmd/management.go b/management/cmd/management.go index d6735f955..dd11a1953 100644 --- a/management/cmd/management.go +++ b/management/cmd/management.go @@ -203,7 +203,7 @@ var ( } permissionsManager := integrations.InitPermissionsManager(store) - userManager := users.NewManager(store) + userManager := users.NewManager(store, permissionsManager) extraSettingsManager := integrations.NewManager(eventStore) settingsManager := settings.NewManager(store, userManager, extraSettingsManager, permissionsManager) peersManager := peers.NewManager(store, permissionsManager) @@ -275,8 +275,9 @@ var ( resourcesManager := resources.NewManager(store, permissionsManager, groupsManager, accountManager) routersManager := routers.NewManager(store, permissionsManager, accountManager) networksManager := networks.NewManager(store, permissionsManager, resourcesManager, routersManager, accountManager) + usersManager := users.NewManager(store, permissionsManager) - httpAPIHandler, err := nbhttp.NewAPIHandler(ctx, accountManager, networksManager, resourcesManager, routersManager, groupsManager, geo, authManager, appMetrics, integratedPeerValidator, proxyController, permissionsManager, peersManager, settingsManager) + httpAPIHandler, err := nbhttp.NewAPIHandler(ctx, accountManager, networksManager, resourcesManager, routersManager, groupsManager, geo, authManager, appMetrics, integratedPeerValidator, proxyController, permissionsManager, peersManager, settingsManager, usersManager) if err != nil { return fmt.Errorf("failed creating HTTP API handler: %v", err) diff --git a/management/server/http/api/openapi.yml b/management/server/http/api/openapi.yml index 9418864d7..1e3b0c443 100644 --- a/management/server/http/api/openapi.yml +++ b/management/server/http/api/openapi.yml @@ -2013,6 +2013,41 @@ components: - policy_name - icmp_type - icmp_code + RolePermissions: + type: object + properties: + role: + type: string + example: admin + modules: + type: object + additionalProperties: + type: object + additionalProperties: + type: boolean + propertyNames: + type: string + enum: + - read + - write + propertyNames: + type: string + enum: + - read + - write + example: {"networks": { "read": true, "write": false}, "peers": { "read": false, "write": false} } + default: + type: object + additionalProperties: + type: boolean + propertyNames: + type: string + enum: + - read + - write + required: + - default + - role responses: not_found: description: Resource not found @@ -2443,6 +2478,31 @@ paths: "$ref": "#/components/responses/forbidden" '500': "$ref": "#/components/responses/internal_error" + /api/users/roles: + get: + summary: Retrieves user roles and permissions + description: Get permissions for user roles + tags: [ Users ] + security: + - BearerAuth: [ ] + - TokenAuth: [ ] + responses: + '200': + description: A JSON Array of RolePermissions objects + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/RolePermissions' + '400': + "$ref": "#/components/responses/bad_request" + '401': + "$ref": "#/components/responses/requires_authentication" + '403': + "$ref": "#/components/responses/forbidden" + '500': + "$ref": "#/components/responses/internal_error" /api/peers: get: summary: List all Peers diff --git a/management/server/http/api/types.gen.go b/management/server/http/api/types.gen.go index f4e90b5bc..9a0264582 100644 --- a/management/server/http/api/types.gen.go +++ b/management/server/http/api/types.gen.go @@ -1400,6 +1400,13 @@ type Resource struct { // ResourceType defines model for ResourceType. type ResourceType string +// RolePermissions defines model for RolePermissions. +type RolePermissions struct { + Default map[string]bool `json:"default"` + Modules *map[string]map[string]bool `json:"modules,omitempty"` + Role string `json:"role"` +} + // Route defines model for Route. type Route struct { // AccessControlGroups Access control group identifier associated with route. diff --git a/management/server/http/handler.go b/management/server/http/handler.go index 483bb989a..a09b23448 100644 --- a/management/server/http/handler.go +++ b/management/server/http/handler.go @@ -36,6 +36,7 @@ import ( "github.com/netbirdio/netbird/management/server/networks/routers" nbpeers "github.com/netbirdio/netbird/management/server/peers" "github.com/netbirdio/netbird/management/server/telemetry" + musers "github.com/netbirdio/netbird/management/server/users" ) const apiPrefix = "/api" @@ -56,6 +57,7 @@ func NewAPIHandler( permissionsManager permissions.Manager, peersManager nbpeers.Manager, settingsManager settings.Manager, + usersManager musers.Manager, ) (http.Handler, error) { authMiddleware := middleware.NewAuthMiddleware( @@ -80,7 +82,7 @@ func NewAPIHandler( accounts.AddEndpoints(accountManager, settingsManager, router) peers.AddEndpoints(accountManager, router) - users.AddEndpoints(accountManager, router) + users.AddEndpoints(accountManager, usersManager, router) setup_keys.AddEndpoints(accountManager, router) policies.AddEndpoints(accountManager, LocationManager, router) policies.AddPostureCheckEndpoints(accountManager, LocationManager, router) diff --git a/management/server/http/handlers/users/users_handler.go b/management/server/http/handlers/users/users_handler.go index 5230f9c8b..0d58d1ab5 100644 --- a/management/server/http/handlers/users/users_handler.go +++ b/management/server/http/handlers/users/users_handler.go @@ -11,6 +11,7 @@ import ( "github.com/netbirdio/netbird/management/server/account" "github.com/netbirdio/netbird/management/server/http/api" "github.com/netbirdio/netbird/management/server/http/util" + "github.com/netbirdio/netbird/management/server/permissions/roles" "github.com/netbirdio/netbird/management/server/status" "github.com/netbirdio/netbird/management/server/types" "github.com/netbirdio/netbird/management/server/users" @@ -21,9 +22,10 @@ import ( // handler is a handler that returns users of the account type handler struct { accountManager account.Manager + usersManager users.Manager } -func AddEndpoints(accountManager account.Manager, router *mux.Router) { +func AddEndpoints(accountManager account.Manager, usersManager users.Manager, router *mux.Router) { userHandler := newHandler(accountManager) router.HandleFunc("/users", userHandler.getAllUsers).Methods("GET", "OPTIONS") router.HandleFunc("/users/current", userHandler.getCurrentUser).Methods("GET", "OPTIONS") @@ -31,6 +33,7 @@ func AddEndpoints(accountManager account.Manager, router *mux.Router) { router.HandleFunc("/users/{userId}", userHandler.deleteUser).Methods("DELETE", "OPTIONS") router.HandleFunc("/users", userHandler.createUser).Methods("POST", "OPTIONS") router.HandleFunc("/users/{userId}/invite", userHandler.inviteUser).Methods("POST", "OPTIONS") + router.HandleFunc("/users/roles", userHandler.getRoles).Methods("GET", "OPTIONS") addUsersTokensEndpoint(accountManager, router) } @@ -284,6 +287,64 @@ func (h *handler) getCurrentUser(w http.ResponseWriter, r *http.Request) { util.WriteJSONObject(r.Context(), w, toUserWithPermissionsResponse(user, userID)) } +func (h *handler) getRoles(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + util.WriteErrorResponse("wrong HTTP method", http.StatusMethodNotAllowed, w) + return + } + ctx := r.Context() + userAuth, err := nbcontext.GetUserAuthFromContext(ctx) + if err != nil { + util.WriteError(r.Context(), err, w) + return + } + + accountID, userID := userAuth.AccountId, userAuth.UserId + + roles, err := h.usersManager.GetRoles(ctx, accountID, userID) + if err != nil { + util.WriteError(r.Context(), err, w) + return + } + + util.WriteJSONObject(r.Context(), w, toRolesResponse(roles)) +} + +func toRolesResponse(roles map[types.UserRole]roles.RolePermissions) []api.RolePermissions { + result := make([]api.RolePermissions, 0, len(roles)) + for _, permissions := range roles { + rolePermissions := api.RolePermissions{ + Role: string(permissions.Role), + } + + if len(permissions.AutoAllowNew) > 0 { + rolePermissions.Default = make(map[string]bool) + for k, v := range permissions.AutoAllowNew { + rolePermissions.Default[string(k)] = v + } + } + + if len(permissions.Permissions) > 0 { + modules := make(map[string]map[string]bool) + for module, operations := range permissions.Permissions { + if len(operations) == 0 { + continue + } + + access := make(map[string]bool) + for k, v := range operations { + access[string(k)] = v + } + modules[string(module)] = access + } + rolePermissions.Modules = &modules + } + + result = append(result, rolePermissions) + } + return result +} + func toUserWithPermissionsResponse(user *users.UserInfoWithPermissions, userID string) *api.User { response := toUserResponse(user.UserInfo, userID) if user.Permissions == nil { diff --git a/management/server/http/testing/testing_tools/tools.go b/management/server/http/testing/testing_tools/tools.go index 12e68e983..4c86446fa 100644 --- a/management/server/http/testing/testing_tools/tools.go +++ b/management/server/http/testing/testing_tools/tools.go @@ -121,9 +121,9 @@ func BuildApiBlackBoxWithDBState(t TB, sqlFile string, expectedPeerUpdate *serve geoMock := &geolocation.Mock{} validatorMock := server.MocIntegratedValidator{} proxyController := integrations.NewController(store) - userManager := users.NewManager(store) permissionsManager := permissions.NewManager(store) - settingsManager := settings.NewManager(store, userManager, integrations.NewManager(&activity.InMemoryEventStore{}), permissionsManager) + usersManager := users.NewManager(store, permissionsManager) + settingsManager := settings.NewManager(store, usersManager, integrations.NewManager(&activity.InMemoryEventStore{}), permissionsManager) am, err := server.BuildManager(context.Background(), store, peersUpdateManager, nil, "", "", &activity.InMemoryEventStore{}, geoMock, false, validatorMock, metrics, proxyController, settingsManager, permissionsManager) if err != nil { t.Fatalf("Failed to create manager: %v", err) @@ -144,7 +144,7 @@ func BuildApiBlackBoxWithDBState(t TB, sqlFile string, expectedPeerUpdate *serve groupsManagerMock := groups.NewManagerMock() peersManager := peers.NewManager(store, permissionsManager) - apiHandler, err := nbhttp.NewAPIHandler(context.Background(), am, networksManagerMock, resourcesManagerMock, routersManagerMock, groupsManagerMock, geoMock, authManagerMock, metrics, validatorMock, proxyController, permissionsManager, peersManager, settingsManager) + apiHandler, err := nbhttp.NewAPIHandler(context.Background(), am, networksManagerMock, resourcesManagerMock, routersManagerMock, groupsManagerMock, geoMock, authManagerMock, metrics, validatorMock, proxyController, permissionsManager, peersManager, settingsManager, usersManager) if err != nil { t.Fatalf("Failed to create API handler: %v", err) } diff --git a/management/server/permissions/manager.go b/management/server/permissions/manager.go index 2b81971c8..5149659f4 100644 --- a/management/server/permissions/manager.go +++ b/management/server/permissions/manager.go @@ -22,6 +22,7 @@ type Manager interface { ValidateAccountAccess(ctx context.Context, accountID string, user *types.User, allowOwnerAndAdmin bool) error GetRolePermissions(ctx context.Context, role types.UserRole) (roles.RolePermissions, error) + GetPermissions(ctx context.Context) map[types.UserRole]roles.RolePermissions } type managerImpl struct { @@ -107,3 +108,7 @@ func (m *managerImpl) GetRolePermissions(ctx context.Context, role types.UserRol return permissions, nil } + +func (m *managerImpl) GetPermissions(ctx context.Context) map[types.UserRole]roles.RolePermissions { + return roles.RolesMap +} diff --git a/management/server/permissions/manager_mock.go b/management/server/permissions/manager_mock.go index 840b007cd..6d7403d1c 100644 --- a/management/server/permissions/manager_mock.go +++ b/management/server/permissions/manager_mock.go @@ -38,6 +38,20 @@ func (m *MockManager) EXPECT() *MockManagerMockRecorder { return m.recorder } +// GetPermissions mocks base method. +func (m *MockManager) GetPermissions(ctx context.Context) map[types.UserRole]roles.RolePermissions { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetPermissions", ctx) + ret0, _ := ret[0].(map[types.UserRole]roles.RolePermissions) + return ret0 +} + +// GetPermissions indicates an expected call of GetPermissions. +func (mr *MockManagerMockRecorder) GetPermissions(ctx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPermissions", reflect.TypeOf((*MockManager)(nil).GetPermissions), ctx) +} + // GetRolePermissions mocks base method. func (m *MockManager) GetRolePermissions(ctx context.Context, role types.UserRole) (roles.RolePermissions, error) { m.ctrl.T.Helper() diff --git a/management/server/users/manager.go b/management/server/users/manager.go index 718eb6190..11c731e77 100644 --- a/management/server/users/manager.go +++ b/management/server/users/manager.go @@ -1,27 +1,31 @@ package users +//go:generate go run github.com/golang/mock/mockgen -package users -destination=manager_mock.go -source=./manager.go -build_flags=-mod=mod + import ( "context" - "errors" + "github.com/netbirdio/netbird/management/server/permissions" + "github.com/netbirdio/netbird/management/server/permissions/roles" + "github.com/netbirdio/netbird/management/server/status" "github.com/netbirdio/netbird/management/server/store" "github.com/netbirdio/netbird/management/server/types" ) type Manager interface { GetUser(ctx context.Context, userID string) (*types.User, error) + GetRoles(ctx context.Context, accountId, userId string) (map[types.UserRole]roles.RolePermissions, error) } type managerImpl struct { - store store.Store + store store.Store + permissionsManager permissions.Manager } -type managerMock struct { -} - -func NewManager(store store.Store) Manager { +func NewManager(store store.Store, permissionsManager permissions.Manager) Manager { return &managerImpl{ - store: store, + store: store, + permissionsManager: permissionsManager, } } @@ -29,21 +33,23 @@ func (m *managerImpl) GetUser(ctx context.Context, userID string) (*types.User, return m.store.GetUserByUserID(ctx, store.LockingStrengthShare, userID) } -func NewManagerMock() Manager { - return &managerMock{} -} - -func (m *managerMock) GetUser(ctx context.Context, userID string) (*types.User, error) { - switch userID { - case "adminUser": - return &types.User{Id: userID, Role: types.UserRoleAdmin}, nil - case "regularUser": - return &types.User{Id: userID, Role: types.UserRoleUser}, nil - case "ownerUser": - return &types.User{Id: userID, Role: types.UserRoleOwner}, nil - case "billingUser": - return &types.User{Id: userID, Role: types.UserRoleBillingAdmin}, nil - default: - return nil, errors.New("user not found") +func (m *managerImpl) GetRoles(ctx context.Context, accountId, userId string) (map[types.UserRole]roles.RolePermissions, error) { + user, err := m.store.GetUserByUserID(ctx, store.LockingStrengthShare, userId) + if err != nil { + return nil, err } + + if user.IsBlocked() { + return nil, status.NewUserBlockedError() + } + + if user.IsServiceUser { + return nil, status.NewPermissionDeniedError() + } + + if err := m.permissionsManager.ValidateAccountAccess(ctx, accountId, user, false); err != nil { + return nil, err + } + + return m.permissionsManager.GetPermissions(ctx), nil } diff --git a/management/server/users/manager_mock.go b/management/server/users/manager_mock.go new file mode 100644 index 000000000..0bedfa509 --- /dev/null +++ b/management/server/users/manager_mock.go @@ -0,0 +1,51 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./manager.go + +// Package users is a generated GoMock package. +package users + +import ( + context "context" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + types "github.com/netbirdio/netbird/management/server/types" +) + +// MockManager is a mock of Manager interface. +type MockManager struct { + ctrl *gomock.Controller + recorder *MockManagerMockRecorder +} + +// MockManagerMockRecorder is the mock recorder for MockManager. +type MockManagerMockRecorder struct { + mock *MockManager +} + +// NewMockManager creates a new mock instance. +func NewMockManager(ctrl *gomock.Controller) *MockManager { + mock := &MockManager{ctrl: ctrl} + mock.recorder = &MockManagerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockManager) EXPECT() *MockManagerMockRecorder { + return m.recorder +} + +// GetUser mocks base method. +func (m *MockManager) GetUser(ctx context.Context, userID string) (*types.User, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUser", ctx, userID) + ret0, _ := ret[0].(*types.User) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetUser indicates an expected call of GetUser. +func (mr *MockManagerMockRecorder) GetUser(ctx, userID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUser", reflect.TypeOf((*MockManager)(nil).GetUser), ctx, userID) +}