pg-meta-policies.ts178 lines · main
| 1 | import { z } from 'zod' |
| 2 | |
| 3 | import { DEFAULT_SYSTEM_SCHEMAS } from './constants' |
| 4 | import { filterByList } from './helpers' |
| 5 | import { |
| 6 | ident, |
| 7 | joinSqlFragments, |
| 8 | keyword, |
| 9 | literal, |
| 10 | safeSql, |
| 11 | type SafeSqlFragment, |
| 12 | } from './pg-format' |
| 13 | import { POLICIES_SQL } from './sql/policies' |
| 14 | |
| 15 | const pgPolicyZod = z.object({ |
| 16 | id: z.number(), |
| 17 | schema: z.string(), |
| 18 | table: z.string(), |
| 19 | table_id: z.number(), |
| 20 | name: z.string(), |
| 21 | action: z.union([z.literal('PERMISSIVE'), z.literal('RESTRICTIVE')]), |
| 22 | roles: z.array(z.string()), |
| 23 | command: z.union([ |
| 24 | z.literal('SELECT'), |
| 25 | z.literal('INSERT'), |
| 26 | z.literal('UPDATE'), |
| 27 | z.literal('DELETE'), |
| 28 | z.literal('ALL'), |
| 29 | ]), |
| 30 | definition: z.union([z.string(), z.null()]), |
| 31 | check: z.union([z.string(), z.null()]), |
| 32 | }) |
| 33 | const pgPolicyArrayZod = z.array(pgPolicyZod) |
| 34 | const pgPolicyOptionalZod = z.optional(pgPolicyZod) |
| 35 | |
| 36 | export type PGPolicy = z.infer<typeof pgPolicyZod> |
| 37 | |
| 38 | type PolicyIdentifier = Pick<PGPolicy, 'id'> | Pick<PGPolicy, 'name' | 'schema' | 'table'> |
| 39 | |
| 40 | function getIdentifierWhereClause(identifier: PolicyIdentifier): SafeSqlFragment { |
| 41 | if ('id' in identifier && identifier.id) { |
| 42 | return safeSql`id = ${literal(identifier.id)}` |
| 43 | } else if ('name' in identifier && identifier.name && identifier.schema && identifier.table) { |
| 44 | return safeSql`name = ${literal(identifier.name)} AND schema = ${literal(identifier.schema)} AND table = ${literal(identifier.table)}` |
| 45 | } |
| 46 | throw new Error('Must provide either id or name, schema and table') |
| 47 | } |
| 48 | |
| 49 | function list({ |
| 50 | includeSystemSchemas = false, |
| 51 | includedSchemas, |
| 52 | excludedSchemas, |
| 53 | limit, |
| 54 | offset, |
| 55 | }: { |
| 56 | includeSystemSchemas?: boolean |
| 57 | includedSchemas?: string[] |
| 58 | excludedSchemas?: string[] |
| 59 | limit?: number |
| 60 | offset?: number |
| 61 | } = {}): { |
| 62 | sql: SafeSqlFragment |
| 63 | zod: typeof pgPolicyArrayZod |
| 64 | } { |
| 65 | let sql = safeSql` |
| 66 | with policies as (${POLICIES_SQL}) |
| 67 | select * |
| 68 | from policies |
| 69 | ` |
| 70 | const filter = filterByList( |
| 71 | includedSchemas, |
| 72 | excludedSchemas, |
| 73 | !includeSystemSchemas ? DEFAULT_SYSTEM_SCHEMAS : undefined |
| 74 | ) |
| 75 | if (filter) { |
| 76 | sql = safeSql`${sql}where schema ${filter}` |
| 77 | } |
| 78 | if (limit) { |
| 79 | sql = safeSql`${sql} limit ${literal(limit)}` |
| 80 | } |
| 81 | if (offset) { |
| 82 | sql = safeSql`${sql} offset ${literal(offset)}` |
| 83 | } |
| 84 | return { |
| 85 | sql, |
| 86 | zod: pgPolicyArrayZod, |
| 87 | } |
| 88 | } |
| 89 | |
| 90 | function retrieve(identifier: PolicyIdentifier): { |
| 91 | sql: SafeSqlFragment |
| 92 | zod: typeof pgPolicyOptionalZod |
| 93 | } { |
| 94 | const sql = safeSql`with policies as (${POLICIES_SQL}) select * from policies where ${getIdentifierWhereClause(identifier)};` |
| 95 | return { |
| 96 | sql, |
| 97 | zod: pgPolicyOptionalZod, |
| 98 | } |
| 99 | } |
| 100 | |
| 101 | type PolicyCreateParams = { |
| 102 | name: string |
| 103 | schema?: string |
| 104 | table: string |
| 105 | definition?: SafeSqlFragment |
| 106 | check?: SafeSqlFragment |
| 107 | action?: 'PERMISSIVE' | 'RESTRICTIVE' |
| 108 | command?: 'ALL' | 'SELECT' | 'INSERT' | 'UPDATE' | 'DELETE' |
| 109 | roles?: string[] |
| 110 | } |
| 111 | |
| 112 | function create({ |
| 113 | name, |
| 114 | schema = 'public', |
| 115 | table, |
| 116 | definition, |
| 117 | check, |
| 118 | action = 'PERMISSIVE', |
| 119 | command = 'ALL', |
| 120 | roles = ['public'], |
| 121 | }: PolicyCreateParams): { sql: SafeSqlFragment } { |
| 122 | const rolesFragment = joinSqlFragments(roles.map(ident), ', ') |
| 123 | const definitionSql = definition ? safeSql`using (${definition})` : safeSql`` |
| 124 | const checkSql = check ? safeSql`with check (${check})` : safeSql`` |
| 125 | const sql = safeSql` |
| 126 | create policy ${ident(name)} on ${ident(schema)}.${ident(table)} |
| 127 | as ${keyword(action)} |
| 128 | for ${keyword(command)} |
| 129 | to ${rolesFragment} |
| 130 | ${definitionSql} |
| 131 | ${checkSql};` |
| 132 | return { sql } |
| 133 | } |
| 134 | |
| 135 | type PolicyUpdateParams = { |
| 136 | name?: string |
| 137 | definition?: SafeSqlFragment |
| 138 | check?: SafeSqlFragment |
| 139 | roles?: string[] |
| 140 | } |
| 141 | |
| 142 | function update( |
| 143 | identifier: Pick<PGPolicy, 'name' | 'schema' | 'table'>, |
| 144 | params: PolicyUpdateParams |
| 145 | ): { sql: SafeSqlFragment } { |
| 146 | const { name, definition, check, roles } = params |
| 147 | |
| 148 | const alter = safeSql`ALTER POLICY ${ident(identifier.name)} ON ${ident(identifier.schema)}.${ident(identifier.table)}` |
| 149 | const nameSql: SafeSqlFragment = |
| 150 | name === undefined ? safeSql`` : safeSql`${alter} RENAME TO ${ident(name)};` |
| 151 | const definitionSql: SafeSqlFragment = |
| 152 | definition === undefined ? safeSql`` : safeSql`${alter} USING (${definition});` |
| 153 | const checkSql: SafeSqlFragment = |
| 154 | check === undefined ? safeSql`` : safeSql`${alter} WITH CHECK (${check});` |
| 155 | const rolesSql: SafeSqlFragment = |
| 156 | roles === undefined |
| 157 | ? safeSql`` |
| 158 | : safeSql`${alter} TO ${joinSqlFragments(roles.map(ident), ', ')};` |
| 159 | |
| 160 | // nameSql must be last |
| 161 | const sql = safeSql`BEGIN; ${definitionSql} ${checkSql} ${rolesSql} ${nameSql} COMMIT;` |
| 162 | |
| 163 | return { sql } |
| 164 | } |
| 165 | |
| 166 | function remove(identifier: Pick<PGPolicy, 'name' | 'schema' | 'table'>): { sql: SafeSqlFragment } { |
| 167 | const sql = safeSql`DROP POLICY ${ident(identifier.name)} ON ${ident(identifier.schema)}.${ident(identifier.table)};` |
| 168 | return { sql } |
| 169 | } |
| 170 | |
| 171 | export default { |
| 172 | list, |
| 173 | retrieve, |
| 174 | create, |
| 175 | update, |
| 176 | remove, |
| 177 | zod: pgPolicyZod, |
| 178 | } |