Merge pull request #67 from dbeaver/feat-routing

chore(authentication): users administration improvements
This commit is contained in:
Serge Rider
2020-05-29 13:04:32 +03:00
committed by GitHub
11 changed files with 173 additions and 54 deletions
@@ -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<AdminRoleInfo[]> {
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;
}
}
@@ -118,6 +118,7 @@ export const CreateUserDialog: DialogComponent<null, null> = observer(
type='role'
name='text'
value={controller.credentials.role}
placeholder={controller.roles.join(', ')}
onChange={handleRoleChange}
disabled={controller.isCreating}
mod='surface'
@@ -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');
}
}
}
@@ -54,6 +54,7 @@ export const UsersAdministration = observer(function UsersAdministration() {
<AdministrationTools>
<IconButton name="add" viewBox="0 0 28 28" onClick={controller.create} />
<IconButton name="trash" viewBox="0 0 28 28" onClick={controller.delete} />
<IconButton name="reload" viewBox="0 0 28 28" onClick={controller.update} />
</AdministrationTools>
<content as='div'>
<UsersTable users={controller.users} selectedItems={controller.selectedItems} />
@@ -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');
@@ -8,6 +8,7 @@
import { AdministrationItemService, AdministrationScreenService } from '@dbeaver/administration';
import { injectable, Bootstrap } from '@dbeaver/core/di';
import { NotificationService } from '@dbeaver/core/eventsLog';
import { UsersManagerService } from '../UsersManagerService';
import { CreateUser } from './CreateUser/CreateUser';
@@ -19,6 +20,7 @@ export class UsersAdministrationService extends Bootstrap {
constructor(
private administrationItemService: AdministrationItemService,
private administrationScreenService: AdministrationScreenService,
private notificationService: NotificationService,
private usersManagerService: UsersManagerService,
) {
super();
@@ -56,6 +58,10 @@ export class UsersAdministrationService extends Bootstrap {
}
private async loadUsers() {
await this.usersManagerService.users.load();
try {
await this.usersManagerService.users.load(undefined);
} catch (exception) {
this.notificationService.logException(exception, 'Error occurred while loading users');
}
}
}
@@ -23,7 +23,7 @@ export const User = observer(function User({ user }: Props) {
return styled(useStyles())(
<TableItem item={user.userId}>
<TableColumnValue>{user.userId}</TableColumnValue>
<TableColumnValue>{user.grantedRoles?.join(', ')}</TableColumnValue>
<TableColumnValue>{user.grantedRoles.join(', ')}</TableColumnValue>
<TableColumnValue></TableColumnValue>
</TableItem>
);
@@ -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<AdminUserInfo> {
async create(userId: string, update?: boolean): Promise<AdminUserInfo> {
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<string, any>) {
@@ -49,10 +63,25 @@ export class UsersManagerService {
});
}
private async refreshAsync(data: AdminUserInfo[]): Promise<AdminUserInfo[]> {
const { users } = await this.graphQLService.gql.getUsersList();
private async refreshAsync(
data: AdminUserInfo[],
_: any,
update: boolean,
userId?: string
): Promise<AdminUserInfo[]> {
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;
}
}
@@ -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) {
@@ -3,6 +3,6 @@ query getRolesList($roleId: ID) {
roleId
roleName
rolePermissions
#rolePermissions
}
}
+35 -37
View File
@@ -1,6 +1,5 @@
/* eslint-disable max-len */
import { GraphQLClient } from 'graphql-request';
export type Maybe<T> = 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<Pick<DatabaseObjectInfo, 'name' | 'description' | 'type' | 'features'>>; schemaList: Array<Pick<DatabaseObjectInfo, 'name' | 'description' | 'type' | 'features'>> } };
export type NavGetStructContainersQuery = { navGetStructContainers: { catalogList: Array<Pick<DatabaseObjectInfo, 'name' | 'description' | 'type' | 'features'>>, schemaList: Array<Pick<DatabaseObjectInfo, 'name' | 'description' | 'type' | 'features'>> } };
export type CloseConnectionMutationVariables = {
id: Scalars['ID'];
@@ -728,7 +727,7 @@ export type DriverPropertiesQueryVariables = {
export type DriverPropertiesQuery = { driver: Array<(
Pick<DriverInfo, 'driverParameters'>
& { driverProperties?: Maybe<Array<Maybe<Pick<DriverPropertyInfo, 'id' | 'displayName' | 'description' | 'category' | 'dataType' | 'defaultValue' | 'validValues'>>>> }
)>; };
)> };
export type GetDriverByIdQueryVariables = {
driverId: Scalars['ID'];
@@ -759,7 +758,7 @@ export type NavNodeChildrenQueryVariables = {
export type NavNodeChildrenQuery = { navNodeChildren: Array<(
Pick<NavigatorNodeInfo, 'id' | 'name' | 'hasChildren' | 'nodeType' | 'icon' | 'folder' | 'inline' | 'navigable' | 'features'>
& { object?: Maybe<Pick<DatabaseObjectInfo, 'features'>> }
)>; };
)> };
export type NavNodeInfoQueryVariables = {
nodePath: Scalars['ID'];
@@ -769,7 +768,7 @@ export type NavNodeInfoQueryVariables = {
export type NavNodeInfoQuery = { navNodeInfo: (
Pick<NavigatorNodeInfo, 'id' | 'name' | 'hasChildren' | 'nodeType' | 'icon' | 'folder' | 'inline' | 'navigable' | 'features'>
& { object?: Maybe<Pick<DatabaseObjectInfo, 'features'>> }
); };
) };
export type NavRefreshNodeQueryVariables = {
nodePath: Scalars['ID'];
@@ -789,8 +788,8 @@ export type QueryChildrenDatabaseObjectInfoQuery = { childrenDatabaseObjectInfo:
& { object?: Maybe<(
Pick<DatabaseObjectInfo, 'features'>
& { properties?: Maybe<Array<Maybe<Pick<ObjectPropertyInfo, 'id' | 'category' | 'dataType' | 'description' | 'displayName' | 'features' | 'value'>>>> }
)>; }
)>; };
)> }
)> };
export type QueryDatabaseObjectInfoQueryVariables = {
nodeId: Scalars['ID'];
@@ -803,8 +802,8 @@ export type QueryDatabaseObjectInfoQuery = { objectInfo: (
& { object?: Maybe<(
Pick<DatabaseObjectInfo, 'features'>
& { properties?: Maybe<Array<Maybe<Pick<ObjectPropertyInfo, 'id' | 'category' | 'dataType' | 'description' | 'displayName' | 'features' | 'value'>>>> }
)>; }
); };
)> }
) };
export type ReadSessionLogQueryVariables = {
maxEntries: Scalars['Int'];
@@ -847,7 +846,7 @@ export type GetRolesListQueryVariables = {
};
export type GetRolesListQuery = { roles: Array<Maybe<Pick<AdminRoleInfo, 'roleId' | 'roleName' | 'rolePermissions'>>> };
export type GetRolesListQuery = { roles: Array<Maybe<Pick<AdminRoleInfo, 'roleId' | 'roleName'>>> };
export type GetUsersListQueryVariables = {
userId?: Maybe<Scalars['ID']>;
@@ -892,7 +891,7 @@ export type GetAuthProvidersQueryVariables = {};
export type GetAuthProvidersQuery = { providers: Array<(
Pick<AuthProviderInfo, 'id' | 'label' | 'icon' | 'description' | 'defaultProvider'>
& { credentialParameters: Array<Pick<AuthCredentialInfo, 'id' | 'displayName' | 'description' | 'admin' | 'user' | 'possibleValues' | 'encryption'>> }
)>; };
)> };
export type GetSessionUserQueryVariables = {};
@@ -907,7 +906,7 @@ export type AsyncExportTaskStatusMutationVariables = {
export type AsyncExportTaskStatusMutation = { taskInfo: (
Pick<AsyncTaskInfo, 'id' | 'name' | 'running' | 'status' | 'taskResult'>
& { error?: Maybe<Pick<ServerError, 'message' | 'errorCode' | 'stackTrace'>> }
); };
) };
export type ExportDataFromContainerQueryVariables = {
connectionId: Scalars['ID'];
@@ -919,7 +918,7 @@ export type ExportDataFromContainerQueryVariables = {
export type ExportDataFromContainerQuery = { taskInfo: (
Pick<AsyncTaskInfo, 'id' | 'running' | 'taskResult'>
& { error?: Maybe<Pick<ServerError, 'message' | 'errorCode' | 'stackTrace'>> }
); };
) };
export type ExportDataFromResultsQueryVariables = {
connectionId: Scalars['ID'];
@@ -932,7 +931,7 @@ export type ExportDataFromResultsQueryVariables = {
export type ExportDataFromResultsQuery = { taskInfo: (
Pick<AsyncTaskInfo, 'id' | 'running' | 'taskResult'>
& { error?: Maybe<Pick<ServerError, 'message' | 'errorCode' | 'stackTrace'>> }
); };
) };
export type GetDataTransferProcessorsQueryVariables = {};
@@ -940,7 +939,7 @@ export type GetDataTransferProcessorsQueryVariables = {};
export type GetDataTransferProcessorsQuery = { processors: Array<(
Pick<DataTransferProcessorInfo, 'id' | 'name' | 'description' | 'fileExtension' | 'appFileExtension' | 'appName' | 'order' | 'icon' | 'isBinary' | 'isHTML'>
& { properties?: Maybe<Array<Maybe<Pick<ObjectPropertyInfo, 'id' | 'displayName' | 'description' | 'category' | 'dataType' | 'defaultValue' | 'validValues' | 'features'>>>> }
)>; };
)> };
export type RemoveDataTransferFileQueryVariables = {
dataFileId: Scalars['String'];
@@ -966,10 +965,10 @@ export type AsyncSqlExecuteQueryMutation = { taskInfo: (
& { resultSet?: Maybe<(
Pick<SqlResultSet, 'id' | 'rows'>
& { columns?: Maybe<Array<Maybe<Pick<SqlResultColumn, 'dataKind' | 'entityName' | 'fullTypeName' | 'icon' | 'label' | 'maxLength' | 'name' | 'position' | 'precision' | 'scale' | 'typeName'>>>> }
)>; }
)>; }
)>; error?: Maybe<Pick<ServerError, 'message' | 'errorCode' | 'stackTrace'>>; }
); };
)> }
)> }
)>, error?: Maybe<Pick<ServerError, 'message' | 'errorCode' | 'stackTrace'>> }
) };
export type AsyncTaskCancelMutationVariables = {
taskId: Scalars['String'];
@@ -992,10 +991,10 @@ export type AsyncTaskStatusMutation = { taskInfo: (
& { resultSet?: Maybe<(
Pick<SqlResultSet, 'id' | 'rows'>
& { columns?: Maybe<Array<Maybe<Pick<SqlResultColumn, 'dataKind' | 'entityName' | 'fullTypeName' | 'icon' | 'label' | 'maxLength' | 'name' | 'position' | 'precision' | 'scale' | 'typeName'>>>> }
)>; }
)>; }
)>; error?: Maybe<Pick<ServerError, 'message' | 'errorCode' | 'stackTrace'>>; }
); };
)> }
)> }
)>, error?: Maybe<Pick<ServerError, 'message' | 'errorCode' | 'stackTrace'>> }
) };
export type ExecuteSqlQueryMutationVariables = {
connectionId: Scalars['ID'];
@@ -1012,9 +1011,9 @@ export type ExecuteSqlQueryMutation = { result?: Maybe<(
& { resultSet?: Maybe<(
Pick<SqlResultSet, 'id' | 'rows'>
& { columns?: Maybe<Array<Maybe<Pick<SqlResultColumn, 'dataKind' | 'entityName' | 'fullTypeName' | 'icon' | 'label' | 'maxLength' | 'name' | 'position' | 'precision' | 'scale' | 'typeName'>>>> }
)>; }
)>; }
)>; };
)> }
)> }
)> };
export type MetadataGetNodeDdlQueryVariables = {
nodeId: Scalars['ID'];
@@ -1056,9 +1055,9 @@ export type ReadDataFromContainerMutation = { readDataFromContainer?: Maybe<(
& { resultSet?: Maybe<(
Pick<SqlResultSet, 'id' | 'rows'>
& { columns?: Maybe<Array<Maybe<Pick<SqlResultColumn, 'dataKind' | 'entityName' | 'fullTypeName' | 'icon' | 'label' | 'maxLength' | 'name' | 'position' | 'precision' | 'scale' | 'typeName'>>>> }
)>; }
)>; }
)>; };
)> }
)> }
)> };
export type SqlContextCreateMutationVariables = {
connectionId: Scalars['ID'];
@@ -1110,8 +1109,8 @@ export type UpdateResultsDataMutation = { result?: Maybe<(
& { results: Array<(
Pick<SqlQueryResults, 'updateRowCount'>
& { resultSet?: Maybe<Pick<SqlResultSet, 'id' | 'rows'>> }
)>; }
)>; };
)> }
)> };
export type OpenSessionMutationVariables = {};
@@ -1119,7 +1118,7 @@ export type OpenSessionMutationVariables = {};
export type OpenSessionMutation = { session: (
Pick<SessionInfo, 'createTime' | 'lastAccessTime' | 'cacheExpired' | 'locale'>
& { connections: Array<Pick<ConnectionInfo, 'id' | 'name' | 'driverId' | 'connected'>> }
); };
) };
export type ServerConfigQueryVariables = {};
@@ -1127,7 +1126,7 @@ export type ServerConfigQueryVariables = {};
export type ServerConfigQuery = { serverConfig: (
Pick<ServerConfig, 'name' | 'version' | 'productConfiguration' | 'supportsPredefinedConnections' | 'supportsProvidedConnections' | 'supportsCustomConnections' | 'supportsConnectionBrowser' | 'supportsWorkspaces' | 'anonymousAccessEnabled' | 'authenticationEnabled'>
& { supportedLanguages: Array<Pick<ServerLanguage, 'isoCode' | 'displayName' | 'nativeName'>> }
); };
) };
export type SessionPermissionsQueryVariables = {};
@@ -1140,7 +1139,7 @@ export type SessionStateQueryVariables = {};
export type SessionStateQuery = { sessionState: (
Pick<SessionInfo, 'createTime' | 'lastAccessTime' | 'locale' | 'cacheExpired'>
& { connections: Array<Pick<ConnectionInfo, 'id' | 'name' | 'driverId' | 'connected'>> }
); };
) };
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<TouchSessionMutation> {
return withWrapper(() => client.request<TouchSessionMutation>(TouchSessionDocument, variables));
},
}
};
}
export type Sdk = ReturnType<typeof getSdk>;
export type Sdk = ReturnType<typeof getSdk>;