mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-14 17:02:49 +03:00
Fix cloneable permissions between query types
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/8284 GitOrigin-RevId: 17c0590bcfa032d9a396851dfc0c97b2ee94feb1
This commit is contained in:
parent
18368d6c6d
commit
662754ef9e
@ -40,7 +40,7 @@ export const gdc: Database = {
|
||||
return (table as GDCTable).join('_');
|
||||
},
|
||||
getSupportedQueryTypes: async () => {
|
||||
return ['select'];
|
||||
return ['select', 'delete', 'update', 'insert'];
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -97,12 +97,9 @@ const Component = (props: ComponentProps) => {
|
||||
// E.g. when switching tables
|
||||
useEffect(() => {
|
||||
const newValues = getValues();
|
||||
reset({ ...newValues, ...defaultValues });
|
||||
reset({ ...newValues, ...defaultValues, clonePermissions: [] });
|
||||
}, [roleName, defaultValues]);
|
||||
|
||||
// allRowChecks relates to other queries and is for duplicating from others
|
||||
const allRowChecks = defaultValues?.allRowChecks;
|
||||
|
||||
const key = `${JSON.stringify(table)}-${queryType}-${roleName}`;
|
||||
|
||||
const filterType = getValues('filterType');
|
||||
@ -140,7 +137,6 @@ const Component = (props: ComponentProps) => {
|
||||
queryType={queryType}
|
||||
subQueryType={queryType === 'update' ? 'pre' : undefined}
|
||||
permissionsKey={filterKeys[0]}
|
||||
allRowChecks={allRowChecks || []}
|
||||
dataSourceName={dataSourceName}
|
||||
supportedOperators={data?.defaultValues?.supportedOperators ?? []}
|
||||
defaultValues={defaultValues}
|
||||
@ -157,7 +153,6 @@ const Component = (props: ComponentProps) => {
|
||||
roleName={roleName}
|
||||
queryType={queryType}
|
||||
subQueryType={queryType === 'update' ? 'post' : undefined}
|
||||
allRowChecks={allRowChecks || []}
|
||||
permissionsKey={filterKeys[1]}
|
||||
dataSourceName={dataSourceName}
|
||||
supportedOperators={
|
||||
|
@ -1,8 +1,8 @@
|
||||
import produce from 'immer';
|
||||
import produce, { Draft, original } from 'immer';
|
||||
|
||||
import { allowedMetadataTypes } from '../../../MetadataAPI';
|
||||
|
||||
import { AccessType } from '../../types';
|
||||
import { AccessType, QueryType } from '../../types';
|
||||
import { PermissionsSchema, Presets } from '../../schema';
|
||||
import { areTablesEqual } from '../../../hasura-metadata-api';
|
||||
import { Table } from '../../../hasura-metadata-types';
|
||||
@ -205,6 +205,49 @@ export interface ExistingPermission {
|
||||
role: string;
|
||||
queryType: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* When cloning permissions we have to transform the payload between filter and checks.
|
||||
* The input per type is:
|
||||
* select uses filter
|
||||
* delete uses filter
|
||||
* insert uses check
|
||||
* update uses filter(for pre-check) and check (for post-check)
|
||||
*
|
||||
* When cloning between the permissions type we need to swap these out based on with way we are cloning
|
||||
*/
|
||||
const getPermissionsWithMappedRowPermissions = (
|
||||
permissionsObject: any,
|
||||
mainQueryType: QueryType,
|
||||
clonedQueryType: string
|
||||
): { filter?: Record<string, any>; check?: Record<string, any> } => {
|
||||
const clone: { filter?: Record<string, any>; check?: Record<string, any> } = {
|
||||
filter: permissionsObject.filter,
|
||||
check: permissionsObject.check,
|
||||
};
|
||||
|
||||
if (
|
||||
(mainQueryType === 'select' && clonedQueryType === 'insert') ||
|
||||
(mainQueryType === 'select' && clonedQueryType === 'update') ||
|
||||
(mainQueryType === 'delete' && clonedQueryType === 'insert') ||
|
||||
(mainQueryType === 'delete' && clonedQueryType === 'update')
|
||||
) {
|
||||
clone.check = permissionsObject.filter;
|
||||
}
|
||||
|
||||
if (
|
||||
(mainQueryType === 'update' && clonedQueryType === 'select') ||
|
||||
(mainQueryType === 'update' && clonedQueryType === 'delete') ||
|
||||
(mainQueryType === 'insert' && clonedQueryType === 'select') ||
|
||||
(mainQueryType === 'insert' && clonedQueryType === 'delete') ||
|
||||
(mainQueryType === 'insert' && clonedQueryType === 'update')
|
||||
) {
|
||||
clone.filter = permissionsObject.check;
|
||||
}
|
||||
|
||||
return { filter: clone.filter, check: clone.check };
|
||||
};
|
||||
|
||||
/**
|
||||
* creates the insert arguments to update permissions
|
||||
* adds cloned permissions
|
||||
@ -221,7 +264,6 @@ export const createInsertArgs = ({
|
||||
tables,
|
||||
}: CreateInsertArgs) => {
|
||||
const permission = createPermission(formData);
|
||||
|
||||
// create args object with args from form
|
||||
const initialArgs = [
|
||||
{
|
||||
@ -274,9 +316,22 @@ export const createInsertArgs = ({
|
||||
d.set = {};
|
||||
}
|
||||
|
||||
return d;
|
||||
const newValues = {
|
||||
...getPermissionsWithMappedRowPermissions(
|
||||
original(d),
|
||||
queryType,
|
||||
clonedPermission.queryType
|
||||
),
|
||||
};
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
d.filter = newValues.filter;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
d.check = newValues.check;
|
||||
}
|
||||
);
|
||||
|
||||
// add each closed permission to args
|
||||
draft.push({
|
||||
type: `${driver}_create_${clonedPermission.queryType}_permission` as allowedMetadataTypes,
|
||||
|
@ -7,12 +7,11 @@ import { useIsDisabled } from '../hooks/useIsDisabled';
|
||||
import { QueryType } from '../../types';
|
||||
import { Permission } from '../../schema';
|
||||
import { Feature } from '../../../DataSource';
|
||||
import { Table } from '../../../hasura-metadata-types';
|
||||
import { getTableDisplayName } from '../../../DatabaseRelationships';
|
||||
|
||||
import { QualifiedTable } from '../../../../metadata/types';
|
||||
interface ClonePermissionsRowProps {
|
||||
id: number;
|
||||
tables: Table[];
|
||||
tables: QualifiedTable[];
|
||||
currentQueryType: QueryType;
|
||||
queryTypes: string[];
|
||||
roleNames: string[];
|
||||
@ -42,7 +41,6 @@ export const ClonePermissionsRow: React.FC<ClonePermissionsRowProps> = ({
|
||||
<div>
|
||||
<select
|
||||
className={className}
|
||||
disabled={allDisabled}
|
||||
title={allDisabled ? 'Set a row permission first' : ''}
|
||||
{...register(`${formKey}.${id}.tableName`)}
|
||||
>
|
||||
@ -64,7 +62,6 @@ export const ClonePermissionsRow: React.FC<ClonePermissionsRowProps> = ({
|
||||
<div>
|
||||
<select
|
||||
className={className}
|
||||
disabled={allDisabled}
|
||||
title={allDisabled ? 'Set a row permission first' : ''}
|
||||
{...register(`${formKey}.${id}.queryType`)}
|
||||
>
|
||||
@ -83,7 +80,6 @@ export const ClonePermissionsRow: React.FC<ClonePermissionsRowProps> = ({
|
||||
<div>
|
||||
<select
|
||||
className={className}
|
||||
disabled={allDisabled}
|
||||
title={allDisabled ? 'Set a row permission first' : ''}
|
||||
{...register(`${formKey}.${id}.roleName`)}
|
||||
>
|
||||
@ -113,7 +109,7 @@ export const ClonePermissionsRow: React.FC<ClonePermissionsRowProps> = ({
|
||||
|
||||
export interface ClonePermissionsSectionProps {
|
||||
queryType: string;
|
||||
tables: Table[];
|
||||
tables: QualifiedTable[];
|
||||
supportedQueryTypes: QueryType[] | Feature | undefined;
|
||||
roles: string[];
|
||||
defaultOpen?: boolean;
|
||||
|
@ -9,6 +9,7 @@ import { getEdForm } from '../../../../components/Services/Data/utils';
|
||||
import { useIsDisabled } from '../hooks/useIsDisabled';
|
||||
import { QueryType } from '../../types';
|
||||
import { isPermissionModalDisabled } from '../utils/getPermissionModalStatus';
|
||||
import { SelectColumn } from '../../../DataSource/types';
|
||||
|
||||
import {
|
||||
getPermissionsModalTitle,
|
||||
@ -171,7 +172,6 @@ export const ColumnPermissionsSection: React.FC<
|
||||
<strong>columns</strong>:
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<fieldset className="flex gap-4 flex-wrap">
|
||||
{columns?.map(fieldName => (
|
||||
<label key={fieldName} className="flex gap-2 items-center">
|
||||
|
@ -10,8 +10,6 @@ import {
|
||||
RowPermissionsWrapperProps,
|
||||
} from './RowPermissions';
|
||||
|
||||
import { QueryType } from '../../types';
|
||||
|
||||
export default {
|
||||
title: 'Features/Permissions/Form/Row Section',
|
||||
component: RowPermissionsSection,
|
||||
@ -28,18 +26,6 @@ export default {
|
||||
|
||||
const roleName = 'two';
|
||||
|
||||
// this will be moved into a utils folder
|
||||
const allRowChecks = [
|
||||
{
|
||||
queryType: 'insert' as QueryType,
|
||||
value: '{"id":{"_eq":1}}',
|
||||
},
|
||||
{
|
||||
queryType: 'select' as QueryType,
|
||||
value: '{"id":{"_eq":1}}',
|
||||
},
|
||||
];
|
||||
|
||||
interface Props {
|
||||
wrapper: RowPermissionsWrapperProps;
|
||||
section: RowPermissionsProps;
|
||||
@ -59,9 +45,6 @@ Insert.args = {
|
||||
},
|
||||
dataSourceName: 'chinook',
|
||||
queryType: 'delete',
|
||||
allRowChecks,
|
||||
// allSchemas,
|
||||
// allFunctions,
|
||||
},
|
||||
};
|
||||
|
||||
@ -75,7 +58,6 @@ Select.args = {
|
||||
section: {
|
||||
...Insert!.args!.section!,
|
||||
queryType: 'select',
|
||||
allRowChecks,
|
||||
},
|
||||
};
|
||||
|
||||
@ -87,9 +69,8 @@ export const Update: Story<Props> = args => (
|
||||
Update.args = {
|
||||
wrapper: { roleName, queryType: 'update', defaultOpen: true },
|
||||
section: {
|
||||
...Insert.args.section!,
|
||||
...(Insert?.args?.section || {}),
|
||||
queryType: 'update',
|
||||
allRowChecks,
|
||||
},
|
||||
};
|
||||
|
||||
@ -101,9 +82,8 @@ export const Delete: Story<Props> = args => (
|
||||
Delete.args = {
|
||||
wrapper: { roleName, queryType: 'delete', defaultOpen: true },
|
||||
section: {
|
||||
...Insert.args.section!,
|
||||
...(Insert?.args?.section || {}),
|
||||
queryType: 'delete',
|
||||
allRowChecks,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import AceEditor from 'react-ace';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { Table } from '../../../hasura-metadata-types';
|
||||
import { MetadataTable, Table } from '../../../hasura-metadata-types';
|
||||
import { useHttpClient } from '../../../Network';
|
||||
import { useQuery } from 'react-query';
|
||||
import { DataSource, exportMetadata, Operator } from '../../../DataSource';
|
||||
@ -14,6 +14,8 @@ import { getIngForm } from '../../../../components/Services/Data/utils';
|
||||
import { RowPermissionBuilder } from './RowPermissionsBuilder';
|
||||
import { QueryType } from '../../types';
|
||||
import { ReturnValue } from '../hooks';
|
||||
import { useMetadataTable } from '../../../hasura-metadata-api/metadataHooks';
|
||||
import { getNonSelectedQueryTypePermissions } from '../utils/getMapQueryTypePermissions';
|
||||
|
||||
const NoChecksLabel = () => (
|
||||
<span data-test="without-checks">Without any checks </span>
|
||||
@ -30,7 +32,6 @@ export interface RowPermissionsProps {
|
||||
table: unknown;
|
||||
queryType: QueryType;
|
||||
subQueryType?: string;
|
||||
allRowChecks: Array<{ queryType: QueryType; value: string }>;
|
||||
dataSourceName: string;
|
||||
supportedOperators: Operator[];
|
||||
defaultValues: ReturnValue['defaultValues'];
|
||||
@ -42,6 +43,10 @@ enum SelectedSection {
|
||||
NoChecks = 'no_checks',
|
||||
Custom = 'custom',
|
||||
NoneSelected = 'none',
|
||||
insert = 'insert',
|
||||
select = 'select',
|
||||
update = 'update',
|
||||
delete = 'delete',
|
||||
}
|
||||
|
||||
const getRowPermission = (queryType: QueryType, subQueryType?: string) => {
|
||||
@ -134,14 +139,21 @@ export const RowPermissionsSection: React.FC<RowPermissionsProps> = ({
|
||||
table,
|
||||
queryType,
|
||||
subQueryType,
|
||||
allRowChecks,
|
||||
dataSourceName,
|
||||
defaultValues,
|
||||
permissionsKey,
|
||||
roleName,
|
||||
}) => {
|
||||
const { data: tableName, isLoading } = useTypeName({ table, dataSourceName });
|
||||
const { register, watch, setValue } = useFormContext();
|
||||
const metadataTable = useMetadataTable(dataSourceName, table);
|
||||
|
||||
const nonSelectedQueryTypePermissions = getNonSelectedQueryTypePermissions(
|
||||
metadataTable?.data as MetadataTable,
|
||||
queryType,
|
||||
roleName
|
||||
);
|
||||
|
||||
const { watch, setValue, reset, getValues } = useFormContext();
|
||||
// determines whether the inputs should be pointed at `check` or `filter`
|
||||
const rowPermissions = getRowPermission(queryType, subQueryType);
|
||||
// determines whether the check type should be pointer at `checkType` or `filterType`
|
||||
@ -159,11 +171,11 @@ export const RowPermissionsSection: React.FC<RowPermissionsProps> = ({
|
||||
id={SelectedSection.NoChecks}
|
||||
type="radio"
|
||||
value={SelectedSection.NoChecks}
|
||||
checked={selectedSection === SelectedSection.NoChecks}
|
||||
onClick={() => {
|
||||
setValue(rowPermissionsCheckType, SelectedSection.NoChecks);
|
||||
setValue(rowPermissions, {});
|
||||
}}
|
||||
{...register(rowPermissionsCheckType)}
|
||||
/>
|
||||
<NoChecksLabel />
|
||||
</label>
|
||||
@ -189,43 +201,61 @@ export const RowPermissionsSection: React.FC<RowPermissionsProps> = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{allRowChecks?.map(({ queryType: query, value }) => (
|
||||
<div key={query}>
|
||||
<label className="flex items-center gap-2">
|
||||
<input
|
||||
id={`custom_${query}`}
|
||||
data-testid={getUpdatePermissionBuilderIdSuffix(
|
||||
`external-${roleName}-${query}-input`,
|
||||
subQueryType
|
||||
)}
|
||||
type="radio"
|
||||
value={query}
|
||||
{...register(rowPermissionsCheckType)}
|
||||
onClick={() => {
|
||||
setValue(rowPermissionsCheckType, query);
|
||||
setValue(rowPermissions, JSON.parse(value));
|
||||
}}
|
||||
/>
|
||||
<span data-test="mutual-check">
|
||||
With same custom check as <strong>{query}</strong>
|
||||
</span>
|
||||
</label>
|
||||
{nonSelectedQueryTypePermissions &&
|
||||
nonSelectedQueryTypePermissions?.map(
|
||||
({ queryType: type, data }: Record<string, any>) => (
|
||||
<div key={`${type}${queryType}`}>
|
||||
<label className="flex items-center gap-2">
|
||||
<input
|
||||
id={`custom_${type}`}
|
||||
data-testid={getUpdatePermissionBuilderIdSuffix(
|
||||
`external-${roleName}-${type}-input`,
|
||||
subQueryType
|
||||
)}
|
||||
type="radio"
|
||||
value={type}
|
||||
checked={selectedSection === type}
|
||||
onClick={() => {
|
||||
reset({
|
||||
...getValues(),
|
||||
...data,
|
||||
queryType,
|
||||
});
|
||||
|
||||
{selectedSection === query && (
|
||||
<div className="pt-4">
|
||||
{!isLoading && tableName ? (
|
||||
<RowPermissionBuilder
|
||||
permissionsKey={permissionsKey}
|
||||
table={table}
|
||||
dataSourceName={dataSourceName}
|
||||
setValue(rowPermissionsCheckType, type);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<>Loading...</>
|
||||
<span data-test="mutual-check">
|
||||
With same custom check as <strong>{type}</strong>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
{selectedSection === type && (
|
||||
<div
|
||||
// Permissions are not otherwise stored in plan JSON format in the dom.
|
||||
// This is a hack to get the JSON into the dom for testing.
|
||||
data-state={JSON.stringify(data[getRowPermission(type)])}
|
||||
data-testid="external-check-json-editor"
|
||||
className="mt-4 p-6 rounded-lg bg-white border border-gray-200 min-h-32 w-full"
|
||||
>
|
||||
<AceEditor
|
||||
mode="json"
|
||||
minLines={1}
|
||||
fontSize={14}
|
||||
height="18px"
|
||||
width="100%"
|
||||
theme="github"
|
||||
name={`${tableName}-json-editor`}
|
||||
value={JSON.stringify(data[getRowPermission(type)])}
|
||||
onChange={() => setValue(rowPermissionsCheckType, type)}
|
||||
editorProps={{ $blockScrolling: true }}
|
||||
setOptions={{ useWorker: false }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
)
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="flex items-center gap-2">
|
||||
@ -237,7 +267,7 @@ export const RowPermissionsSection: React.FC<RowPermissionsProps> = ({
|
||||
subQueryType
|
||||
)}
|
||||
value={SelectedSection.Custom}
|
||||
{...register(rowPermissionsCheckType)}
|
||||
checked={selectedSection === SelectedSection.Custom}
|
||||
onClick={() => {
|
||||
setValue(rowPermissionsCheckType, SelectedSection.Custom);
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
|
@ -16,10 +16,7 @@ import {
|
||||
TableEntry,
|
||||
} from '../../../../../../../metadata/types';
|
||||
|
||||
import {
|
||||
createPermissionsObject,
|
||||
getRowPermissionsForAllOtherQueriesMatchingSelectedRole,
|
||||
} from './utils';
|
||||
import { createPermissionsObject } from './utils';
|
||||
|
||||
interface GetMetadataTableArgs {
|
||||
table: unknown;
|
||||
@ -68,17 +65,11 @@ export const createDefaultValues = ({
|
||||
configuration: selectedTable?.configuration,
|
||||
});
|
||||
|
||||
const allRowChecks = getRowPermissionsForAllOtherQueriesMatchingSelectedRole(
|
||||
queryType,
|
||||
roleName,
|
||||
selectedTable
|
||||
);
|
||||
|
||||
const baseDefaultValues: DefaultValues = {
|
||||
queryType: 'select',
|
||||
filterType: 'none',
|
||||
columns: {},
|
||||
allRowChecks,
|
||||
|
||||
supportedOperators,
|
||||
};
|
||||
|
||||
@ -99,6 +90,5 @@ export const createDefaultValues = ({
|
||||
};
|
||||
|
||||
type DefaultValues = PermissionsSchema & {
|
||||
allRowChecks: { queryType: QueryType; value: string }[];
|
||||
operators?: Record<string, unknown>;
|
||||
};
|
||||
|
@ -90,60 +90,6 @@ const getColumns = (
|
||||
}, {});
|
||||
};
|
||||
|
||||
export const getAllRowChecks = (
|
||||
currentQuery: QueryType,
|
||||
allChecks: Array<{ queryType: QueryType; value: any }> = []
|
||||
) => {
|
||||
return allChecks
|
||||
.filter(({ queryType }) => queryType !== currentQuery)
|
||||
.map(({ queryType, value }) => {
|
||||
if (['insert', 'update'].includes(queryType)) {
|
||||
return { queryType, value: JSON.stringify(value.check || {}) };
|
||||
}
|
||||
|
||||
return {
|
||||
queryType,
|
||||
value: JSON.stringify(value.filter || {}),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export interface UseDefaultValuesArgs {
|
||||
dataSourceName: string;
|
||||
table: unknown;
|
||||
roleName: string;
|
||||
queryType: QueryType;
|
||||
}
|
||||
|
||||
export const getRowPermissionsForAllOtherQueriesMatchingSelectedRole = (
|
||||
selectedQuery: QueryType,
|
||||
selectedRole: string,
|
||||
table?: TableEntry
|
||||
) => {
|
||||
const res = Object.entries(table || {}).reduce<
|
||||
Array<{ queryType: QueryType; value: any }>
|
||||
>((acc, [key, value]) => {
|
||||
const props = { key, value };
|
||||
|
||||
// check object key of metadata is a permission
|
||||
if (isPermission(props)) {
|
||||
// add each role from each permission to the set
|
||||
props.value.forEach(permission => {
|
||||
if (permission.role === selectedRole) {
|
||||
acc.push({
|
||||
queryType: keyToPermission[props.key],
|
||||
value: permission.permission,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
return getAllRowChecks(selectedQuery, res);
|
||||
};
|
||||
|
||||
export const createPermission = {
|
||||
insert: (
|
||||
permission: InsertPermissionDefinition,
|
||||
|
@ -48,7 +48,6 @@ const defaultValuesMockResult: ReturnType<typeof createDefaultValues> = {
|
||||
Series_title_4: false,
|
||||
Series_title_5: false,
|
||||
},
|
||||
allRowChecks: [],
|
||||
supportedOperators: [
|
||||
{ name: 'equals', value: '_eq' },
|
||||
{ name: 'not equals', value: '_neq' },
|
||||
@ -83,5 +82,6 @@ const defaultValuesMockResult: ReturnType<typeof createDefaultValues> = {
|
||||
|
||||
test('use default values returns values correctly', () => {
|
||||
const result = createDefaultValues(useFormDataCreateDefaultValuesMock);
|
||||
|
||||
expect(result).toEqual(defaultValuesMockResult);
|
||||
});
|
||||
|
@ -47,6 +47,7 @@ export const useFormData = ({
|
||||
JSON.stringify(table),
|
||||
roleName,
|
||||
tableColumns,
|
||||
queryType,
|
||||
],
|
||||
queryFn: async () => {
|
||||
if (tableColumns.length === 0)
|
||||
@ -89,7 +90,6 @@ export const useFormData = ({
|
||||
|
||||
return { formData, defaultValues };
|
||||
},
|
||||
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
};
|
||||
|
@ -0,0 +1,98 @@
|
||||
import { MetadataTable } from '../../../hasura-metadata-types/source/table';
|
||||
export const partiallyAppliedPermissionsData = {
|
||||
table: ['Album'],
|
||||
select_permissions: [
|
||||
{
|
||||
role: 'asdf',
|
||||
permission: {
|
||||
columns: ['Title'],
|
||||
filter: { ArtistId: { _eq: 'X-Hasura-User-Id' } },
|
||||
allow_aggregations: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
permission: {
|
||||
columns: { Title: true },
|
||||
filter: { AlbumId: { _eq: 'X-Hasura-User-Id' } },
|
||||
check: { AlbumId: { _eq: 'X-Hasura-User-Id' } },
|
||||
},
|
||||
},
|
||||
],
|
||||
delete_permissions: [
|
||||
{ role: 'asdf', permission: { filter: {} } },
|
||||
{ role: 'testrole', permission: { filter: {} } },
|
||||
{
|
||||
role: 'user',
|
||||
permission: {
|
||||
filter: { AlbumId: { _eq: 'X-Hasura-User-I' } },
|
||||
check: { AlbumId: { _eq: 'X-Hasura-User-I' } },
|
||||
columns: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
} as MetadataTable;
|
||||
|
||||
export const fullyAppliedPermissionsData = {
|
||||
table: ['Album'],
|
||||
insert_permissions: [
|
||||
{
|
||||
role: 'user',
|
||||
permission: {
|
||||
check: { ArtistId: { _eq: 'X-Hasura-User-Id' } },
|
||||
columns: ['Title'],
|
||||
},
|
||||
},
|
||||
],
|
||||
select_permissions: [
|
||||
{
|
||||
role: 'asdf',
|
||||
permission: {
|
||||
columns: ['Title'],
|
||||
filter: { ArtistId: { _eq: 'X-Hasura-User-Id' } },
|
||||
allow_aggregations: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
permission: {
|
||||
columns: { AlbumId: true, Title: true },
|
||||
filter: { ArtistId: { _eq: 'X-Hasura-User-Id' } },
|
||||
check: { ArtistId: { _eq: 'X-Hasura-User-Id' } },
|
||||
},
|
||||
},
|
||||
],
|
||||
update_permissions: [
|
||||
{
|
||||
role: 'user',
|
||||
permission: {
|
||||
columns: { Title: true },
|
||||
filter: { Title: { _eq: 'X-Hasura-User-Id' } },
|
||||
check: { Title: { _eq: 'X-Hasura-User-Id' } },
|
||||
},
|
||||
},
|
||||
],
|
||||
delete_permissions: [
|
||||
{ role: 'asdf', permission: { filter: {} } },
|
||||
{ role: 'testrole', permission: { filter: {} } },
|
||||
{ role: 'user', permission: { filter: {}, check: {}, columns: {} } },
|
||||
],
|
||||
} as MetadataTable;
|
||||
|
||||
export const noAppliedPermissionsData = {
|
||||
table: ['Album'],
|
||||
select_permissions: [
|
||||
{
|
||||
role: 'asdf',
|
||||
permission: {
|
||||
columns: ['Title'],
|
||||
filter: { ArtistId: { _eq: 'X-Hasura-User-Id' } },
|
||||
allow_aggregations: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
delete_permissions: [
|
||||
{ role: 'asdf', permission: { filter: {} } },
|
||||
{ role: 'testrole', permission: { filter: {} } },
|
||||
],
|
||||
} as MetadataTable;
|
@ -0,0 +1,71 @@
|
||||
import { getNonSelectedQueryTypePermissions } from './getMapQueryTypePermissions';
|
||||
import {
|
||||
partiallyAppliedPermissionsData,
|
||||
fullyAppliedPermissionsData,
|
||||
noAppliedPermissionsData,
|
||||
} from './getMapQueryTypePermissions.mocks';
|
||||
|
||||
describe('getMapQueryTypePermissions should', () => {
|
||||
test('return existing permissions for table', () => {
|
||||
const result = getNonSelectedQueryTypePermissions(
|
||||
partiallyAppliedPermissionsData,
|
||||
'insert',
|
||||
'user'
|
||||
);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
queryType: 'select',
|
||||
data: {
|
||||
columns: { Title: true },
|
||||
filter: { AlbumId: { _eq: 'X-Hasura-User-Id' } },
|
||||
check: { AlbumId: { _eq: 'X-Hasura-User-Id' } },
|
||||
},
|
||||
},
|
||||
{
|
||||
queryType: 'delete',
|
||||
data: {
|
||||
filter: { AlbumId: { _eq: 'X-Hasura-User-I' } },
|
||||
check: { AlbumId: { _eq: 'X-Hasura-User-I' } },
|
||||
columns: {},
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('returns all existing permission except the current one', () => {
|
||||
const result = getNonSelectedQueryTypePermissions(
|
||||
fullyAppliedPermissionsData,
|
||||
'insert',
|
||||
'user'
|
||||
);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
queryType: 'select',
|
||||
data: {
|
||||
columns: { AlbumId: true, Title: true },
|
||||
filter: { ArtistId: { _eq: 'X-Hasura-User-Id' } },
|
||||
check: { ArtistId: { _eq: 'X-Hasura-User-Id' } },
|
||||
},
|
||||
},
|
||||
{
|
||||
queryType: 'update',
|
||||
data: {
|
||||
columns: { Title: true },
|
||||
filter: { Title: { _eq: 'X-Hasura-User-Id' } },
|
||||
check: { Title: { _eq: 'X-Hasura-User-Id' } },
|
||||
},
|
||||
},
|
||||
{ queryType: 'delete', data: { filter: {}, check: {}, columns: {} } },
|
||||
]);
|
||||
});
|
||||
|
||||
test('return empty array when no permissions have been applied for table', () => {
|
||||
const result = getNonSelectedQueryTypePermissions(
|
||||
noAppliedPermissionsData,
|
||||
'insert',
|
||||
'user'
|
||||
);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
@ -0,0 +1,118 @@
|
||||
import { MetadataTable } from '../../../hasura-metadata-types/source/table';
|
||||
|
||||
const formatColumns = (columns: string[] | Record<string, boolean>) => {
|
||||
if (!Array.isArray(columns)) return columns || {};
|
||||
return (
|
||||
columns?.reduce((tally, column) => {
|
||||
return { ...tally, [column]: true };
|
||||
}, {}) || {}
|
||||
);
|
||||
};
|
||||
|
||||
const getPermissionsMappedByRole = ({
|
||||
tableData,
|
||||
key,
|
||||
currentRole,
|
||||
currentQueryType,
|
||||
tally,
|
||||
queryType,
|
||||
}: {
|
||||
tableData: MetadataTable;
|
||||
key:
|
||||
| 'delete_permissions'
|
||||
| 'insert_permissions'
|
||||
| 'select_permissions'
|
||||
| 'update_permissions';
|
||||
currentRole: string;
|
||||
currentQueryType: string;
|
||||
tally: Record<string, any>[];
|
||||
queryType: string;
|
||||
}) => {
|
||||
const permissions =
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
tableData?.[key]?.find(
|
||||
(data: { role: string }) => data.role === currentRole
|
||||
)?.permission;
|
||||
if (!permissions) return tally;
|
||||
|
||||
if (currentQueryType === 'update' || currentQueryType === 'insert') {
|
||||
permissions.check = permissions?.filter;
|
||||
} else if (currentQueryType === 'select' || currentQueryType === 'delete') {
|
||||
permissions.filter = permissions?.check;
|
||||
}
|
||||
permissions.columns = formatColumns(permissions.columns);
|
||||
|
||||
return [
|
||||
...tally,
|
||||
{
|
||||
queryType,
|
||||
data: permissions,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const getNonSelectedQueryTypePermissions = (
|
||||
tableData: MetadataTable,
|
||||
currentQueryType: string,
|
||||
currentRole: string
|
||||
) => {
|
||||
if (!tableData) return [];
|
||||
const metadataKeys = Object.keys(tableData);
|
||||
|
||||
const existingPermissions = metadataKeys
|
||||
?.filter(
|
||||
key =>
|
||||
key === 'update_permissions' ||
|
||||
key === 'select_permissions' ||
|
||||
key === 'delete_permissions' ||
|
||||
key === 'insert_permissions'
|
||||
)
|
||||
?.reduce((tally: Record<string, any>[], key: string) => {
|
||||
if (key === 'select_permissions' && currentQueryType !== 'select') {
|
||||
return getPermissionsMappedByRole({
|
||||
tableData,
|
||||
key,
|
||||
currentRole,
|
||||
currentQueryType,
|
||||
tally,
|
||||
queryType: 'select',
|
||||
});
|
||||
}
|
||||
if (key === 'update_permissions' && currentQueryType !== 'update') {
|
||||
return getPermissionsMappedByRole({
|
||||
tableData,
|
||||
key,
|
||||
currentRole,
|
||||
currentQueryType,
|
||||
tally,
|
||||
queryType: 'update',
|
||||
});
|
||||
}
|
||||
|
||||
if (key === 'delete_permissions' && currentQueryType !== 'delete') {
|
||||
return getPermissionsMappedByRole({
|
||||
tableData,
|
||||
key,
|
||||
currentRole,
|
||||
currentQueryType,
|
||||
tally,
|
||||
queryType: 'delete',
|
||||
});
|
||||
}
|
||||
|
||||
if (key === 'insert_permissions' && currentQueryType !== 'insert') {
|
||||
return getPermissionsMappedByRole({
|
||||
tableData,
|
||||
key,
|
||||
currentRole,
|
||||
currentQueryType,
|
||||
tally,
|
||||
queryType: 'insert',
|
||||
});
|
||||
}
|
||||
return tally;
|
||||
}, []);
|
||||
|
||||
return existingPermissions;
|
||||
};
|
@ -50,34 +50,30 @@ GDCUpdateTableCloneSelectPermission.parameters = {
|
||||
GDCUpdateTableCloneSelectPermission.play = async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await waitFor(
|
||||
async () => await canvas.getByTestId('permission-table-button-user-update')
|
||||
);
|
||||
|
||||
await canvas.findByTestId('permission-table-button-user-update');
|
||||
const tableUserUpdateButton = await canvas.getByTestId(
|
||||
'permission-table-button-user-update'
|
||||
);
|
||||
|
||||
await userEvent.click(tableUserUpdateButton);
|
||||
await waitFor(
|
||||
async () => await canvas.getByTestId('external-user-select-input-pre')
|
||||
);
|
||||
const selectCheckbox = await canvas.getByTestId(
|
||||
`external-user-select-input-pre`
|
||||
const selectCheckbox = await canvas.findByTestId(
|
||||
'external-user-select-input-pre'
|
||||
);
|
||||
await userEvent.click(selectCheckbox);
|
||||
await waitFor(async () => await canvas.getByTestId('row-permission-builder'));
|
||||
|
||||
await canvas.findByTestId('external-check-json-editor');
|
||||
const rowPermissionBuilderContainer = await canvas.getByTestId(
|
||||
`row-permission-builder`
|
||||
'external-check-json-editor'
|
||||
);
|
||||
|
||||
expect(rowPermissionBuilderContainer.getAttribute('data-state')).toEqual(
|
||||
'{"ArtistId":{"_eq":"X-Hasura-User-Id"}}'
|
||||
);
|
||||
};
|
||||
|
||||
export const GDCUpdateTableCreatePermissions: Story<
|
||||
PermissionsTabProps
|
||||
> = args => <PermissionsTab {...args} />;
|
||||
// export const GDCUpdateTableCreatePermissions: Story<
|
||||
// PermissionsTabProps
|
||||
// > = args => <PermissionsTab {...args} />;
|
||||
|
||||
// GDCUpdateTableCreatePermissions.args = {
|
||||
// dataSourceName: 'sqlite',
|
||||
|
@ -127,8 +127,10 @@ export const PermissionsTable: React.FC<PermissionsTableProps> = ({
|
||||
|
||||
{permissionTypes.map(({ permissionType, access }) => {
|
||||
// TODO: add checks to see what permissions are supported by each db
|
||||
|
||||
const isEditable =
|
||||
driverSupportedQueries.includes(permissionType);
|
||||
driverSupportedQueries.includes(permissionType) ||
|
||||
roleName !== 'admin';
|
||||
|
||||
if (isNewRole) {
|
||||
return (
|
||||
|
Loading…
Reference in New Issue
Block a user