diff --git a/source/commands/env/apply/trino.tsx b/source/commands/env/apply/trino.tsx index ea1d9db6..9194a728 100644 --- a/source/commands/env/apply/trino.tsx +++ b/source/commands/env/apply/trino.tsx @@ -56,6 +56,16 @@ export const options = zod.object({ alias: 's', }), ), + createColumnResources: zod + .boolean() + .optional() + .default(false) + .describe( + option({ + description: 'Create individual column resources (default: false)', + alias: 'cols', + }), + ), }); export default function Trino({ options }: { options: TrinoOptions }) { diff --git a/source/commands/pdp/run.tsx b/source/commands/pdp/run.tsx index edb8c1b5..6e01e5d6 100644 --- a/source/commands/pdp/run.tsx +++ b/source/commands/pdp/run.tsx @@ -28,16 +28,24 @@ export const options = object({ alias: 'k', }), ), + tag: string() + .default('latest') + .describe( + option({ + description: 'The tag of the PDP image to use', + alias: 't', + }), + ), }); type Props = { options: zInfer; }; -export default function Run({ options: { opa, dryRun, apiKey } }: Props) { +export default function Run({ options: { opa, dryRun, apiKey, tag } }: Props) { return ( - + ); } diff --git a/source/components/env/trino/TrinoComponent.tsx b/source/components/env/trino/TrinoComponent.tsx index 7b7b2e41..a74a5b49 100644 --- a/source/components/env/trino/TrinoComponent.tsx +++ b/source/components/env/trino/TrinoComponent.tsx @@ -21,7 +21,9 @@ export default function TrinoComponent( catalog: props.catalog, schema: props.schema, }); - const permitResources = mapTrinoSchemaToPermitResources(trinoSchema); + const permitResources = mapTrinoSchemaToPermitResources(trinoSchema, { + createColumnResources: props.createColumnResources, + }); setCreatedResources(permitResources); await processTrinoSchema(props); })(); diff --git a/source/components/env/trino/types.ts b/source/components/env/trino/types.ts index 413099dc..8c59d8dd 100644 --- a/source/components/env/trino/types.ts +++ b/source/components/env/trino/types.ts @@ -5,6 +5,7 @@ export type TrinoOptions = { password?: string; catalog?: string; schema?: string; + createColumnResources?: boolean; }; export interface PermitResource { diff --git a/source/components/pdp/PDPRunComponent.tsx b/source/components/pdp/PDPRunComponent.tsx index c9100d11..a968e315 100644 --- a/source/components/pdp/PDPRunComponent.tsx +++ b/source/components/pdp/PDPRunComponent.tsx @@ -16,6 +16,7 @@ type Props = { onComplete?: () => void; onError?: (error: string) => void; skipWaitScreen?: boolean; // New prop to control wait behavior + tag?: string; }; export default function PDPRunComponent({ @@ -24,6 +25,7 @@ export default function PDPRunComponent({ onComplete, onError, skipWaitScreen = true, // Default to showing wait screen + tag = 'latest', }: Props) { const { authToken } = useAuth(); const [loading, setLoading] = useState(true); @@ -82,7 +84,7 @@ export default function PDPRunComponent({ // Generate the Docker command const cmd = `docker run -d -p 7766:7000 ${ opa ? `-p ${opa}:8181` : '' - } -e PDP_API_KEY=${token} -e PDP_CONTROL_PLANE=${config.controlPlane || 'https://api.permit.io'} permitio/pdp-v2:latest`; + } -e PDP_API_KEY=${token} -e PDP_CONTROL_PLANE=${config.controlPlane || 'https://api.permit.io'} permitio/pdp-v2:${tag}`; setDockerCommand(cmd); diff --git a/source/utils/trinoUtils.ts b/source/utils/trinoUtils.ts index 6a951e4c..ffeaa658 100644 --- a/source/utils/trinoUtils.ts +++ b/source/utils/trinoUtils.ts @@ -105,20 +105,24 @@ export function trinoTypeToPermitType( /** * Map Trino schema data to Permit resources. - * - Each catalog, schema, table, and column is a resource. + * - Each catalog, schema, table, and optionally column is a resource. * - Each table resource includes columns as attributes (with type/description). + * @param trino - The Trino schema data to map + * @param options - Configuration options + * @param options.createColumnResources - Whether to create individual column resources (default: false) */ export function mapTrinoSchemaToPermitResources( trino: TrinoSchemaData, + options: { createColumnResources?: boolean } = {}, ): PermitResource[] { const resources: PermitResource[] = []; - const SEP = '-'; + const SEP = '_'; // Catalogs for (const catalog of trino.catalogs) { resources.push({ key: `trino${SEP}catalog${SEP}${catalog.name}`, - name: catalog.name, + name: `Catalog: ${catalog.name}`, description: `Trino resource type: catalog. Trino catalog: ${catalog.name}`, actions: [ 'AccessCatalog', @@ -133,7 +137,7 @@ export function mapTrinoSchemaToPermitResources( for (const schema of trino.schemas) { resources.push({ key: `trino${SEP}schema${SEP}${schema.catalog}${SEP}${schema.name}`, - name: `${schema.catalog}.${schema.name}`, + name: `Schema: ${schema.catalog}.${schema.name}`, description: `Trino resource type: schema. Schema ${schema.name} in catalog ${schema.catalog}`, actions: [ 'CreateSchema', @@ -175,7 +179,7 @@ export function mapTrinoSchemaToPermitResources( const tableKey = `trino${SEP}table${SEP}${table.catalog}${SEP}${table.schema}${SEP}${table.name}`; resources.push({ key: tableKey, - name: `${table.catalog}.${table.schema}.${table.name}`, + name: `Table: ${table.catalog}.${table.schema}.${table.name}`, description: `Trino resource type: ${table.type.toLowerCase()}. ${table.type} ${table.name} in ${table.catalog}.${table.schema}`, actions: TABLE_AND_COLUMN_ACTIONS, attributes: table.columns.reduce( @@ -202,29 +206,35 @@ export function mapTrinoSchemaToPermitResources( }, ), }); - // Columns as resources - for (const column of table.columns) { - resources.push({ - key: `trino${SEP}column${SEP}${table.catalog}${SEP}${table.schema}${SEP}${table.name}${SEP}${column.name}`, - name: `${table.catalog}.${table.schema}.${table.name}.${column.name}`, - description: `Trino resource type: column. Column ${column.name} in ${table.catalog}.${table.schema}.${table.name}`, - actions: TABLE_AND_COLUMN_ACTIONS, - attributes: { - parent_table: { - type: 'string', - description: `${table.catalog}.${table.schema}.${table.name}`, - }, - table_type: { type: 'string', description: table.type.toLowerCase() }, - type: { - type: trinoTypeToPermitType(column.type), - description: column.type, - }, - nullable: { - type: 'bool', - description: column.nullable ? 'nullable' : undefined, + + // Create column resources if requested + if (options.createColumnResources) { + for (const column of table.columns) { + resources.push({ + key: `trino${SEP}column${SEP}${table.catalog}${SEP}${table.schema}${SEP}${table.name}${SEP}${column.name}`, + name: `Column: ${table.catalog}.${table.schema}.${table.name}.${column.name}`, + description: `Trino resource type: column. Column ${column.name} in ${table.catalog}.${table.schema}.${table.name}`, + actions: TABLE_AND_COLUMN_ACTIONS, + attributes: { + parent_table: { + type: 'string', + description: `${table.catalog}.${table.schema}.${table.name}`, + }, + table_type: { + type: 'string', + description: table.type.toLowerCase(), + }, + type: { + type: trinoTypeToPermitType(column.type), + description: column.type, + }, + nullable: { + type: 'bool', + description: column.nullable ? 'nullable' : undefined, + }, }, - }, - }); + }); + } } } @@ -232,7 +242,7 @@ export function mapTrinoSchemaToPermitResources( for (const view of trino.views) { resources.push({ key: `trino${SEP}view${SEP}${view.catalog}${SEP}${view.schema}${SEP}${view.name}`, - name: `${view.catalog}.${view.schema}.${view.name}`, + name: `View: ${view.catalog}.${view.schema}.${view.name}`, description: `Trino resource type: view. View ${view.name} in ${view.catalog}.${view.schema}`, actions: [ 'CreateView', @@ -272,7 +282,7 @@ export function mapTrinoSchemaToPermitResources( for (const mview of trino.materializedViews) { resources.push({ key: `trino${SEP}materialized_view${SEP}${mview.catalog}${SEP}${mview.schema}${SEP}${mview.name}`, - name: `${mview.catalog}.${mview.schema}.${mview.name}`, + name: `Materialized View: ${mview.catalog}.${mview.schema}.${mview.name}`, description: `Trino resource type: materialized view. Materialized view ${mview.name} in ${mview.catalog}.${mview.schema}`, actions: [ 'CreateMaterializedView', @@ -311,7 +321,7 @@ export function mapTrinoSchemaToPermitResources( for (const fn of trino.functions) { resources.push({ key: `trino${SEP}function${SEP}${fn.catalog}${SEP}${fn.schema}${SEP}${fn.name}`, - name: `${fn.catalog}.${fn.schema}.${fn.name}`, + name: `Function: ${fn.catalog}.${fn.schema}.${fn.name}`, description: `Trino resource type: function. Function ${fn.name} in ${fn.catalog}.${fn.schema}`, actions: [ 'ShowFunctions', @@ -333,7 +343,7 @@ export function mapTrinoSchemaToPermitResources( for (const proc of trino.procedures) { resources.push({ key: `trino${SEP}procedure${SEP}${proc.catalog}${SEP}${proc.schema}${SEP}${proc.name}`, - name: `${proc.catalog}.${proc.schema}.${proc.name}`, + name: `Procedure: ${proc.catalog}.${proc.schema}.${proc.name}`, description: `Trino resource type: procedure. Procedure ${proc.name} in ${proc.catalog}.${proc.schema}`, actions: ['ExecuteProcedure', 'ExecuteTableProcedure'], attributes: { @@ -426,20 +436,20 @@ export async function fetchTrinoFunctionsAndProceduresPassthrough( if (catalog.toLowerCase() === 'postgresql') { const passthrough = `SELECT * FROM TABLE(postgresql.system.query(query => ' - SELECT p.proname as function_name, n.nspname as schema_name, - pg_catalog.pg_get_function_result(p.oid) as return_type, - pg_catalog.pg_get_function_arguments(p.oid) as arguments, - CASE p.prokind - WHEN ''f'' THEN ''FUNCTION'' - WHEN ''p'' THEN ''PROCEDURE'' - WHEN ''a'' THEN ''AGGREGATE'' - WHEN ''w'' THEN ''WINDOW'' - ELSE p.prokind::text - END as kind - FROM pg_catalog.pg_proc p - LEFT JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace - WHERE n.nspname = ''${schema}'' - AND p.proname NOT LIKE ''pg_%'' + SELECT p.proname as function_name, n.nspname as schema_name, + pg_catalog.pg_get_function_result(p.oid) as return_type, + pg_catalog.pg_get_function_arguments(p.oid) as arguments, + CASE p.prokind + WHEN ''f'' THEN ''FUNCTION'' + WHEN ''p'' THEN ''PROCEDURE'' + WHEN ''a'' THEN ''AGGREGATE'' + WHEN ''w'' THEN ''WINDOW'' + ELSE p.prokind::text + END as kind + FROM pg_catalog.pg_proc p + LEFT JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace + WHERE n.nspname = ''${schema}'' + AND p.proname NOT LIKE ''pg_%'' ORDER BY p.proname '))`; try { @@ -472,8 +482,8 @@ export async function fetchTrinoFunctionsAndProceduresPassthrough( } } else if (catalog.toLowerCase() === 'mysql') { const passthrough = `SELECT * FROM TABLE(mysql.system.query(query => ' - SELECT routine_name, routine_type, data_type, routine_definition - FROM information_schema.routines + SELECT routine_name, routine_type, data_type, routine_definition + FROM information_schema.routines WHERE routine_schema = ''${schema}'' '))`; try { @@ -556,8 +566,8 @@ export async function fetchTrinoSchema( const tableComments = new Map(); try { const commentsQuery = ` - SELECT catalog_name, schema_name, table_name, comment - FROM system.metadata.table_comments + SELECT catalog_name, schema_name, table_name, comment + FROM system.metadata.table_comments WHERE comment IS NOT NULL `; const commentRows = await executeTrinoQuery(client, commentsQuery); diff --git a/tests/trino/docker-compose.yml b/tests/trino/docker-compose.yml index e7815389..7e670b7f 100644 --- a/tests/trino/docker-compose.yml +++ b/tests/trino/docker-compose.yml @@ -14,7 +14,7 @@ services: - ./sample_data/postgres_init.sql:/docker-entrypoint-initdb.d/init.sql healthcheck: test: ['CMD-SHELL', 'pg_isready -U testuser -d testdb'] - interval: 10s + interval: 5s timeout: 5s retries: 5 @@ -42,7 +42,7 @@ services: 'testuser', '-ptestpass', ] - interval: 10s + interval: 5s timeout: 5s retries: 5 diff --git a/tests/trino/trino_config/access-control.properties b/tests/trino/trino_config/access-control.properties new file mode 100644 index 00000000..96e1dac8 --- /dev/null +++ b/tests/trino/trino_config/access-control.properties @@ -0,0 +1,5 @@ +# access-control.name=allow-all +access-control.name=opa +opa.policy.uri=http://host.docker.internal:7766/trino/allowed +opa.log-requests=true +opa.log-responses=true diff --git a/tests/utils/trinoUtils.test.tsx b/tests/utils/trinoUtils.test.tsx index 635adba9..fd87550d 100644 --- a/tests/utils/trinoUtils.test.tsx +++ b/tests/utils/trinoUtils.test.tsx @@ -40,28 +40,30 @@ describe('mapTrinoSchemaToPermitResources', () => { materializedViews: [], procedures: [], }; - const resources = mapTrinoSchemaToPermitResources(schema); + const resources = mapTrinoSchemaToPermitResources(schema, { + createColumnResources: true, + }); // Check catalogs - const catalog = resources.find(r => r.key === 'trino-catalog-testcat'); + const catalog = resources.find(r => r.key === 'trino_catalog_testcat'); expect(catalog).toBeDefined(); - expect(catalog?.name).toBe('testcat'); + expect(catalog?.name).toBe('Catalog: testcat'); expect(catalog?.actions).toContain('AccessCatalog'); // Check schemas const schemaResource = resources.find( - r => r.key === 'trino-schema-testcat-public', + r => r.key === 'trino_schema_testcat_public', ); expect(schemaResource).toBeDefined(); - expect(schemaResource?.name).toBe('testcat.public'); + expect(schemaResource?.name).toBe('Schema: testcat.public'); expect(schemaResource?.actions).toContain('CreateSchema'); // Check tables const table = resources.find( - r => r.key === 'trino-table-testcat-public-users', + r => r.key === 'trino_table_testcat_public_users', ); expect(table).toBeDefined(); - expect(table?.name).toBe('testcat.public.users'); + expect(table?.name).toBe('Table: testcat.public.users'); expect(table?.actions).toContain('CreateTable'); expect(table?.attributes).toBeDefined(); expect(table?.attributes?.id).toEqual({ type: 'number' }); @@ -69,10 +71,10 @@ describe('mapTrinoSchemaToPermitResources', () => { // Check columns const column = resources.find( - r => r.key === 'trino-column-testcat-public-users-id', + r => r.key === 'trino_column_testcat_public_users_id', ); expect(column).toBeDefined(); - expect(column?.name).toBe('testcat.public.users.id'); + expect(column?.name).toBe('Column: testcat.public.users.id'); expect(column?.actions).toContain('SelectFromColumns'); }); @@ -123,10 +125,10 @@ describe('mapTrinoSchemaToPermitResources', () => { // Check function const func = resources.find( - r => r.key === 'trino-function-testcat-public-my_func', + r => r.key === 'trino_function_testcat_public_my_func', ); expect(func).toBeDefined(); - expect(func?.name).toBe('testcat.public.my_func'); + expect(func?.name).toBe('Function: testcat.public.my_func'); expect(func?.actions).toContain('ExecuteFunction'); expect(func?.actions).toContain('ShowFunctions'); expect(func?.attributes?.returnType).toEqual({ type: 'number' }); @@ -134,10 +136,10 @@ describe('mapTrinoSchemaToPermitResources', () => { // Check view const view = resources.find( - r => r.key === 'trino-view-testcat-public-my_view', + r => r.key === 'trino_view_testcat_public_my_view', ); expect(view).toBeDefined(); - expect(view?.name).toBe('testcat.public.my_view'); + expect(view?.name).toBe('View: testcat.public.my_view'); expect(view?.actions).toContain('CreateView'); expect(view?.actions).toContain('DropView'); expect(view?.attributes?.col1).toEqual({ type: 'string' }); @@ -148,20 +150,20 @@ describe('mapTrinoSchemaToPermitResources', () => { // Check materialized view const mview = resources.find( - r => r.key === 'trino-materialized_view-testcat-public-my_mview', + r => r.key === 'trino_materialized_view_testcat_public_my_mview', ); expect(mview).toBeDefined(); - expect(mview?.name).toBe('testcat.public.my_mview'); + expect(mview?.name).toBe('Materialized View: testcat.public.my_mview'); expect(mview?.actions).toContain('CreateMaterializedView'); expect(mview?.actions).toContain('RefreshMaterializedView'); expect(mview?.attributes?.total).toEqual({ type: 'number' }); // Check procedure const proc = resources.find( - r => r.key === 'trino-procedure-testcat-public-my_proc', + r => r.key === 'trino_procedure_testcat_public_my_proc', ); expect(proc).toBeDefined(); - expect(proc?.name).toBe('testcat.public.my_proc'); + expect(proc?.name).toBe('Procedure: testcat.public.my_proc'); expect(proc?.actions).toContain('ExecuteProcedure'); expect(proc?.attributes?.argumentTypes).toEqual({ type: 'array' }); });