diff --git a/src/lib/PostgresMeta.ts b/src/lib/PostgresMeta.ts index 04d6634f..06844ba2 100644 --- a/src/lib/PostgresMeta.ts +++ b/src/lib/PostgresMeta.ts @@ -1,14 +1,17 @@ import { PoolConfig } from 'pg' import * as Parser from './Parser.js' +import PostgresMetaColumnPermissions from './PostgresMetaColumnPermissions.js' import PostgresMetaColumns from './PostgresMetaColumns.js' import PostgresMetaConfig from './PostgresMetaConfig.js' import PostgresMetaExtensions from './PostgresMetaExtensions.js' import PostgresMetaForeignTables from './PostgresMetaForeignTables.js' import PostgresMetaFunctions from './PostgresMetaFunctions.js' +import PostgresMetaPermissions from './PostgresMetaPermissions.js' import PostgresMetaPolicies from './PostgresMetaPolicies.js' import PostgresMetaPublications from './PostgresMetaPublications.js' import PostgresMetaRoles from './PostgresMetaRoles.js' import PostgresMetaSchemas from './PostgresMetaSchemas.js' +import PostgresMetaTablePermissions from './PostgresMetaTablePermissions.js' import PostgresMetaTables from './PostgresMetaTables.js' import PostgresMetaTriggers from './PostgresMetaTriggers.js' import PostgresMetaTypes from './PostgresMetaTypes.js' @@ -20,15 +23,18 @@ import { PostgresMetaResult } from './types.js' export default class PostgresMeta { query: (sql: string) => Promise> end: () => Promise + columnPermissions: PostgresMetaColumnPermissions columns: PostgresMetaColumns config: PostgresMetaConfig extensions: PostgresMetaExtensions foreignTables: PostgresMetaForeignTables functions: PostgresMetaFunctions + permissions: PostgresMetaPermissions policies: PostgresMetaPolicies publications: PostgresMetaPublications roles: PostgresMetaRoles schemas: PostgresMetaSchemas + tablePermissions: PostgresMetaTablePermissions tables: PostgresMetaTables triggers: PostgresMetaTriggers types: PostgresMetaTypes @@ -44,14 +50,17 @@ export default class PostgresMeta { this.query = query this.end = end this.columns = new PostgresMetaColumns(this.query) + this.columnPermissions = new PostgresMetaColumnPermissions(this.query) this.config = new PostgresMetaConfig(this.query) this.extensions = new PostgresMetaExtensions(this.query) this.foreignTables = new PostgresMetaForeignTables(this.query) this.functions = new PostgresMetaFunctions(this.query) + this.permissions = new PostgresMetaPermissions(this.query) this.policies = new PostgresMetaPolicies(this.query) this.publications = new PostgresMetaPublications(this.query) this.roles = new PostgresMetaRoles(this.query) this.schemas = new PostgresMetaSchemas(this.query) + this.tablePermissions = new PostgresMetaTablePermissions(this.query) this.tables = new PostgresMetaTables(this.query) this.triggers = new PostgresMetaTriggers(this.query) this.types = new PostgresMetaTypes(this.query) diff --git a/src/lib/PostgresMetaColumnPermissions.ts b/src/lib/PostgresMetaColumnPermissions.ts new file mode 100644 index 00000000..590f3c9d --- /dev/null +++ b/src/lib/PostgresMetaColumnPermissions.ts @@ -0,0 +1,110 @@ +import { ident, literal } from 'pg-format' +import { ColumnPermissionListSchema } from './inputs.js' +import { columnPermissionsSql } from './sql/index.js' +import { PostgresMetaResult, PostgresColumnPermission } from './types.js' + +export default class PostgresMetaColumns { + query: (sql: string) => Promise> + + constructor(query: (sql: string) => Promise>) { + this.query = query + } + + async list({ + table_schema, + table_name, + column_name, + privilege, + include_system_schemas = false, + limit, + offset, + }: ColumnPermissionListSchema = {}): Promise> { + let sql = ` +WITH + column_permissions AS (${columnPermissionsSql}) +SELECT + * +FROM + column_permissions +WHERE + true` + if (table_schema) { + sql += ` AND table_schema = ${literal(table_schema)}` + } + if (table_name) { + sql += ` AND table_name = ${literal(table_name)}` + } + if (column_name) { + sql += ` AND column_name = ${literal(column_name)}` + } + if (privilege) { + sql += ` AND privilege = ${literal(privilege)}` + } + if (!include_system_schemas) { + sql += ` AND table_schema NOT LIKE 'pg_%'` + sql += ` AND table_schema != 'information_schema'` + } + if (limit) { + sql += ` LIMIT ${limit}` + } + if (offset) { + sql += ` OFFSET ${offset}` + } + console.log(sql) + return await this.query(sql) + } + + async grant( + column_name: string, + { + table_schema, + table_name, + privilege_type, + role, + }: { + table_schema: string + table_name: string + privilege_type?: 'SELECT' | 'INSERT' | 'UPDATE' + role: string + } + ): Promise> { + let sql = 'GRANT ' + sql += privilege_type ?? 'ALL PRIVILEGES' + sql += ` (${ident(column_name)}) on ${ident(table_schema)}.${ident(table_name)}` + sql += ` to ${ident(role)}` + { + const { error } = await this.query(sql) + if (error) { + return { data: null, error } + } + } + return { data: 'OK', error: null } + } + + async revoke( + column_name: string, + { + table_schema, + table_name, + privilege_type, + role, + }: { + table_schema: string + table_name: string + privilege_type?: 'SELECT' | 'INSERT' | 'UPDATE' + role: string + } + ): Promise> { + let sql = 'REVOKE ' + sql += privilege_type ?? 'ALL PRIVILEGES' + sql += ` (${ident(column_name)}) on ${ident(table_schema)}.${ident(table_name)}` + sql += ` to ${ident(role)}` + { + const { error } = await this.query(sql) + if (error) { + return { data: null, error } + } + } + return { data: 'OK', error: null } + } +} diff --git a/src/lib/PostgresMetaPermissions.ts b/src/lib/PostgresMetaPermissions.ts new file mode 100644 index 00000000..3109886b --- /dev/null +++ b/src/lib/PostgresMetaPermissions.ts @@ -0,0 +1,56 @@ +import { ident, literal } from 'pg-format' +import { PermissionListSchema } from './inputs.js' +import { permissionsSql } from './sql/index.js' +import { PostgresMetaResult, PostgresPermission } from './types.js' + +export default class PostgresMetaColumns { + query: (sql: string) => Promise> + + constructor(query: (sql: string) => Promise>) { + this.query = query + } + + async list({ + table_schema, + table_name, + column_name, + privilege, + include_system_schemas = false, + limit, + offset, + }: PermissionListSchema = {}): Promise> { + let sql = ` +WITH + permissions AS (${permissionsSql}) +SELECT + * +FROM + permissions +WHERE + true` + if (table_schema) { + sql += ` AND table_schema = ${literal(table_schema)}` + } + if (table_name) { + sql += ` AND table_name = ${literal(table_name)}` + } + if (column_name) { + sql += ` AND column_name = ${literal(column_name)}` + } + if (privilege) { + sql += ` AND privilege = ${literal(privilege)}` + } + if (!include_system_schemas) { + sql += ` AND table_schema NOT LIKE 'pg_%'` + sql += ` AND table_schema != 'information_schema'` + } + if (limit) { + sql += ` LIMIT ${limit}` + } + if (offset) { + sql += ` OFFSET ${offset}` + } + console.log(sql) + return await this.query(sql) + } +} diff --git a/src/lib/PostgresMetaTablePermissions.ts b/src/lib/PostgresMetaTablePermissions.ts new file mode 100644 index 00000000..9b4341a3 --- /dev/null +++ b/src/lib/PostgresMetaTablePermissions.ts @@ -0,0 +1,102 @@ +import { ident, literal } from 'pg-format' +import { tablePermissionsSql } from './sql/index.js' +import type { TablePermissionListSchema } from './inputs.js' +import type { PostgresMetaResult, PostgresTablePermission } from './types.js' + +export default class PostgresMetaColumns { + query: (sql: string) => Promise> + + constructor(query: (sql: string) => Promise>) { + this.query = query + } + + async list({ + table_schema, + table_name, + privilege, + include_system_schemas = false, + limit, + offset, + }: TablePermissionListSchema = {}): Promise> { + let sql = ` +WITH + table_permissions AS (${tablePermissionsSql}) +SELECT + * +FROM + table_permissions +WHERE + true` + if (table_schema) { + sql += ` AND table_schema = ${literal(table_schema)}` + } + if (table_name) { + sql += ` AND table_name = ${literal(table_name)}` + } + if (privilege) { + sql += ` AND privilege = ${literal(privilege)}` + } + if (!include_system_schemas) { + sql += ` AND table_schema NOT LIKE 'pg_%'` + sql += ` AND table_schema != 'information_schema'` + } + if (limit) { + sql += ` LIMIT ${limit}` + } + if (offset) { + sql += ` OFFSET ${offset}` + } + console.log(sql) + return await this.query(sql) + } + + async grant( + table_name: string, + { + table_schema, + privilege_type, + role, + }: { + table_schema: string + privilege_type?: 'SELECT' | 'INSERT' | 'UPDATE' + role: string + } + ): Promise> { + let sql = 'GRANT ' + sql += privilege_type ?? 'ALL PRIVILEGES' + sql += ` on ${ident(table_schema)}.${ident(table_name)}` + sql += ` to ${ident(role)}` + { + const { error } = await this.query(sql) + if (error) { + return { data: null, error } + } + } + return { data: 'OK', error: null } + } + + async revoke( + table_name: string, + { + table_schema, + privilege_type, + role, + }: { + table_schema: string + privilege_type?: 'SELECT' | 'INSERT' | 'UPDATE' + role: string + } + ): Promise> { + let sql = 'REVOKE ' + sql += privilege_type ?? 'ALL PRIVILEGES' + sql += ` on ${ident(table_schema)}.${ident(table_name)}` + sql += ` to ${ident(role)}` + { + const { error } = await this.query(sql) + if (error) { + return { data: null, error } + } + } + return { data: 'OK', error: null } + } +} diff --git a/src/lib/index.ts b/src/lib/index.ts index 1fe3fd5a..241a540c 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -3,11 +3,13 @@ export { PostgresMetaOk, PostgresMetaErr, PostgresMetaResult, + PostgresColumnPermission, PostgresColumn, PostgresConfig, PostgresExtension, PostgresFunction, PostgresFunctionCreate, + PostgresPermission, PostgresPolicy, PostgresPrimaryKey, PostgresPublication, @@ -16,6 +18,7 @@ export { PostgresSchema, PostgresSchemaCreate, PostgresSchemaUpdate, + PostgresTablePermission, PostgresTable, PostgresTrigger, PostgresType, diff --git a/src/lib/inputs.ts b/src/lib/inputs.ts new file mode 100644 index 00000000..2b54c0a9 --- /dev/null +++ b/src/lib/inputs.ts @@ -0,0 +1,56 @@ +// This file includes all route input schemas + +import { Static, Type } from '@sinclair/typebox' + +const limit = Type.Optional(Type.Integer()) +const offset = Type.Optional(Type.Integer()) + +export const columnPermissionListSchema = Type.Object({ + table_schema: Type.Optional(Type.String()), + table_name: Type.Optional(Type.String()), + column_name: Type.Optional(Type.String()), + privilege: Type.Optional( + Type.Union([Type.Literal('SELECT'), Type.Literal('INSERT'), Type.Literal('UPDATE')]) + ), + include_system_schemas: Type.Optional( + Type.Boolean({ + default: false, + }) + ), + limit, + offset, +}) +export type ColumnPermissionListSchema = Static + +export const permissionListSchema = Type.Object({ + table_schema: Type.Optional(Type.String()), + table_name: Type.Optional(Type.String()), + column_name: Type.Optional(Type.String()), + privilege: Type.Optional( + Type.Union([Type.Literal('SELECT'), Type.Literal('INSERT'), Type.Literal('UPDATE')]) + ), + include_system_schemas: Type.Optional( + Type.Boolean({ + default: false, + }) + ), + limit, + offset, +}) +export type PermissionListSchema = Static + +export const tablePermissionListSchema = Type.Object({ + table_schema: Type.Optional(Type.String()), + table_name: Type.Optional(Type.String()), + privilege: Type.Optional( + Type.Union([Type.Literal('SELECT'), Type.Literal('INSERT'), Type.Literal('UPDATE')]) + ), + include_system_schemas: Type.Optional( + Type.Boolean({ + default: false, + }) + ), + limit, + offset, +}) +export type TablePermissionListSchema = Static diff --git a/src/lib/sql/column_permissions.sql b/src/lib/sql/column_permissions.sql new file mode 100644 index 00000000..c941da9d --- /dev/null +++ b/src/lib/sql/column_permissions.sql @@ -0,0 +1,82 @@ +-- Adapted from information_schema.column_privileges view +-- Read all roles, not only those granted to login role of postgres-meta +-- Deleted database column +-- Deleted is_grantable column + +SELECT + nc.nspname as table_schema, + c.relname as table_name, + pr_a.attname as column_name, + pr_a.prtype as privilege, + u_grantor.rolname as grantor, + grantee.rolname as grantee +FROM + ( + SELECT + a.attrelid, + a.attname, + ( + aclexplode( + COALESCE( + a.attacl, + acldefault('c' :: "char", cc.relowner) + ) + ) + ).grantor AS grantor, + ( + aclexplode( + COALESCE( + a.attacl, + acldefault('c' :: "char", cc.relowner) + ) + ) + ).grantee AS grantee, + ( + aclexplode( + COALESCE( + a.attacl, + acldefault('c' :: "char", cc.relowner) + ) + ) + ).privilege_type AS privilege_type + FROM + pg_attribute a + JOIN pg_class cc ON a.attrelid = cc.oid + WHERE + a.attnum > 0 + AND NOT a.attisdropped + ) pr_a( + attrelid, attname, grantor, grantee, + prtype + ), + pg_class c, + pg_namespace nc, + pg_authid u_grantor, + ( + SELECT + pg_authid.oid, + pg_authid.rolname + FROM + pg_authid + UNION ALL + SELECT + 0 :: oid AS oid, + 'PUBLIC' :: name + ) grantee(oid, rolname) +WHERE + pr_a.attrelid = c.oid + AND c.relnamespace = nc.oid + AND pr_a.grantee = grantee.oid + AND pr_a.grantor = u_grantor.oid + AND ( + pr_a.prtype = ANY ( + ARRAY[ 'INSERT' :: text, 'SELECT' :: text, + 'UPDATE' :: text, 'REFERENCES' :: text] + ) + ) + AND ( + c.relkind = ANY ( + ARRAY[ 'r' :: "char", 'v' :: "char", 'f' :: "char", + 'p' :: "char" ] + ) + ) diff --git a/src/lib/sql/index.ts b/src/lib/sql/index.ts index d941ccc9..df5c00e0 100644 --- a/src/lib/sql/index.ts +++ b/src/lib/sql/index.ts @@ -3,17 +3,23 @@ import { dirname, join } from 'path' import { fileURLToPath } from 'url' const __dirname = dirname(fileURLToPath(import.meta.url)) +export const columnPermissionsSql = await readFile( + join(__dirname, 'column_permissions.sql'), + 'utf-8' +) export const columnsSql = await readFile(join(__dirname, 'columns.sql'), 'utf-8') export const configSql = await readFile(join(__dirname, 'config.sql'), 'utf-8') export const extensionsSql = await readFile(join(__dirname, 'extensions.sql'), 'utf-8') export const foreignTablesSql = await readFile(join(__dirname, 'foreign_tables.sql'), 'utf-8') export const functionsSql = await readFile(join(__dirname, 'functions.sql'), 'utf-8') +export const permissionsSql = await readFile(join(__dirname, 'permissions.sql'), 'utf-8') export const policiesSql = await readFile(join(__dirname, 'policies.sql'), 'utf-8') export const primaryKeysSql = await readFile(join(__dirname, 'primary_keys.sql'), 'utf-8') export const publicationsSql = await readFile(join(__dirname, 'publications.sql'), 'utf-8') export const relationshipsSql = await readFile(join(__dirname, 'relationships.sql'), 'utf-8') export const rolesSql = await readFile(join(__dirname, 'roles.sql'), 'utf-8') export const schemasSql = await readFile(join(__dirname, 'schemas.sql'), 'utf-8') +export const tablePermissionsSql = await readFile(join(__dirname, 'table_permissions.sql'), 'utf-8') export const tablesSql = await readFile(join(__dirname, 'tables.sql'), 'utf-8') export const triggersSql = await readFile(join(__dirname, 'triggers.sql'), 'utf-8') export const typesSql = await readFile(join(__dirname, 'types.sql'), 'utf-8') diff --git a/src/lib/sql/permissions.sql b/src/lib/sql/permissions.sql new file mode 100644 index 00000000..b0e1291c --- /dev/null +++ b/src/lib/sql/permissions.sql @@ -0,0 +1,152 @@ +-- Adapted from information_schema.column_privileges view +-- Read all roles, not only those granted to login role of postgres-meta +-- Deleted database column +-- Deleted is_grantable column + +SELECT + nc.nspname AS table_schema, + x.relname AS table_name, + x.attname AS column_name, + x.prtype AS privilege, + u_grantor.rolname AS grantor, + grantee.rolname AS grantee +FROM + ( + SELECT + pr_c.grantor, + pr_c.grantee, + a.attname, + pr_c.relname, + pr_c.relnamespace, + pr_c.prtype, + pr_c.grantable, + pr_c.relowner + FROM + ( + SELECT + pg_class.oid, + pg_class.relname, + pg_class.relnamespace, + pg_class.relowner, + ( + aclexplode( + COALESCE( + pg_class.relacl, + acldefault('r' :: "char", pg_class.relowner) + ) + ) + ).grantor AS grantor, + ( + aclexplode( + COALESCE( + pg_class.relacl, + acldefault('r' :: "char", pg_class.relowner) + ) + ) + ).grantee AS grantee, + ( + aclexplode( + COALESCE( + pg_class.relacl, + acldefault('r' :: "char", pg_class.relowner) + ) + ) + ).privilege_type AS privilege_type + FROM + pg_class + WHERE + pg_class.relkind = ANY ( + ARRAY[ 'r' :: "char", 'v' :: "char", 'f' :: "char", + 'p' :: "char" ] + ) + ) pr_c( + oid, relname, relnamespace, relowner, + grantor, grantee, prtype, grantable + ), + pg_attribute a + WHERE + a.attrelid = pr_c.oid + AND a.attnum > 0 + AND NOT a.attisdropped + UNION + SELECT + pr_a.grantor, + pr_a.grantee, + pr_a.attname, + c.relname, + c.relnamespace, + pr_a.prtype, + pr_a.grantable, + c.relowner + FROM + ( + SELECT + a.attrelid, + a.attname, + ( + aclexplode( + COALESCE( + a.attacl, + acldefault('c' :: "char", cc.relowner) + ) + ) + ).grantor AS grantor, + ( + aclexplode( + COALESCE( + a.attacl, + acldefault('c' :: "char", cc.relowner) + ) + ) + ).grantee AS grantee, + ( + aclexplode( + COALESCE( + a.attacl, + acldefault('c' :: "char", cc.relowner) + ) + ) + ).privilege_type AS privilege_type + FROM + pg_attribute a + JOIN pg_class cc ON a.attrelid = cc.oid + WHERE + a.attnum > 0 + AND NOT a.attisdropped + ) pr_a( + attrelid, attname, grantor, grantee, + prtype, grantable + ), + pg_class c + WHERE + pr_a.attrelid = c.oid + AND ( + c.relkind = ANY ( + ARRAY[ 'r' :: "char", 'v' :: "char", 'f' :: "char", + 'p' :: "char" ] + ) + ) + ) x, + pg_namespace nc, + pg_authid u_grantor, + ( + SELECT + pg_authid.oid, + pg_authid.rolname + FROM + pg_authid + UNION ALL + SELECT + 0 :: oid AS oid, + 'PUBLIC' :: name + ) grantee(oid, rolname) +WHERE + x.relnamespace = nc.oid + AND x.grantee = grantee.oid + AND x.grantor = u_grantor.oid + AND ( + x.prtype = ANY ( + ARRAY[ 'INSERT' :: text, 'SELECT' :: text, + 'UPDATE' :: text, 'REFERENCES' :: text] + ) + ) diff --git a/src/lib/sql/table_permissions.sql b/src/lib/sql/table_permissions.sql new file mode 100644 index 00000000..1efadec9 --- /dev/null +++ b/src/lib/sql/table_permissions.sql @@ -0,0 +1,76 @@ +-- Adapted from information_schema.column_privileges view +-- Read all roles, not only those granted to login role of postgres-meta +-- Deleted database column +-- Deleted is_grantable column + +SELECT + nc.nspname as table_schema, + pr_c.relname as table_name, + pr_c.prtype as privilege, + u_grantor.rolname as grantor, + grantee.rolname as grantee +FROM + ( + SELECT + pg_class.oid, + pg_class.relname, + pg_class.relnamespace, + pg_class.relowner, + ( + aclexplode( + COALESCE( + pg_class.relacl, + acldefault('r' :: "char", pg_class.relowner) + ) + ) + ).grantor AS grantor, + ( + aclexplode( + COALESCE( + pg_class.relacl, + acldefault('r' :: "char", pg_class.relowner) + ) + ) + ).grantee AS grantee, + ( + aclexplode( + COALESCE( + pg_class.relacl, + acldefault('r' :: "char", pg_class.relowner) + ) + ) + ).privilege_type AS privilege_type + FROM + pg_class + WHERE + pg_class.relkind = ANY ( + ARRAY[ 'r' :: "char", 'v' :: "char", 'f' :: "char", + 'p' :: "char" ] + ) + ) pr_c( + oid, relname, relnamespace, relowner, + grantor, grantee, prtype + ), + pg_namespace nc, + pg_authid u_grantor, + ( + SELECT + pg_authid.oid, + pg_authid.rolname + FROM + pg_authid + UNION ALL + SELECT + 0 :: oid AS oid, + 'PUBLIC' :: name + ) grantee(oid, rolname) +WHERE + pr_c.relnamespace = nc.oid + AND pr_c.grantee = grantee.oid + AND pr_c.grantor = u_grantor.oid + AND ( + pr_c.prtype = ANY ( + ARRAY[ 'INSERT' :: text, 'SELECT' :: text, + 'UPDATE' :: text, 'REFERENCES' :: text] + ) + ) \ No newline at end of file diff --git a/src/lib/types.ts b/src/lib/types.ts index 599837fd..7886bbf1 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -17,6 +17,16 @@ export interface PostgresMetaErr { export type PostgresMetaResult = PostgresMetaOk | PostgresMetaErr +export const postgresColumnPermissionSchema = Type.Object({ + table_schema: Type.String(), + table_name: Type.String(), + column_name: Type.String(), + privilege: Type.Union([Type.Literal('SELECT'), Type.Literal('INSERT'), Type.Literal('UPDATE')]), + grantor: Type.String(), + grantee: Type.String(), +}) +export type PostgresColumnPermission = Static + export const postgresColumnSchema = Type.Object({ table_id: Type.Integer(), schema: Type.String(), @@ -135,6 +145,16 @@ export const postgresFunctionCreateFunction = Type.Object({ }) export type PostgresFunctionCreate = Static +export const postgresPermissionSchema = Type.Object({ + table_schema: Type.String(), + table_name: Type.String(), + column_name: Type.String(), + privilege: Type.Union([Type.Literal('SELECT'), Type.Literal('INSERT'), Type.Literal('UPDATE')]), + grantor: Type.String(), + grantee: Type.String(), +}) +export type PostgresPermission = Static + export const postgresPolicySchema = Type.Object({ id: Type.Integer(), schema: Type.String(), @@ -275,6 +295,20 @@ export const postgresSchemaUpdateSchema = Type.Object({ }) export type PostgresSchemaUpdate = Static +export const postgresTablePermissionSchema = Type.Object({ + table_schema: Type.String(), + table_name: Type.String(), + column_name: Type.String(), + privilege_type: Type.Union([ + Type.Literal('SELECT'), + Type.Literal('INSERT'), + Type.Literal('UPDATE'), + ]), + grantor: Type.String(), + grantee: Type.String(), +}) +export type PostgresTablePermission = Static + export const postgresTableSchema = Type.Object({ id: Type.Integer(), schema: Type.String(), diff --git a/src/server/routes/column-permissions.ts b/src/server/routes/column-permissions.ts new file mode 100644 index 00000000..987fc565 --- /dev/null +++ b/src/server/routes/column-permissions.ts @@ -0,0 +1,137 @@ +import { FastifyPluginAsyncTypebox } from '@fastify/type-provider-typebox' +import { Type } from '@sinclair/typebox' +import { PostgresMeta } from '../../lib/index.js' +import { postgresColumnPermissionSchema } from '../../lib/types.js' +import { DEFAULT_POOL_CONFIG } from '../constants.js' +import { extractRequestForLogging } from '../utils.js' +import { columnPermissionListSchema } from '../../lib/inputs.js' + +const route: FastifyPluginAsyncTypebox = async (fastify) => { + fastify.get( + '/', + { + schema: { + headers: Type.Object({ pg: Type.String() }), + querystring: columnPermissionListSchema, + response: { + 200: Type.Array(postgresColumnPermissionSchema), + 500: Type.Object({ + error: Type.String(), + }), + }, + }, + }, + async (request, reply) => { + const connectionString = request.headers.pg + const { table_schema, table_name, column_name, privilege, limit, offset } = request.query + + const pgMeta = new PostgresMeta({ ...DEFAULT_POOL_CONFIG, connectionString }) + const { data, error } = await pgMeta.columnPermissions.list({ + table_schema, + table_name, + column_name, + privilege, + limit, + offset, + }) + await pgMeta.end() + if (error) { + request.log.error({ error, request: extractRequestForLogging(request) }) + reply.code(500) + return { error: error.message } + } + + return data + } + ) + + fastify.post( + '/:column_name', + { + schema: { + headers: Type.Object({ pg: Type.String() }), + params: Type.Object({ + column_name: Type.String(), + }), + body: Type.Object({ + table_schema: Type.String(), + table_name: Type.String(), + role: Type.String(), + privilege: Type.Optional( + Type.Union([Type.Literal('SELECT'), Type.Literal('INSERT'), Type.Literal('UPDATE')]) + ), + }), + response: { + 200: Type.Literal('OK'), + 404: Type.Object({ + error: Type.String(), + }), + }, + }, + }, + async (request, reply) => { + const connectionString = request.headers.pg + + const pgMeta = new PostgresMeta({ ...DEFAULT_POOL_CONFIG, connectionString }) + const { data, error } = await pgMeta.columnPermissions.grant( + request.params.column_name, + request.body + ) + await pgMeta.end() + if (error) { + request.log.error({ error, request: extractRequestForLogging(request) }) + reply.code(400) + if (error.message.startsWith('Cannot find')) reply.code(404) + return { error: error.message } + } + + return data + } + ) + + fastify.delete( + '/:column_name', + { + schema: { + headers: Type.Object({ pg: Type.String() }), + params: Type.Object({ + column_name: Type.String(), + }), + body: Type.Object({ + table_schema: Type.String(), + table_name: Type.String(), + role: Type.String(), + privilege: Type.Optional( + Type.Union([Type.Literal('SELECT'), Type.Literal('INSERT'), Type.Literal('UPDATE')]) + ), + }), + response: { + 200: Type.Literal('OK'), + 404: Type.Object({ + error: Type.String(), + }), + }, + }, + }, + async (request, reply) => { + const connectionString = request.headers.pg + + const pgMeta = new PostgresMeta({ ...DEFAULT_POOL_CONFIG, connectionString }) + const { data, error } = await pgMeta.columnPermissions.revoke( + request.params.column_name, + request.body + ) + await pgMeta.end() + if (error) { + request.log.error({ error, request: extractRequestForLogging(request) }) + reply.code(400) + if (error.message.startsWith('Cannot find')) reply.code(404) + return { error: error.message } + } + + return data + } + ) +} + +export default route diff --git a/src/server/routes/index.ts b/src/server/routes/index.ts index 0c01269e..69979f43 100644 --- a/src/server/routes/index.ts +++ b/src/server/routes/index.ts @@ -1,5 +1,6 @@ import CryptoJS from 'crypto-js' import { FastifyInstance } from 'fastify' +import ColumnPermissionsRoute from './column-permissions.js' import ColumnRoute from './columns.js' import ConfigRoute from './config.js' import ExtensionsRoute from './extensions.js' @@ -40,6 +41,7 @@ export default async (fastify: FastifyInstance) => { done() }) + fastify.register(ColumnPermissionsRoute, { prefix: '/column-permissions' }) fastify.register(ColumnRoute, { prefix: '/columns' }) fastify.register(ConfigRoute, { prefix: '/config' }) fastify.register(ExtensionsRoute, { prefix: '/extensions' }) diff --git a/src/server/routes/permission.ts b/src/server/routes/permission.ts new file mode 100644 index 00000000..16177687 --- /dev/null +++ b/src/server/routes/permission.ts @@ -0,0 +1,49 @@ +import { FastifyPluginAsyncTypebox } from '@fastify/type-provider-typebox' +import { Type } from '@sinclair/typebox' +import { PostgresMeta } from '../../lib/index.js' +import { postgresPermissionSchema } from '../../lib/types.js' +import { DEFAULT_POOL_CONFIG } from '../constants.js' +import { extractRequestForLogging } from '../utils.js' +import { permissionListSchema } from '../../lib/inputs.js' + +const route: FastifyPluginAsyncTypebox = async (fastify) => { + fastify.get( + '/', + { + schema: { + headers: Type.Object({ pg: Type.String() }), + querystring: permissionListSchema, + response: { + 200: Type.Array(postgresPermissionSchema), + 500: Type.Object({ + error: Type.String(), + }), + }, + }, + }, + async (request, reply) => { + const connectionString = request.headers.pg + const { table_schema, table_name, column_name, privilege, limit, offset } = request.query + + const pgMeta = new PostgresMeta({ ...DEFAULT_POOL_CONFIG, connectionString }) + const { data, error } = await pgMeta.permissions.list({ + table_schema, + table_name, + column_name, + privilege, + limit, + offset, + }) + await pgMeta.end() + if (error) { + request.log.error({ error, request: extractRequestForLogging(request) }) + reply.code(500) + return { error: error.message } + } + + return data + } + ) +} + +export default route diff --git a/src/server/routes/table-permission.ts b/src/server/routes/table-permission.ts new file mode 100644 index 00000000..1edfcffa --- /dev/null +++ b/src/server/routes/table-permission.ts @@ -0,0 +1,136 @@ +import { FastifyPluginAsyncTypebox } from '@fastify/type-provider-typebox' +import { Type } from '@sinclair/typebox' +import { PostgresMeta } from '../../lib/index.js' +import { postgresTablePermissionSchema } from '../../lib/types.js' +import { DEFAULT_POOL_CONFIG } from '../constants.js' +import { extractRequestForLogging } from '../utils.js' +import { tablePermissionListSchema } from '../../lib/inputs.js' + +const route: FastifyPluginAsyncTypebox = async (fastify) => { + fastify.get( + '/', + { + schema: { + headers: Type.Object({ pg: Type.String() }), + querystring: tablePermissionListSchema, + response: { + 200: Type.Array(postgresTablePermissionSchema), + 500: Type.Object({ + error: Type.String(), + }), + }, + }, + }, + async (request, reply) => { + const connectionString = request.headers.pg + const { table_schema, table_name, privilege, limit, offset } = request.query + + const pgMeta = new PostgresMeta({ ...DEFAULT_POOL_CONFIG, connectionString }) + const { data, error } = await pgMeta.tablePermissions.list({ + table_schema, + table_name, + privilege, + limit, + offset, + }) + await pgMeta.end() + if (error) { + request.log.error({ error, request: extractRequestForLogging(request) }) + reply.code(500) + return { error: error.message } + } + + return data + } + ) + + fastify.post( + '/:table_name', + { + schema: { + headers: Type.Object({ pg: Type.String() }), + params: Type.Object({ + table_name: Type.String(), + }), + body: Type.Object({ + table_schema: Type.String(), + table_name: Type.String(), + role: Type.String(), + privilege: Type.Optional( + Type.Union([Type.Literal('SELECT'), Type.Literal('INSERT'), Type.Literal('UPDATE')]) + ), + }), + response: { + 200: Type.Literal('OK'), + 404: Type.Object({ + error: Type.String(), + }), + }, + }, + }, + async (request, reply) => { + const connectionString = request.headers.pg + + const pgMeta = new PostgresMeta({ ...DEFAULT_POOL_CONFIG, connectionString }) + const { data, error } = await pgMeta.tablePermissions.grant( + request.params.table_name, + request.body + ) + await pgMeta.end() + if (error) { + request.log.error({ error, request: extractRequestForLogging(request) }) + reply.code(400) + if (error.message.startsWith('Cannot find')) reply.code(404) + return { error: error.message } + } + + return data + } + ) + + fastify.delete( + '/:table_name', + { + schema: { + headers: Type.Object({ pg: Type.String() }), + params: Type.Object({ + table_name: Type.String(), + }), + body: Type.Object({ + table_schema: Type.String(), + table_name: Type.String(), + role: Type.String(), + privilege: Type.Optional( + Type.Union([Type.Literal('SELECT'), Type.Literal('INSERT'), Type.Literal('UPDATE')]) + ), + }), + response: { + 200: Type.Literal('OK'), + 404: Type.Object({ + error: Type.String(), + }), + }, + }, + }, + async (request, reply) => { + const connectionString = request.headers.pg + + const pgMeta = new PostgresMeta({ ...DEFAULT_POOL_CONFIG, connectionString }) + const { data, error } = await pgMeta.tablePermissions.revoke( + request.params.table_name, + request.body + ) + await pgMeta.end() + if (error) { + request.log.error({ error, request: extractRequestForLogging(request) }) + reply.code(400) + if (error.message.startsWith('Cannot find')) reply.code(404) + return { error: error.message } + } + + return data + } + ) +} + +export default route