console: add hook useEnabledRoleFromAllowList

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/5645
GitOrigin-RevId: 9484f4512839b3790f56110800dab590c996eece
This commit is contained in:
Varun Choudhary 2022-08-29 13:40:23 +05:30 committed by hasura-bot
parent d5b356c53f
commit 7773687528
9 changed files with 235 additions and 0 deletions

View File

@ -0,0 +1 @@
export * from './useEnabledRolesFromAllowList';

View File

@ -0,0 +1,11 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import { rest } from 'msw';
import { metadata } from './metadata';
const baseUrl = 'http://localhost:8080';
export const handlers = (url = baseUrl) => [
rest.post(`${url}/v1/metadata`, (req, res, ctx) => {
return res(ctx.json(metadata));
}),
];

View File

@ -0,0 +1,106 @@
import { MetadataResponse } from '@/features/MetadataAPI';
export const metadata: MetadataResponse = {
resource_version: 2,
metadata: {
version: 3,
inherited_roles: [],
sources: [
{
name: 'default',
kind: 'postgres',
tables: [
{
table: { name: 'test', schema: 'public' },
insert_permissions: [
{ role: 'manager', permission: { check: {}, columns: [] } },
{ role: 'users', permission: { check: {}, columns: ['id'] } },
],
},
],
configuration: {
connection_info: {
use_prepared_statements: true,
database_url: {
from_env: 'HASURA_GRAPHQL_DATABASE_URL',
},
isolation_level: 'read-committed',
pool_settings: {
connection_lifetime: 600,
retries: 1,
idle_timeout: 180,
max_connections: 50,
},
},
},
},
],
remote_schemas: [
{
name: 'RS1',
definition: {
url: 'https://graphql-pokemon2.vercel.app',
timeout_seconds: 60,
},
permissions: [
{
role: 'rs_role',
definition: {
schema:
'schema { query: Query }\n\ntype Attack { damage: Int\n name: String\n type: String\n}\n\ntype Pokemon { attacks: PokemonAttack\n classification: String\n evolutionRequirements: PokemonEvolutionRequirement\n evolutions: [Pokemon]\n fleeRate: Float\n height: PokemonDimension\n id: ID!\n image: String\n maxCP: Int\n maxHP: Int\n name: String\n number: String\n resistant: [String]\n types: [String]\n weaknesses: [String]\n weight: PokemonDimension\n}\n\ntype PokemonAttack { fast: [Attack]\n special: [Attack]\n}\n\ntype PokemonDimension { maximum: String\n minimum: String\n}\n\ntype PokemonEvolutionRequirement { amount: Int\n name: String\n}\n\ntype Query { pokemon(id: String @preset(value: "X-Hasura-User-Id"), name: String @preset(value: "X-Hasura-User-Id")): Pokemon\n pokemons(first: Int!): [Pokemon]\n query: Query\n}',
},
remote_schema_name: '',
comment: null,
},
],
},
],
query_collections: [
{
name: 'allowed-queries',
definition: {
queries: [
{
name: 'introspection query',
query:
'query IntrospectionQuery {\n __schema {\n queryType { name }\n mutationType { name }\n subscriptionType { name }\n types {\n ...FullType\n }\n directives {\n name\n description\n locations\n args {\n ...InputValue\n }\n }\n }\n }\n\n fragment FullType on __Type {\n kind\n name\n description\n fields(includeDeprecated: true) {\n name\n description\n args {\n ...InputValue\n }\n type {\n ...TypeRef\n }\n isDeprecated\n deprecationReason\n }\n inputFields {\n ...InputValue\n }\n interfaces {\n ...TypeRef\n }\n enumValues(includeDeprecated: true) {\n name\n description\n isDeprecated\n deprecationReason\n }\n possibleTypes {\n ...TypeRef\n }\n }\n\n fragment InputValue on __InputValue {\n name\n description\n type { ...TypeRef }\n defaultValue\n }\n\n fragment TypeRef on __Type {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n }\n }\n }\n }\n }\n }\n }\n }',
},
],
},
},
],
allowlist: [
{
collection: 'allowed-queries',
scope: {
global: false,
roles: ['manager'],
},
},
{
collection: 'admin-only-queries',
scope: {
global: false,
roles: ['users'],
},
},
{
collection: 'rest-endpoint',
scope: {
global: false,
roles: ['users', 'manager'],
},
},
],
},
};
export const metadata_with_no_query_collections: MetadataResponse = {
resource_version: 48,
metadata: {
version: 3,
sources: [],
remote_schemas: [],
inherited_roles: [],
},
};

View File

@ -0,0 +1,42 @@
import React from 'react';
import { ReactQueryDecorator } from '@/storybook/decorators/react-query';
import { ReduxDecorator } from '@/storybook/decorators/redux-decorator';
import ReactJson from 'react-json-view';
import { Meta, Story } from '@storybook/react';
import { useEnabledRolesFromAllowList } from './useEnabledRolesFromAllowList';
import { handlers } from './mock/handlers.mocks';
const UseEnabledRolesFromAllowList: React.FC = () => {
const { data, isLoading, isError } =
useEnabledRolesFromAllowList('rest-endpoint');
if (isLoading) {
return <div>Loading...</div>;
}
if (isError) {
return <div>Error</div>;
}
return data ? <ReactJson src={data} /> : null;
};
export const Primary: Story = args => {
return <UseEnabledRolesFromAllowList {...args} />;
};
Primary.args = {
collectionName: 'rest-endpoint',
};
export default {
title: 'hooks/Allow List Permission/useCreateNewRolePermission',
decorators: [
ReduxDecorator({ tables: { currentDataSource: 'default' } }),
ReactQueryDecorator(),
],
parameters: {
msw: handlers(),
},
} as Meta;

View File

@ -0,0 +1,55 @@
import { setupServer } from 'msw/node';
import { rest } from 'msw';
import { renderHook } from '@testing-library/react-hooks';
import { metadata, metadata_with_no_query_collections } from './mock/metadata';
import { wrapper } from '../../../../hooks/__tests__/common/decorator';
import { useEnabledRolesFromAllowList } from '../../hooks/AllowListPermissions/useEnabledRolesFromAllowList';
const server = setupServer();
beforeAll(() => server.listen());
afterAll(() => server.close());
describe('useEnabledRolesFromAllowList with valid data', () => {
beforeEach(() => {
server.use(
rest.post('/v1/metadata', (req, res, ctx) =>
res(ctx.status(200), ctx.json(metadata))
)
);
});
test('When useEnabledRolesFromAllowList hook is called with query collection Name, then a valid list of enabled roles are returned', async () => {
const { result, waitForValueToChange } = renderHook(
() => useEnabledRolesFromAllowList('allowed-queries'),
{ wrapper }
);
await waitForValueToChange(() => result.current.data);
const roles = result.current.data!;
expect(roles).toEqual(['manager']);
});
});
describe("useEnabledRolesFromAllowList hooks' with no query collections", () => {
beforeEach(() => {
server.use(
rest.post('/v1/metadata', (req, res, ctx) =>
res(ctx.status(200), ctx.json(metadata_with_no_query_collections))
)
);
});
test('When useEnabledRolesFromAllowListn is called with an invalid query collection, then empty array should be returned', async () => {
const { result, waitForValueToChange } = renderHook(
() => useEnabledRolesFromAllowList('allowed-queries'),
{ wrapper }
);
await waitForValueToChange(() => result.current.data);
const roles = result.current.data!;
expect(roles).toEqual([]);
});
});

View File

@ -0,0 +1,4 @@
import { MetadataSelector, useMetadata } from '@/features/MetadataAPI';
export const useEnabledRolesFromAllowList = (queryCollectionName: string) =>
useMetadata(MetadataSelector.getNewRolePermission(queryCollectionName));

View File

@ -271,4 +271,13 @@ export namespace MetadataSelector {
);
return queryCollectionDefinition?.definition?.queries ?? [];
};
export const getNewRolePermission =
(queryCollectionName: string) => (m: MetadataResponse) => {
const queryCollectionDefinition = m.metadata?.allowlist?.find(
qs => qs.collection === queryCollectionName
);
return queryCollectionDefinition?.scope?.global === false
? queryCollectionDefinition?.scope?.roles
: [];
};
}

View File

@ -22,6 +22,7 @@ export function useRoles() {
MetadataSelector.getTablesFromAllSources
);
const { data: remoteSchemas } = useMetadata(d => d.metadata.remote_schemas);
const { data: allowlists } = useMetadata(d => d.metadata.allowlist);
const { data: securitySettings } = useMetadata(
MetadataSelector.getSecuritySettings
);
@ -37,6 +38,11 @@ export function useRoles() {
remoteSchemas?.forEach(remoteSchema => {
remoteSchema?.permissions?.forEach(p => roleNames.push(p.role));
});
allowlists?.forEach(allowlist => {
if (allowlist?.scope?.global === false) {
allowlist?.scope?.roles?.forEach(role => roleNames.push(role));
}
});
Object.entries(securitySettings?.api_limits ?? {}).forEach(
([limit, value]) => {

View File

@ -39,6 +39,7 @@ export const allowedMetadataTypesArr = [
'update_remote_schema_remote_relationship',
'delete_remote_schema_remote_relationship',
'add_remote_schema',
'update_scope_of_collection_in_allowlist',
'bulk',
] as const;