From 66f02a68671ef26b1a9f9eb9169113e194e283f8 Mon Sep 17 00:00:00 2001 From: Wroud Date: Fri, 29 May 2020 12:44:49 +0300 Subject: [PATCH] chore(authentication): users administration improvements CB-109 --- .../src/Administration/RolesManagerService.ts | 47 ++++++++++++ .../CreateUserDialog/CreateUserDialog.tsx | 1 + .../CreateUserDialogController.ts | 30 +++++++- .../Users/UsersAdministration.tsx | 1 + .../Users/UsersAdministrationController.ts | 13 +++- .../Users/UsersAdministrationService.ts | 2 +- .../Administration/Users/UsersTable/User.tsx | 2 +- .../src/Administration/UsersManagerService.ts | 49 ++++++++++--- .../packages/authentication/src/manifest.ts | 2 + .../Authentication/Users/getRolesList.gql | 2 +- webapp/packages/core/src/sdk/sdk.ts | 72 +++++++++---------- 11 files changed, 167 insertions(+), 54 deletions(-) create mode 100644 webapp/packages/authentication/src/Administration/RolesManagerService.ts diff --git a/webapp/packages/authentication/src/Administration/RolesManagerService.ts b/webapp/packages/authentication/src/Administration/RolesManagerService.ts new file mode 100644 index 0000000000..8aa749f970 --- /dev/null +++ b/webapp/packages/authentication/src/Administration/RolesManagerService.ts @@ -0,0 +1,47 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { injectable } from '@dbeaver/core/di'; +import { + CachedResource, GraphQLService, AdminRoleInfo +} from '@dbeaver/core/sdk'; + +@injectable() +export class RolesManagerService { + readonly roles = new CachedResource( + [], + this.refreshAsync.bind(this), + data => !!data.length + ) + constructor( + private graphQLService: GraphQLService, + ) { + } + + private async refreshAsync( + data: AdminRoleInfo[], + _: any, + update: boolean, + roleId?: string + ): Promise { + const { roles } = await this.graphQLService.gql.getRolesList({ roleId }); + + if (!roleId) { + return roles as AdminRoleInfo[]; + } + + const index = data.findIndex(role => role.roleId === roleId); + if (index !== -1) { + data.splice(index, 1, ...roles as AdminRoleInfo[]); + } else { + data.push(...roles as AdminRoleInfo[]); + } + + return data; + } +} diff --git a/webapp/packages/authentication/src/Administration/Users/CreateUserDialog/CreateUserDialog.tsx b/webapp/packages/authentication/src/Administration/Users/CreateUserDialog/CreateUserDialog.tsx index 8583de6046..cd28ab1f64 100644 --- a/webapp/packages/authentication/src/Administration/Users/CreateUserDialog/CreateUserDialog.tsx +++ b/webapp/packages/authentication/src/Administration/Users/CreateUserDialog/CreateUserDialog.tsx @@ -118,6 +118,7 @@ export const CreateUserDialog: DialogComponent = observer( type='role' name='text' value={controller.credentials.role} + placeholder={controller.roles.join(', ')} onChange={handleRoleChange} disabled={controller.isCreating} mod='surface' diff --git a/webapp/packages/authentication/src/Administration/Users/CreateUserDialog/CreateUserDialogController.ts b/webapp/packages/authentication/src/Administration/Users/CreateUserDialog/CreateUserDialogController.ts index dea6653869..3c2bcba7b0 100644 --- a/webapp/packages/authentication/src/Administration/Users/CreateUserDialog/CreateUserDialogController.ts +++ b/webapp/packages/authentication/src/Administration/Users/CreateUserDialog/CreateUserDialogController.ts @@ -6,7 +6,7 @@ * you may not use this file except in compliance with the License. */ -import { observable } from 'mobx'; +import { observable, computed } from 'mobx'; import { ErrorDetailsDialog } from '@dbeaver/core/app'; import { injectable, IInitializableController, IDestructibleController } from '@dbeaver/core/di'; @@ -14,6 +14,7 @@ import { CommonDialogService } from '@dbeaver/core/dialogs'; import { NotificationService } from '@dbeaver/core/eventsLog'; import { GQLErrorCatcher } from '@dbeaver/core/sdk'; +import { RolesManagerService } from '../../RolesManagerService'; import { UsersManagerService } from '../../UsersManagerService'; @@ -26,6 +27,9 @@ export class CreateUserDialogController implements IInitializableController, IDe role: '', }; + @computed get roles() { + return this.rolesManagerService.roles.data.map(role => role.roleId); + } readonly error = new GQLErrorCatcher(); private isDistructed = false; private close!: () => void; @@ -34,10 +38,12 @@ export class CreateUserDialogController implements IInitializableController, IDe private notificationService: NotificationService, private usersManagerService: UsersManagerService, private commonDialogService: CommonDialogService, + private rolesManagerService: RolesManagerService, ) { } init(onClose: () => void) { this.close = onClose; + this.loadRoles(); } destruct(): void { @@ -52,14 +58,14 @@ export class CreateUserDialogController implements IInitializableController, IDe this.isCreating = true; let isUserCreated = false; try { - const user = await this.usersManagerService.create(this.credentials.login); + const user = await this.usersManagerService.create(this.credentials.login, false); isUserCreated = !!user; await this.usersManagerService.updateCredentials(user.userId, { password: this.credentials.password }); await this.usersManagerService.grantRole(user.userId, this.credentials.role); this.close(); } catch (exception) { if (isUserCreated) { - this.usersManagerService.delete(this.credentials.login); + await this.deleteUser(this.credentials.login); } if (!this.error.catch(exception) || this.isDistructed) { this.notificationService.logException(exception, 'Error creating new user'); @@ -74,4 +80,22 @@ export class CreateUserDialogController implements IInitializableController, IDe this.commonDialogService.open(ErrorDetailsDialog, this.error.exception); } } + + private async deleteUser(userId: string) { + try { + await this.usersManagerService.delete(userId); + } catch (exception) { + if (!this.error.catch(exception) || this.isDistructed) { + this.notificationService.logException(exception, 'Error deleting partially created user'); + } + } + } + + private async loadRoles() { + try { + await this.rolesManagerService.roles.load(); + } catch (exception) { + this.notificationService.logException(exception, 'Can\'t load roles'); + } + } } diff --git a/webapp/packages/authentication/src/Administration/Users/UsersAdministration.tsx b/webapp/packages/authentication/src/Administration/Users/UsersAdministration.tsx index 4fa892b3eb..03ee6f9997 100644 --- a/webapp/packages/authentication/src/Administration/Users/UsersAdministration.tsx +++ b/webapp/packages/authentication/src/Administration/Users/UsersAdministration.tsx @@ -54,6 +54,7 @@ export const UsersAdministration = observer(function UsersAdministration() { + diff --git a/webapp/packages/authentication/src/Administration/Users/UsersAdministrationController.ts b/webapp/packages/authentication/src/Administration/Users/UsersAdministrationController.ts index 37546ef1c8..dda03474a6 100644 --- a/webapp/packages/authentication/src/Administration/Users/UsersAdministrationController.ts +++ b/webapp/packages/authentication/src/Administration/Users/UsersAdministrationController.ts @@ -40,6 +40,16 @@ export class UsersAdministrationController { this.commonDialogService.open(CreateUserDialog, null); } + update = async () => { + try { + await this.usersManagerService.users.refresh(undefined); + } catch (exception) { + if (!this.error.catch(exception)) { + this.notificationService.logException(exception, 'Users update failed'); + } + } + } + delete = async () => { if (this.isDeleting) { return; @@ -52,7 +62,8 @@ export class UsersAdministrationController { await this.usersManagerService.delete(userId); } } - await this.usersManagerService.users.refresh(); + this.selectedItems.clear(); + await this.usersManagerService.users.refresh(undefined); } catch (exception) { if (!this.error.catch(exception)) { this.notificationService.logException(exception, 'User delete failed'); diff --git a/webapp/packages/authentication/src/Administration/Users/UsersAdministrationService.ts b/webapp/packages/authentication/src/Administration/Users/UsersAdministrationService.ts index a41364ccc3..2c337e6124 100644 --- a/webapp/packages/authentication/src/Administration/Users/UsersAdministrationService.ts +++ b/webapp/packages/authentication/src/Administration/Users/UsersAdministrationService.ts @@ -59,7 +59,7 @@ export class UsersAdministrationService extends Bootstrap { private async loadUsers() { try { - await this.usersManagerService.users.load(); + await this.usersManagerService.users.load(undefined); } catch (exception) { this.notificationService.logException(exception, 'Error occurred while loading users'); } diff --git a/webapp/packages/authentication/src/Administration/Users/UsersTable/User.tsx b/webapp/packages/authentication/src/Administration/Users/UsersTable/User.tsx index 97bcdca0a7..bea346f02f 100644 --- a/webapp/packages/authentication/src/Administration/Users/UsersTable/User.tsx +++ b/webapp/packages/authentication/src/Administration/Users/UsersTable/User.tsx @@ -23,7 +23,7 @@ export const User = observer(function User({ user }: Props) { return styled(useStyles())( {user.userId} - {user.grantedRoles?.join(', ')} + {user.grantedRoles.join(', ')} ); diff --git a/webapp/packages/authentication/src/Administration/UsersManagerService.ts b/webapp/packages/authentication/src/Administration/UsersManagerService.ts index 71e565ebd4..042cad6fe3 100644 --- a/webapp/packages/authentication/src/Administration/UsersManagerService.ts +++ b/webapp/packages/authentication/src/Administration/UsersManagerService.ts @@ -9,33 +9,47 @@ import { injectable } from '@dbeaver/core/di'; import { CachedResource, GraphQLService, AdminUserInfo } from '@dbeaver/core/sdk'; +import { AuthInfoService } from '../AuthInfoService'; import { AuthProviderService } from '../AuthProviderService'; @injectable() export class UsersManagerService { - readonly users = new CachedResource([], this.refreshAsync.bind(this), data => !!data.length) + readonly users = new CachedResource( + [], + this.refreshAsync.bind(this), + (data, _, userId) => (userId ? data.some(user => user.userId === userId) : !!data.length) + ) constructor( private graphQLService: GraphQLService, private authProviderService: AuthProviderService, + private authInfoService: AuthInfoService, ) { } - async create(userId: string): Promise { + async create(userId: string, update?: boolean): Promise { const { user } = await this.graphQLService.gql.createUser({ userId }); + if (update) { // TODO: maybe better to do refresh - this.users.data.push(user as AdminUserInfo); + this.users.data.push(user as AdminUserInfo); + } return user as AdminUserInfo; } async grantRole(userId: string, roleId: string) { await this.graphQLService.gql.grantUserRole({ userId, roleId }); + await this.users.refresh(userId); } - async delete(userId: string) { + async delete(userId: string, update?: boolean) { + if (this.authInfoService.userInfo?.userId === userId) { + throw new Error('You can\'t delete current logged user'); + } await this.graphQLService.gql.deleteUser({ userId }); - // TODO: maybe better to do refresh - this.users.data.splice(this.users.data.findIndex(user => user.userId === userId), 1); + + if (update) { + await this.users.refresh(userId); + } } async updateCredentials(userId: string, credentials: Record) { @@ -49,10 +63,25 @@ export class UsersManagerService { }); } - private async refreshAsync(data: AdminUserInfo[]): Promise { - const { users } = await this.graphQLService.gql.getUsersList(); + private async refreshAsync( + data: AdminUserInfo[], + _: any, + update: boolean, + userId?: string + ): Promise { + const { users } = await this.graphQLService.gql.getUsersList({ userId }); - // TODO: temporary before full implementation was provided - return users as AdminUserInfo[]; + if (!userId) { + return users as AdminUserInfo[]; + } + + const index = data.findIndex(user => user.userId === userId); + if (index !== -1) { + data.splice(index, 1, ...users as AdminUserInfo[]); + } else { + data.push(...users as AdminUserInfo[]); + } + + return data; } } diff --git a/webapp/packages/authentication/src/manifest.ts b/webapp/packages/authentication/src/manifest.ts index e8bda7e10a..8534e69f51 100644 --- a/webapp/packages/authentication/src/manifest.ts +++ b/webapp/packages/authentication/src/manifest.ts @@ -8,6 +8,7 @@ import { IServiceInjector, PluginManifest } from '@dbeaver/core/di'; +import { RolesManagerService } from './Administration/RolesManagerService'; import { UsersAdministrationService } from './Administration/Users/UsersAdministrationService'; import { UsersManagerService } from './Administration/UsersManagerService'; import { AuthenticationService } from './AuthenticationService'; @@ -30,6 +31,7 @@ export const manifest: PluginManifest = { AuthMenuService, UsersAdministrationService, UsersManagerService, + RolesManagerService, ], async initialize(services: IServiceInjector) { diff --git a/webapp/packages/core/src/queries/Authentication/Users/getRolesList.gql b/webapp/packages/core/src/queries/Authentication/Users/getRolesList.gql index 00c4f2ee83..b7a323567a 100644 --- a/webapp/packages/core/src/queries/Authentication/Users/getRolesList.gql +++ b/webapp/packages/core/src/queries/Authentication/Users/getRolesList.gql @@ -3,6 +3,6 @@ query getRolesList($roleId: ID) { roleId roleName - rolePermissions + #rolePermissions } } \ No newline at end of file diff --git a/webapp/packages/core/src/sdk/sdk.ts b/webapp/packages/core/src/sdk/sdk.ts index 12aa229ea6..f424d9d083 100644 --- a/webapp/packages/core/src/sdk/sdk.ts +++ b/webapp/packages/core/src/sdk/sdk.ts @@ -1,6 +1,5 @@ /* eslint-disable max-len */ import { GraphQLClient } from 'graphql-request'; - export type Maybe = T; /** All built-in and custom scalars, mapped to their actual values */ @@ -687,7 +686,7 @@ export type NavGetStructContainersQueryVariables = { }; -export type NavGetStructContainersQuery = { navGetStructContainers: { catalogList: Array>; schemaList: Array> } }; +export type NavGetStructContainersQuery = { navGetStructContainers: { catalogList: Array>, schemaList: Array> } }; export type CloseConnectionMutationVariables = { id: Scalars['ID']; @@ -728,7 +727,7 @@ export type DriverPropertiesQueryVariables = { export type DriverPropertiesQuery = { driver: Array<( Pick & { driverProperties?: Maybe>>> } - )>; }; + )> }; export type GetDriverByIdQueryVariables = { driverId: Scalars['ID']; @@ -759,7 +758,7 @@ export type NavNodeChildrenQueryVariables = { export type NavNodeChildrenQuery = { navNodeChildren: Array<( Pick & { object?: Maybe> } - )>; }; + )> }; export type NavNodeInfoQueryVariables = { nodePath: Scalars['ID']; @@ -769,7 +768,7 @@ export type NavNodeInfoQueryVariables = { export type NavNodeInfoQuery = { navNodeInfo: ( Pick & { object?: Maybe> } - ); }; + ) }; export type NavRefreshNodeQueryVariables = { nodePath: Scalars['ID']; @@ -789,8 +788,8 @@ export type QueryChildrenDatabaseObjectInfoQuery = { childrenDatabaseObjectInfo: & { object?: Maybe<( Pick & { properties?: Maybe>>> } - )>; } - )>; }; + )> } + )> }; export type QueryDatabaseObjectInfoQueryVariables = { nodeId: Scalars['ID']; @@ -803,8 +802,8 @@ export type QueryDatabaseObjectInfoQuery = { objectInfo: ( & { object?: Maybe<( Pick & { properties?: Maybe>>> } - )>; } - ); }; + )> } + ) }; export type ReadSessionLogQueryVariables = { maxEntries: Scalars['Int']; @@ -847,7 +846,7 @@ export type GetRolesListQueryVariables = { }; -export type GetRolesListQuery = { roles: Array>> }; +export type GetRolesListQuery = { roles: Array>> }; export type GetUsersListQueryVariables = { userId?: Maybe; @@ -892,7 +891,7 @@ export type GetAuthProvidersQueryVariables = {}; export type GetAuthProvidersQuery = { providers: Array<( Pick & { credentialParameters: Array> } - )>; }; + )> }; export type GetSessionUserQueryVariables = {}; @@ -907,7 +906,7 @@ export type AsyncExportTaskStatusMutationVariables = { export type AsyncExportTaskStatusMutation = { taskInfo: ( Pick & { error?: Maybe> } - ); }; + ) }; export type ExportDataFromContainerQueryVariables = { connectionId: Scalars['ID']; @@ -919,7 +918,7 @@ export type ExportDataFromContainerQueryVariables = { export type ExportDataFromContainerQuery = { taskInfo: ( Pick & { error?: Maybe> } - ); }; + ) }; export type ExportDataFromResultsQueryVariables = { connectionId: Scalars['ID']; @@ -932,7 +931,7 @@ export type ExportDataFromResultsQueryVariables = { export type ExportDataFromResultsQuery = { taskInfo: ( Pick & { error?: Maybe> } - ); }; + ) }; export type GetDataTransferProcessorsQueryVariables = {}; @@ -940,7 +939,7 @@ export type GetDataTransferProcessorsQueryVariables = {}; export type GetDataTransferProcessorsQuery = { processors: Array<( Pick & { properties?: Maybe>>> } - )>; }; + )> }; export type RemoveDataTransferFileQueryVariables = { dataFileId: Scalars['String']; @@ -966,10 +965,10 @@ export type AsyncSqlExecuteQueryMutation = { taskInfo: ( & { resultSet?: Maybe<( Pick & { columns?: Maybe>>> } - )>; } - )>; } - )>; error?: Maybe>; } - ); }; + )> } + )> } + )>, error?: Maybe> } + ) }; export type AsyncTaskCancelMutationVariables = { taskId: Scalars['String']; @@ -992,10 +991,10 @@ export type AsyncTaskStatusMutation = { taskInfo: ( & { resultSet?: Maybe<( Pick & { columns?: Maybe>>> } - )>; } - )>; } - )>; error?: Maybe>; } - ); }; + )> } + )> } + )>, error?: Maybe> } + ) }; export type ExecuteSqlQueryMutationVariables = { connectionId: Scalars['ID']; @@ -1012,9 +1011,9 @@ export type ExecuteSqlQueryMutation = { result?: Maybe<( & { resultSet?: Maybe<( Pick & { columns?: Maybe>>> } - )>; } - )>; } - )>; }; + )> } + )> } + )> }; export type MetadataGetNodeDdlQueryVariables = { nodeId: Scalars['ID']; @@ -1056,9 +1055,9 @@ export type ReadDataFromContainerMutation = { readDataFromContainer?: Maybe<( & { resultSet?: Maybe<( Pick & { columns?: Maybe>>> } - )>; } - )>; } - )>; }; + )> } + )> } + )> }; export type SqlContextCreateMutationVariables = { connectionId: Scalars['ID']; @@ -1110,8 +1109,8 @@ export type UpdateResultsDataMutation = { result?: Maybe<( & { results: Array<( Pick & { resultSet?: Maybe> } - )>; } - )>; }; + )> } + )> }; export type OpenSessionMutationVariables = {}; @@ -1119,7 +1118,7 @@ export type OpenSessionMutationVariables = {}; export type OpenSessionMutation = { session: ( Pick & { connections: Array> } - ); }; + ) }; export type ServerConfigQueryVariables = {}; @@ -1127,7 +1126,7 @@ export type ServerConfigQueryVariables = {}; export type ServerConfigQuery = { serverConfig: ( Pick & { supportedLanguages: Array> } - ); }; + ) }; export type SessionPermissionsQueryVariables = {}; @@ -1140,7 +1139,7 @@ export type SessionStateQueryVariables = {}; export type SessionStateQuery = { sessionState: ( Pick & { connections: Array> } - ); }; + ) }; export type TouchSessionMutationVariables = {}; @@ -1381,7 +1380,6 @@ export const GetRolesListDocument = ` roles: listRoles(roleId: $roleId) { roleId roleName - rolePermissions } } `; @@ -1965,7 +1963,7 @@ export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper = }, touchSession(variables?: TouchSessionMutationVariables): Promise { return withWrapper(() => client.request(TouchSessionDocument, variables)); - }, + } }; } -export type Sdk = ReturnType; +export type Sdk = ReturnType; \ No newline at end of file