Merge remote-tracking branch 'origin/CB-2406-projects-permission-managment' into CB-2406-projects-permission-managment

This commit is contained in:
Aleksandr Skoblikov
2022-09-13 16:02:05 +03:00
21 changed files with 236 additions and 25 deletions
@@ -8,9 +8,10 @@
import { injectable } from '@cloudbeaver/core-di';
import { SessionDataResource } from '@cloudbeaver/core-root';
import { GraphQLService, AdminPermissionInfo, CachedMapResource, CachedMapAllKey } from '@cloudbeaver/core-sdk';
import { GraphQLService, CachedMapResource, CachedMapAllKey, AdminPermissionInfoFragment, AdminObjectGrantInfoFragment } from '@cloudbeaver/core-sdk';
export type PermissionInfo = AdminPermissionInfo;
export type PermissionInfo = AdminPermissionInfoFragment;
export type AdminObjectGrantInfo = AdminObjectGrantInfoFragment;
@injectable()
export class PermissionsResource extends CachedMapResource<string, PermissionInfo> {
+9 -1
View File
@@ -109,6 +109,7 @@ export const Button = observer<ButtonProps>(function Button({
mod,
styles,
tag = 'button',
type = 'button',
disabled = false,
loading,
loader,
@@ -143,7 +144,14 @@ export const Button = observer<ButtonProps>(function Button({
const Button = tag;
return styled(useStyles(styles, buttonStyles, ...(mod || []).map(mod => buttonMod[mod])))(
<Button {...rest} disabled={disabled} {...use({ loading })} className={className} onClick={state.click}>
<Button
{...rest}
type={type}
disabled={disabled}
{...use({ loading })}
className={className}
onClick={state.click}
>
<ripple />
{icon && <button-icon><Icon name={icon} viewBox={viewBox} /></button-icon>}
<button-label as='span'>{children}</button-label>
@@ -55,7 +55,7 @@ export const StatusMessage = observer<Props>(function StatusMessage({
}) {
const translate = useTranslate();
const errorDetails = useErrorDetails(exception);
message = (message ? translate(message) : message) ?? errorDetails.details?.name;
message = (message ? translate(message) : message) ?? errorDetails.details?.message;
let icon = '/icons/info_icon.svg';
if (status === ENotificationType.Error || exception !== null) {
@@ -6,8 +6,8 @@
* you may not use this file except in compliance with the License.
*/
export function getSelectedItems(items: Map<any, boolean>): any[] {
const result: any[] = [];
export function getSelectedItems<T>(items: Map<T, boolean>): T[] {
const result: T[] = [];
for (const [key, value] of items) {
if (value) {
@@ -0,0 +1,30 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2022 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 type { PermissionInfo } from '@cloudbeaver/core-administration';
import { injectable } from '@cloudbeaver/core-di';
import { ServerConfigResource } from '@cloudbeaver/core-root';
import { GraphQLService, CachedDataResource } from '@cloudbeaver/core-sdk';
@injectable()
export class SharedProjectPermissionsResource extends CachedDataResource<PermissionInfo[], void | any> {
constructor(
private readonly graphQLService: GraphQLService,
serverConfigResource: ServerConfigResource
) {
super([]);
this.sync(serverConfigResource, () => {}, () => {});
}
protected async loader(): Promise<PermissionInfo[]> {
const { permissions } = await this.graphQLService.sdk.getProjectPermissionsList();
return permissions as any as PermissionInfo[];
}
}
@@ -8,14 +8,16 @@
import { runInAction } from 'mobx';
import { EAdminPermission } from '@cloudbeaver/core-administration';
import { AdminObjectGrantInfo, EAdminPermission } from '@cloudbeaver/core-administration';
import { injectable } from '@cloudbeaver/core-di';
import { SessionPermissionsResource } from '@cloudbeaver/core-root';
import { GraphQLService, CachedMapResource, CachedMapAllKey, ResourceKey, ResourceKeyUtils, resourceKeyList, RmProject, ResourceKeyList } from '@cloudbeaver/core-sdk';
import { GraphQLService, CachedMapResource, CachedMapAllKey, ResourceKey, ResourceKeyUtils, resourceKeyList, RmProject, ResourceKeyList, AdminSubjectType, RmProjectPermissions } from '@cloudbeaver/core-sdk';
import { isArraysEqual } from '@cloudbeaver/core-utils';
const newSymbol = Symbol('new-project');
export type SharedProject = RmProject;
export type ProjectPermission = RmProjectPermissions;
type SharedProjectNew = SharedProject & { [newSymbol]: boolean };
interface IProjectConfig {
@@ -44,6 +46,21 @@ export class SharedProjectsResource extends CachedMapResource<string, SharedProj
return newSymbol in this.get(id)!;
}
async setAccessSubjects(projectId: string, permissions: ProjectPermission[]): Promise<void> {
await this.graphQLService.sdk.setProjectPermissions({
projectId,
permissions,
});
}
async loadAccessSubjects(projectId: string): Promise<AdminObjectGrantInfo[]> {
const { grantedPermissions } = await this.graphQLService.sdk.getProjectGrantedPermissions({
projectId,
});
return grantedPermissions;
}
async create(config: IProjectConfig): Promise<SharedProject> {
const { project } = await this.graphQLService.sdk.createProject({
projectId: config.id,
@@ -108,4 +125,13 @@ export class SharedProjectsResource extends CachedMapResource<string, SharedProj
const data = this.data.get(key);
this.data.set(key, Object.assign(data ?? {}, value));
}
}
export function isEqualSharedProjectGrantInfo(a: AdminObjectGrantInfo, b: AdminObjectGrantInfo): boolean {
return (
a.subjectId === b.subjectId
&& a.subjectType === b.subjectType
&& a.objectPermissions.objectId === b.objectPermissions.objectId
&& isArraysEqual(a.objectPermissions.permissions, b.objectPermissions.permissions)
);
}
@@ -1,2 +1,3 @@
export * from './SharedProjectsResource';
export * from './SharedProjectPermissionsResource';
export { resourceManagerManifest } from './manifest';
@@ -8,6 +8,7 @@
import type { PluginManifest } from '@cloudbeaver/core-di';
import { SharedProjectPermissionsResource } from './SharedProjectPermissionsResource';
import { SharedProjectsResource } from './SharedProjectsResource';
@@ -18,5 +19,6 @@ export const resourceManagerManifest: PluginManifest = {
providers: [
SharedProjectsResource,
SharedProjectPermissionsResource,
],
};
@@ -0,0 +1,8 @@
fragment AdminObjectGrantInfo on AdminObjectGrantInfo {
subjectId
subjectType
objectPermissions {
objectId
permissions
}
}
@@ -3,7 +3,9 @@ query getProjectList {
id
name
description
canCreateConnections
canViewConnections
canEditDataSources
canViewDataSources
canEditResources
canViewResources
}
}
@@ -0,0 +1,9 @@
query getProjectGrantedPermissions(
$projectId: String!
) {
grantedPermissions: rmListProjectGrantedPermissions(
projectId: $projectId
) {
...AdminObjectGrantInfo
}
}
@@ -0,0 +1,5 @@
query getProjectPermissionsList {
permissions: rmListProjectPermissions {
...AdminPermissionInfo
}
}
@@ -0,0 +1,9 @@
mutation setProjectPermissions(
$projectId: String!
$permissions: [RMProjectPermissions!]!
) {
rmSetProjectPermissions(
projectId: $projectId
permissions: $permissions
)
}
+105 -5
View File
@@ -47,6 +47,17 @@ export interface AdminConnectionSearchInfo {
possibleDrivers: Array<Scalars['ID']>;
}
export interface AdminObjectGrantInfo {
objectPermissions: AdminObjectPermissions;
subjectId: Scalars['ID'];
subjectType: AdminSubjectType;
}
export interface AdminObjectPermissions {
objectId: Scalars['ID'];
permissions: Array<Scalars['String']>;
}
export interface AdminPermissionInfo {
category?: Maybe<Scalars['String']>;
description?: Maybe<Scalars['String']>;
@@ -371,6 +382,7 @@ export interface Mutation {
rmDeleteProject: Scalars['Boolean'];
rmDeleteResource?: Maybe<Scalars['Boolean']>;
rmMoveResource: Scalars['String'];
rmSetProjectPermissions: Scalars['Boolean'];
rmWriteResourceStringContent: Scalars['String'];
setConnectionNavigatorSettings: ConnectionInfo;
setUserConfigurationParameter: Scalars['Boolean'];
@@ -563,6 +575,12 @@ export interface MutationRmMoveResourceArgs {
}
export interface MutationRmSetProjectPermissionsArgs {
permissions: Array<RmProjectPermissions>;
projectId: Scalars['String'];
}
export interface MutationRmWriteResourceStringContentArgs {
data: Scalars['String'];
forceOverwrite: Scalars['Boolean'];
@@ -807,8 +825,10 @@ export interface ProductInfo {
}
export interface ProjectInfo {
canCreateConnections: Scalars['Boolean'];
canViewConnections: Scalars['Boolean'];
canEditDataSources: Scalars['Boolean'];
canEditResources: Scalars['Boolean'];
canViewDataSources: Scalars['Boolean'];
canViewResources: Scalars['Boolean'];
description?: Maybe<Scalars['String']>;
id: Scalars['String'];
name: Scalars['String'];
@@ -861,9 +881,12 @@ export interface Query {
networkHandlers: Array<NetworkHandlerDescriptor>;
readSessionLog: Array<LogEntry>;
revokeUserRole?: Maybe<Scalars['Boolean']>;
rmListProjectGrantedPermissions: Array<AdminObjectGrantInfo>;
rmListProjectPermissions?: Maybe<AdminPermissionInfo>;
rmListProjects: Array<RmProject>;
rmListResources: Array<RmResource>;
rmListSharedProjects: Array<RmProject>;
rmListSubjectProjectsPermissionGrants: Array<AdminObjectGrantInfo>;
rmProject: RmProject;
rmReadResourceAsString: Scalars['String'];
saveAuthProviderConfiguration: AdminAuthProviderConfiguration;
@@ -1109,6 +1132,11 @@ export interface QueryRevokeUserRoleArgs {
}
export interface QueryRmListProjectGrantedPermissionsArgs {
projectId: Scalars['String'];
}
export interface QueryRmListResourcesArgs {
folder?: InputMaybe<Scalars['String']>;
nameMask?: InputMaybe<Scalars['String']>;
@@ -1118,6 +1146,11 @@ export interface QueryRmListResourcesArgs {
}
export interface QueryRmListSubjectProjectsPermissionGrantsArgs {
subjectId: Scalars['String'];
}
export interface QueryRmProjectArgs {
projectId: Scalars['String'];
}
@@ -1291,6 +1324,11 @@ export interface RmProject {
shared: Scalars['Boolean'];
}
export interface RmProjectPermissions {
permissions: Array<Scalars['String']>;
subjectId: Scalars['String'];
}
export interface RmResource {
folder: Scalars['Boolean'];
length: Scalars['Int'];
@@ -2163,6 +2201,8 @@ export type NavGetStructContainersQueryVariables = Exact<{
export type NavGetStructContainersQuery = { navGetStructContainers: { supportsCatalogChange: boolean, supportsSchemaChange: boolean, catalogList: Array<{ catalog: { id: string, name?: string, hasChildren?: boolean, nodeType?: string, icon?: string, folder?: boolean, inline?: boolean, navigable?: boolean, features?: Array<string>, object?: { features?: Array<string> }, nodeDetails?: Array<{ id?: string, category?: string, dataType?: string, description?: string, displayName?: string, length: ObjectPropertyLength, features: Array<string>, value?: any, order: number }> }, schemaList: Array<{ id: string, name?: string, hasChildren?: boolean, nodeType?: string, icon?: string, folder?: boolean, inline?: boolean, navigable?: boolean, features?: Array<string>, object?: { features?: Array<string> }, nodeDetails?: Array<{ id?: string, category?: string, dataType?: string, description?: string, displayName?: string, length: ObjectPropertyLength, features: Array<string>, value?: any, order: number }> }> }>, schemaList: Array<{ id: string, name?: string, hasChildren?: boolean, nodeType?: string, icon?: string, folder?: boolean, inline?: boolean, navigable?: boolean, features?: Array<string>, object?: { features?: Array<string> }, nodeDetails?: Array<{ id?: string, category?: string, dataType?: string, description?: string, displayName?: string, length: ObjectPropertyLength, features: Array<string>, value?: any, order: number }> }> } };
export type AdminObjectGrantInfoFragment = { subjectId: string, subjectType: AdminSubjectType, objectPermissions: { objectId: string, permissions: Array<string> } };
export type AdminPermissionInfoFragment = { id: string, label?: string, description?: string, category?: string };
export type AdminRoleInfoFragment = { roleId: string, roleName?: string, description?: string, rolePermissions: Array<string> };
@@ -2401,7 +2441,7 @@ export type NavRenameNodeMutation = { navRenameNode?: string };
export type GetProjectListQueryVariables = Exact<{ [key: string]: never; }>;
export type GetProjectListQuery = { projects: Array<{ id: string, name: string, description?: string, canCreateConnections: boolean, canViewConnections: boolean }> };
export type GetProjectListQuery = { projects: Array<{ id: string, name: string, description?: string, canEditDataSources: boolean, canViewDataSources: boolean, canEditResources: boolean, canViewResources: boolean }> };
export type CreateProjectMutationVariables = Exact<{
projectId?: InputMaybe<Scalars['ID']>;
@@ -2444,6 +2484,18 @@ export type GetProjectQueryVariables = Exact<{
export type GetProjectQuery = { project: { id: string, name: string, shared: boolean, description?: string, projectPermissions: Array<string> } };
export type GetProjectGrantedPermissionsQueryVariables = Exact<{
projectId: Scalars['String'];
}>;
export type GetProjectGrantedPermissionsQuery = { grantedPermissions: Array<{ subjectId: string, subjectType: AdminSubjectType, objectPermissions: { objectId: string, permissions: Array<string> } }> };
export type GetProjectPermissionsListQueryVariables = Exact<{ [key: string]: never; }>;
export type GetProjectPermissionsListQuery = { permissions?: { id: string, label?: string, description?: string, category?: string } };
export type GetResourceListQueryVariables = Exact<{
projectId: Scalars['String'];
folder?: InputMaybe<Scalars['String']>;
@@ -2482,6 +2534,14 @@ export type ReadResourceQueryVariables = Exact<{
export type ReadResourceQuery = { value: string };
export type SetProjectPermissionsMutationVariables = Exact<{
projectId: Scalars['String'];
permissions: Array<RmProjectPermissions> | RmProjectPermissions;
}>;
export type SetProjectPermissionsMutation = { rmSetProjectPermissions: boolean };
export type WriteResourceContentMutationVariables = Exact<{
projectId: Scalars['String'];
resourcePath: Scalars['String'];
@@ -2614,6 +2674,16 @@ export type SqlGenerateEntityQueryQueryVariables = Exact<{
export type SqlGenerateEntityQueryQuery = { sqlGenerateEntityQuery: string };
export const AdminObjectGrantInfoFragmentDoc = `
fragment AdminObjectGrantInfo on AdminObjectGrantInfo {
subjectId
subjectType
objectPermissions {
objectId
permissions
}
}
`;
export const AdminPermissionInfoFragmentDoc = `
fragment AdminPermissionInfo on AdminPermissionInfo {
id
@@ -3890,8 +3960,10 @@ export const GetProjectListDocument = `
id
name
description
canCreateConnections
canViewConnections
canEditDataSources
canViewDataSources
canEditResources
canViewResources
}
}
`;
@@ -3936,6 +4008,20 @@ export const GetProjectDocument = `
}
}
${SharedProjectFragmentDoc}`;
export const GetProjectGrantedPermissionsDocument = `
query getProjectGrantedPermissions($projectId: String!) {
grantedPermissions: rmListProjectGrantedPermissions(projectId: $projectId) {
...AdminObjectGrantInfo
}
}
${AdminObjectGrantInfoFragmentDoc}`;
export const GetProjectPermissionsListDocument = `
query getProjectPermissionsList {
permissions: rmListProjectPermissions {
...AdminPermissionInfo
}
}
${AdminPermissionInfoFragmentDoc}`;
export const GetResourceListDocument = `
query getResourceList($projectId: String!, $folder: String, $nameMask: String, $readProperties: Boolean, $readHistory: Boolean) {
resources: rmListResources(
@@ -3986,6 +4072,11 @@ export const ReadResourceDocument = `
)
}
`;
export const SetProjectPermissionsDocument = `
mutation setProjectPermissions($projectId: String!, $permissions: [RMProjectPermissions!]!) {
rmSetProjectPermissions(projectId: $projectId, permissions: $permissions)
}
`;
export const WriteResourceContentDocument = `
mutation writeResourceContent($projectId: String!, $resourcePath: String!, $data: String!, $forceOverwrite: Boolean!) {
rmWriteResourceStringContent(
@@ -4494,6 +4585,12 @@ export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper =
getProject(variables: GetProjectQueryVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise<GetProjectQuery> {
return withWrapper((wrappedRequestHeaders) => client.request<GetProjectQuery>(GetProjectDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'getProject', 'query');
},
getProjectGrantedPermissions(variables: GetProjectGrantedPermissionsQueryVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise<GetProjectGrantedPermissionsQuery> {
return withWrapper((wrappedRequestHeaders) => client.request<GetProjectGrantedPermissionsQuery>(GetProjectGrantedPermissionsDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'getProjectGrantedPermissions', 'query');
},
getProjectPermissionsList(variables?: GetProjectPermissionsListQueryVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise<GetProjectPermissionsListQuery> {
return withWrapper((wrappedRequestHeaders) => client.request<GetProjectPermissionsListQuery>(GetProjectPermissionsListDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'getProjectPermissionsList', 'query');
},
getResourceList(variables: GetResourceListQueryVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise<GetResourceListQuery> {
return withWrapper((wrappedRequestHeaders) => client.request<GetResourceListQuery>(GetResourceListDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'getResourceList', 'query');
},
@@ -4509,6 +4606,9 @@ export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper =
readResource(variables: ReadResourceQueryVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise<ReadResourceQuery> {
return withWrapper((wrappedRequestHeaders) => client.request<ReadResourceQuery>(ReadResourceDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'readResource', 'query');
},
setProjectPermissions(variables: SetProjectPermissionsMutationVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise<SetProjectPermissionsMutation> {
return withWrapper((wrappedRequestHeaders) => client.request<SetProjectPermissionsMutation>(SetProjectPermissionsDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'setProjectPermissions', 'mutation');
},
writeResourceContent(variables: WriteResourceContentMutationVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise<WriteResourceContentMutation> {
return withWrapper((wrappedRequestHeaders) => client.request<WriteResourceContentMutation>(WriteResourceContentDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'writeResourceContent', 'mutation');
},
@@ -28,4 +28,8 @@ describe('Is array equals', () => {
test('should return "false" when arrays length is different and the second array is larger', () => {
expect(isArraysEqual([1, true], [1, true, '1'])).toBe(false);
});
test('should use isEqual argument if passed', () => {
expect(isArraysEqual([{ a:1 }], [{ a: 1 }], (a, b) => a.a === b.a)).toBe(true);
});
});
@@ -6,10 +6,16 @@
* you may not use this file except in compliance with the License.
*/
export function isArraysEqual<T>(first: T[], second: T[]): boolean {
const isObjectEqual = <T>(a: T, b: T) => a === b;
export function isArraysEqual<T>(
first: T[],
second: T[],
isEqual: (a: T, b: T) => boolean = isObjectEqual
): boolean {
if (first.length !== second.length) {
return false;
}
return !first.some(value => !second.includes(value));
return !first.some(b => !second.some(a => isEqual(a, b)));
}
@@ -36,7 +36,7 @@ export class CustomConnectionPluginBootstrap extends Bootstrap {
title: 'app_shared_connectionMenu_custom',
isHidden: () => (
!this.serverConfigResource.data?.supportsCustomConnections
|| !this.projectsService.activeProject?.canCreateConnections
|| !this.projectsService.activeProject?.canEditDataSources
),
onClick: () => this.openConnectionsDialog(),
});
@@ -36,7 +36,7 @@ export class SearchConnectionPluginBootstrap extends Bootstrap {
order: 3,
title: 'connections_connection_create_search_database',
isHidden: () => (
!this.projectsService.activeProject?.canCreateConnections
!this.projectsService.activeProject?.canEditDataSources
|| !this.permissionsService.has(EAdminPermission.admin)
),
onClick: () => {
@@ -136,7 +136,7 @@ export class ConnectionOptionsTabService extends Bootstrap {
if (state.projectId !== null && state.mode === 'create') {
const project = this.projectsResource.get(state.projectId);
if (!project?.canCreateConnections) {
if (!project?.canEditDataSources) {
validation.error('plugin_connections_connection_form_project_invalid');
}
}
@@ -36,12 +36,12 @@ export const ProjectSelect = observer(function ProjectSelect(props: Props) {
const value = props.value ?? projectsService.activeProject?.id;
const projects = projectsLoader.data as ProjectInfo[];
const possibleOptions = projects.filter(project => project.canCreateConnections);
const possibleOptions = projects.filter(project => project.canEditDataSources);
function handleProjectSelect(projectId: string) {
const project = projectsLoader.resource.get(projectId);
if (project?.canCreateConnections) {
if (project?.canEditDataSources) {
props.onChange(projectId);
}
}
@@ -54,7 +54,7 @@ export const ProjectSelect = observer(function ProjectSelect(props: Props) {
keySelector={project => project.id}
valueSelector={project => project.name}
titleSelector={project => project.description}
isDisabled={project => !project.canCreateConnections}
isDisabled={project => !project.canEditDataSources}
readOnly={props.readOnly || possibleOptions.length <= 1}
searchable={projects.length > 10}
disabled={props.disabled}
@@ -185,7 +185,7 @@ export class ConnectionFoldersBootstrap extends Bootstrap {
}
if (type === ENodeMoveType.CanDrop) {
if (targetProject?.canCreateConnections) {
if (targetProject?.canEditDataSources) {
move.setCanMove(true);
}
} else {
@@ -337,7 +337,7 @@ export class ConnectionFoldersBootstrap extends Bootstrap {
const project = this.projectsNavNodeService.getByNodeId(projectNode.id);
if (!project?.canCreateConnections) {
if (!project?.canEditDataSources) {
return;
}