@kysera/rls 0.8.7 → 0.8.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +11 -4
- package/dist/index.js +11 -12
- package/dist/index.js.map +1 -1
- package/dist/native/index.d.ts +1 -1
- package/dist/native/index.js +77 -8
- package/dist/native/index.js.map +1 -1
- package/dist/{types-CyqksFKU.d.ts → types-D3hQINlj.d.ts} +11 -1
- package/package.json +15 -15
- package/src/native/postgres.ts +60 -10
- package/src/plugin.ts +19 -12
- package/src/policy/types.ts +12 -1
- package/src/transformer/mutation.ts +3 -4
package/dist/native/index.d.ts
CHANGED
package/dist/native/index.js
CHANGED
|
@@ -1,6 +1,63 @@
|
|
|
1
1
|
import { sql } from 'kysely';
|
|
2
|
+
import { DatabaseError } from '@kysera/core';
|
|
2
3
|
|
|
3
4
|
// src/native/postgres.ts
|
|
5
|
+
var RLSErrorCodes = {
|
|
6
|
+
/** RLS schema definition is invalid */
|
|
7
|
+
RLS_SCHEMA_INVALID: "RLS_SCHEMA_INVALID"};
|
|
8
|
+
var RLSError = class extends DatabaseError {
|
|
9
|
+
/**
|
|
10
|
+
* Creates a new RLS error
|
|
11
|
+
*
|
|
12
|
+
* @param message - Error message
|
|
13
|
+
* @param code - RLS error code
|
|
14
|
+
*/
|
|
15
|
+
constructor(message, code) {
|
|
16
|
+
super(message, code);
|
|
17
|
+
this.name = "RLSError";
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
var RLSSchemaError = class extends RLSError {
|
|
21
|
+
details;
|
|
22
|
+
/**
|
|
23
|
+
* Creates a new schema validation error
|
|
24
|
+
*
|
|
25
|
+
* @param message - Error message
|
|
26
|
+
* @param details - Additional details about the validation failure
|
|
27
|
+
*/
|
|
28
|
+
constructor(message, details = {}) {
|
|
29
|
+
super(message, RLSErrorCodes.RLS_SCHEMA_INVALID);
|
|
30
|
+
this.name = "RLSSchemaError";
|
|
31
|
+
this.details = details;
|
|
32
|
+
}
|
|
33
|
+
toJSON() {
|
|
34
|
+
return {
|
|
35
|
+
...super.toJSON(),
|
|
36
|
+
details: this.details
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// src/native/postgres.ts
|
|
42
|
+
var SAFE_IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
43
|
+
function quoteIdent(name, context) {
|
|
44
|
+
if (!SAFE_IDENTIFIER.test(name)) {
|
|
45
|
+
throw new RLSSchemaError(
|
|
46
|
+
`Unsafe ${context}: "${name}". Only alphanumeric and underscore characters are allowed.`,
|
|
47
|
+
{ identifier: name, context }
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
return '"' + name.replace(/"/g, '""') + '"';
|
|
51
|
+
}
|
|
52
|
+
function quoteLiteral(value, context) {
|
|
53
|
+
if (!SAFE_IDENTIFIER.test(value)) {
|
|
54
|
+
throw new RLSSchemaError(
|
|
55
|
+
`Unsafe ${context}: "${value}". Only alphanumeric and underscore characters are allowed.`,
|
|
56
|
+
{ identifier: value, context }
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
return "'" + value.replace(/'/g, "''") + "'";
|
|
60
|
+
}
|
|
4
61
|
var PostgresRLSGenerator = class {
|
|
5
62
|
/**
|
|
6
63
|
* Generate all PostgreSQL RLS statements from schema
|
|
@@ -8,9 +65,11 @@ var PostgresRLSGenerator = class {
|
|
|
8
65
|
generateStatements(schema, options = {}) {
|
|
9
66
|
const { force = true, schemaName = "public", policyPrefix = "rls" } = options;
|
|
10
67
|
const statements = [];
|
|
68
|
+
const quotedSchema = quoteIdent(schemaName, "schema name");
|
|
11
69
|
for (const [table, config] of Object.entries(schema)) {
|
|
12
70
|
if (!config) continue;
|
|
13
|
-
const
|
|
71
|
+
const quotedTable = quoteIdent(table, "table name");
|
|
72
|
+
const qualifiedTable = `${quotedSchema}.${quotedTable}`;
|
|
14
73
|
const tableConfig = config;
|
|
15
74
|
statements.push(`ALTER TABLE ${qualifiedTable} ENABLE ROW LEVEL SECURITY;`);
|
|
16
75
|
if (force) {
|
|
@@ -18,8 +77,8 @@ var PostgresRLSGenerator = class {
|
|
|
18
77
|
}
|
|
19
78
|
let policyIndex = 0;
|
|
20
79
|
for (const policy of tableConfig.policies) {
|
|
21
|
-
const
|
|
22
|
-
const policySQL = this.generatePolicy(qualifiedTable,
|
|
80
|
+
const rawPolicyName = policy.name ?? `${policyPrefix}_${table}_${policy.type}_${policyIndex++}`;
|
|
81
|
+
const policySQL = this.generatePolicy(qualifiedTable, rawPolicyName, policy);
|
|
23
82
|
if (policySQL) {
|
|
24
83
|
statements.push(policySQL);
|
|
25
84
|
}
|
|
@@ -37,13 +96,15 @@ var PostgresRLSGenerator = class {
|
|
|
37
96
|
if (!policy.using && !policy.withCheck) {
|
|
38
97
|
return null;
|
|
39
98
|
}
|
|
40
|
-
const
|
|
99
|
+
const quotedName = quoteIdent(name, "policy name");
|
|
100
|
+
const parts = [`CREATE POLICY ${quotedName}`, `ON ${table}`];
|
|
41
101
|
if (policy.type === "deny") {
|
|
42
102
|
parts.push("AS RESTRICTIVE");
|
|
43
103
|
} else {
|
|
44
104
|
parts.push("AS PERMISSIVE");
|
|
45
105
|
}
|
|
46
|
-
|
|
106
|
+
const role = policy.role ?? "public";
|
|
107
|
+
parts.push(`TO ${quoteIdent(role, "role name")}`);
|
|
47
108
|
parts.push(`FOR ${this.mapOperation(policy.operation)}`);
|
|
48
109
|
if (policy.using) {
|
|
49
110
|
parts.push(`USING (${policy.using})`);
|
|
@@ -138,15 +199,23 @@ AS $$ SELECT COALESCE(current_setting('app.is_system', true), 'false')::boolean
|
|
|
138
199
|
generateDropStatements(schema, options = {}) {
|
|
139
200
|
const { schemaName = "public", policyPrefix = "rls" } = options;
|
|
140
201
|
const statements = [];
|
|
202
|
+
const quotedSchema = quoteIdent(schemaName, "schema name");
|
|
203
|
+
if (!SAFE_IDENTIFIER.test(policyPrefix)) {
|
|
204
|
+
throw new RLSSchemaError(
|
|
205
|
+
`Unsafe policy prefix: "${policyPrefix}". Only alphanumeric and underscore characters are allowed.`,
|
|
206
|
+
{ identifier: policyPrefix, context: "policy prefix" }
|
|
207
|
+
);
|
|
208
|
+
}
|
|
141
209
|
for (const table of Object.keys(schema)) {
|
|
142
|
-
const
|
|
210
|
+
const quotedTable = quoteIdent(table, "table name");
|
|
211
|
+
const qualifiedTable = `${quotedSchema}.${quotedTable}`;
|
|
143
212
|
statements.push(
|
|
144
213
|
`DO $$ BEGIN
|
|
145
214
|
EXECUTE (
|
|
146
215
|
SELECT string_agg('DROP POLICY IF EXISTS ' || quote_ident(policyname) || ' ON ${qualifiedTable};', E'\\n')
|
|
147
216
|
FROM pg_policies
|
|
148
|
-
WHERE tablename =
|
|
149
|
-
AND schemaname =
|
|
217
|
+
WHERE tablename = ${quoteLiteral(table, "table name")}
|
|
218
|
+
AND schemaname = ${quoteLiteral(schemaName, "schema name")}
|
|
150
219
|
AND policyname LIKE '${policyPrefix}_%'
|
|
151
220
|
);
|
|
152
221
|
END $$;`
|
package/dist/native/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/native/postgres.ts","../../src/native/migration.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { Kysely } from 'kysely'\nimport { sql } from 'kysely'\nimport type { RLSSchema, TableRLSConfig, PolicyDefinition, Operation } from '../policy/types.js'\n\n/**\n * Options for PostgreSQL RLS generation\n */\nexport interface PostgresRLSOptions {\n /** Force RLS on table owners */\n force?: boolean\n /** Schema name (default: public) */\n schemaName?: string\n /** Prefix for generated policy names */\n policyPrefix?: string\n}\n\n/**\n * PostgreSQL RLS Generator\n * Generates native PostgreSQL RLS statements from Kysera RLS schema\n */\nexport class PostgresRLSGenerator {\n /**\n * Generate all PostgreSQL RLS statements from schema\n */\n generateStatements<DB>(schema: RLSSchema<DB>, options: PostgresRLSOptions = {}): string[] {\n const { force = true, schemaName = 'public', policyPrefix = 'rls' } = options\n\n const statements: string[] = []\n\n for (const [table, config] of Object.entries(schema)) {\n if (!config) continue\n\n const qualifiedTable = `${schemaName}.${table}`\n const tableConfig = config as TableRLSConfig\n\n // Enable RLS on table\n statements.push(`ALTER TABLE ${qualifiedTable} ENABLE ROW LEVEL SECURITY;`)\n\n if (force) {\n statements.push(`ALTER TABLE ${qualifiedTable} FORCE ROW LEVEL SECURITY;`)\n }\n\n // Generate policies\n let policyIndex = 0\n for (const policy of tableConfig.policies) {\n const policyName = policy.name ?? `${policyPrefix}_${table}_${policy.type}_${policyIndex++}`\n const policySQL = this.generatePolicy(qualifiedTable, policyName, policy)\n if (policySQL) {\n statements.push(policySQL)\n }\n }\n }\n\n return statements\n }\n\n /**\n * Generate a single policy statement\n */\n private generatePolicy(table: string, name: string, policy: PolicyDefinition): string | null {\n // Skip filter policies (they're ORM-only)\n if (policy.type === 'filter' || policy.type === 'validate') {\n return null\n }\n\n // Need USING or WITH CHECK clause for native RLS\n if (!policy.using && !policy.withCheck) {\n return null\n }\n\n const parts: string[] = [`CREATE POLICY \"${name}\"`, `ON ${table}`]\n\n // Policy type\n if (policy.type === 'deny') {\n parts.push('AS RESTRICTIVE')\n } else {\n parts.push('AS PERMISSIVE')\n }\n\n // Target role\n parts.push(`TO ${policy.role ?? 'public'}`)\n\n // Operation\n parts.push(`FOR ${this.mapOperation(policy.operation)}`)\n\n // USING clause\n if (policy.using) {\n parts.push(`USING (${policy.using})`)\n }\n\n // WITH CHECK clause\n if (policy.withCheck) {\n parts.push(`WITH CHECK (${policy.withCheck})`)\n }\n\n return parts.join('\\n ') + ';'\n }\n\n /**\n * Map Kysera operation to PostgreSQL operation\n */\n private mapOperation(operation: Operation | Operation[]): string {\n if (Array.isArray(operation)) {\n if (operation.length === 0) {\n return 'ALL'\n }\n if (operation.length === 4 || operation.includes('all')) {\n return 'ALL'\n }\n // PostgreSQL doesn't support multiple operations in one policy\n // Return first operation\n return this.mapSingleOperation(operation[0]!)\n }\n return this.mapSingleOperation(operation)\n }\n\n /**\n * Map single operation\n */\n private mapSingleOperation(op: Operation): string {\n switch (op) {\n case 'read':\n return 'SELECT'\n case 'create':\n return 'INSERT'\n case 'update':\n return 'UPDATE'\n case 'delete':\n return 'DELETE'\n case 'all':\n return 'ALL'\n default:\n return 'ALL'\n }\n }\n\n /**\n * Generate context-setting functions for PostgreSQL\n * These functions should be STABLE for optimal performance\n */\n generateContextFunctions(): string {\n return `\n-- RLS Context Functions (STABLE for query planner optimization)\n-- These functions read session variables set by the application\n\nCREATE OR REPLACE FUNCTION rls_current_user_id()\nRETURNS text\nLANGUAGE SQL STABLE\nAS $$ SELECT current_setting('app.user_id', true) $$;\n\nCREATE OR REPLACE FUNCTION rls_current_tenant_id()\nRETURNS uuid\nLANGUAGE SQL STABLE\nAS $$ SELECT NULLIF(current_setting('app.tenant_id', true), '')::uuid $$;\n\nCREATE OR REPLACE FUNCTION rls_current_roles()\nRETURNS text[]\nLANGUAGE SQL STABLE\nAS $$ SELECT string_to_array(COALESCE(current_setting('app.roles', true), ''), ',') $$;\n\nCREATE OR REPLACE FUNCTION rls_has_role(role_name text)\nRETURNS boolean\nLANGUAGE SQL STABLE\nAS $$ SELECT role_name = ANY(rls_current_roles()) $$;\n\nCREATE OR REPLACE FUNCTION rls_current_permissions()\nRETURNS text[]\nLANGUAGE SQL STABLE\nAS $$ SELECT string_to_array(COALESCE(current_setting('app.permissions', true), ''), ',') $$;\n\nCREATE OR REPLACE FUNCTION rls_has_permission(permission_name text)\nRETURNS boolean\nLANGUAGE SQL STABLE\nAS $$ SELECT permission_name = ANY(rls_current_permissions()) $$;\n\nCREATE OR REPLACE FUNCTION rls_is_system()\nRETURNS boolean\nLANGUAGE SQL STABLE\nAS $$ SELECT COALESCE(current_setting('app.is_system', true), 'false')::boolean $$;\n`\n }\n\n /**\n * Generate DROP statements for cleaning up\n */\n generateDropStatements<DB>(schema: RLSSchema<DB>, options: PostgresRLSOptions = {}): string[] {\n const { schemaName = 'public', policyPrefix = 'rls' } = options\n const statements: string[] = []\n\n for (const table of Object.keys(schema)) {\n const qualifiedTable = `${schemaName}.${table}`\n\n // Drop all policies with prefix\n statements.push(\n `DO $$ BEGIN\n EXECUTE (\n SELECT string_agg('DROP POLICY IF EXISTS ' || quote_ident(policyname) || ' ON ${qualifiedTable};', E'\\\\n')\n FROM pg_policies\n WHERE tablename = '${table}'\n AND schemaname = '${schemaName}'\n AND policyname LIKE '${policyPrefix}_%'\n );\nEND $$;`\n )\n\n // Disable RLS\n statements.push(`ALTER TABLE ${qualifiedTable} DISABLE ROW LEVEL SECURITY;`)\n }\n\n return statements\n }\n}\n\n/**\n * Sync RLS context to PostgreSQL session settings\n * Call this at the start of each request/transaction\n */\nexport async function syncContextToPostgres<DB>(\n db: Kysely<DB>,\n context: {\n userId: string | number\n tenantId?: string | number\n roles?: string[]\n permissions?: string[]\n isSystem?: boolean\n }\n): Promise<void> {\n const { userId, tenantId, roles, permissions, isSystem } = context\n\n await sql`\n SELECT\n set_config('app.user_id', ${String(userId)}, true),\n set_config('app.tenant_id', ${tenantId ? String(tenantId) : ''}, true),\n set_config('app.roles', ${(roles ?? []).join(',')}, true),\n set_config('app.permissions', ${(permissions ?? []).join(',')}, true),\n set_config('app.is_system', ${isSystem ? 'true' : 'false'}, true)\n `.execute(db)\n}\n\n/**\n * Clear RLS context from PostgreSQL session\n */\nexport async function clearPostgresContext<DB>(db: Kysely<DB>): Promise<void> {\n await sql`\n SELECT\n set_config('app.user_id', '', true),\n set_config('app.tenant_id', '', true),\n set_config('app.roles', '', true),\n set_config('app.permissions', '', true),\n set_config('app.is_system', 'false', true)\n `.execute(db)\n}\n","import type { RLSSchema } from '../policy/types.js'\nimport { PostgresRLSGenerator, type PostgresRLSOptions } from './postgres.js'\n\n/**\n * Options for migration generation\n */\nexport interface MigrationOptions extends PostgresRLSOptions {\n /** Migration name */\n name?: string\n /** Include context functions in migration */\n includeContextFunctions?: boolean\n}\n\n/**\n * RLS Migration Generator\n * Generates Kysely migration files for RLS policies\n */\nexport class RLSMigrationGenerator {\n private generator = new PostgresRLSGenerator()\n\n /**\n * Generate migration file content\n */\n generateMigration<DB>(schema: RLSSchema<DB>, options: MigrationOptions = {}): string {\n const { name = 'rls_policies', includeContextFunctions = true, ...generatorOptions } = options\n\n const upStatements = this.generator.generateStatements(schema, generatorOptions)\n const downStatements = this.generator.generateDropStatements(schema, generatorOptions)\n\n const contextFunctions = includeContextFunctions\n ? this.generator.generateContextFunctions()\n : ''\n\n return `import { Kysely, sql } from 'kysely';\n\n/**\n * Migration: ${name}\n * Generated by @kysera/rls\n *\n * This migration sets up Row-Level Security policies for the database.\n */\n\nexport async function up(db: Kysely<any>): Promise<void> {\n${\n includeContextFunctions\n ? ` // Create RLS context functions\n await sql.raw(\\`${this.escapeTemplate(contextFunctions)}\\`).execute(db);\n\n`\n : ''\n} // Enable RLS and create policies\n${upStatements.map(s => ` await sql.raw(\\`${this.escapeTemplate(s)}\\`).execute(db);`).join('\\n')}\n}\n\nexport async function down(db: Kysely<any>): Promise<void> {\n${downStatements.map(s => ` await sql.raw(\\`${this.escapeTemplate(s)}\\`).execute(db);`).join('\\n')}\n${\n includeContextFunctions\n ? `\n // Drop RLS context functions\n await sql.raw(\\`\n DROP FUNCTION IF EXISTS rls_current_user_id();\n DROP FUNCTION IF EXISTS rls_current_tenant_id();\n DROP FUNCTION IF EXISTS rls_current_roles();\n DROP FUNCTION IF EXISTS rls_has_role(text);\n DROP FUNCTION IF EXISTS rls_current_permissions();\n DROP FUNCTION IF EXISTS rls_has_permission(text);\n DROP FUNCTION IF EXISTS rls_is_system();\n \\`).execute(db);`\n : ''\n}\n}\n`\n }\n\n /**\n * Escape template literal for embedding in string\n */\n private escapeTemplate(str: string): string {\n return str.replace(/`/g, '\\\\`').replace(/\\$/g, '\\\\$')\n }\n\n /**\n * Generate migration filename with timestamp\n */\n generateFilename(name = 'rls_policies'): string {\n const timestamp = new Date()\n .toISOString()\n .replace(/[-:]/g, '')\n .replace('T', '_')\n .replace(/\\..+/, '')\n return `${timestamp}_${name}.ts`\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/errors.ts","../../src/native/postgres.ts","../../src/native/migration.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * RLS Error Classes\n *\n * This module provides specialized error classes for Row-Level Security operations.\n * All errors extend base error classes from @kysera/core for consistency across\n * the Kysera ecosystem.\n *\n * @module @kysera/rls/errors\n */\n\nimport { DatabaseError } from '@kysera/core'\nimport type { ErrorCode } from '@kysera/core'\n\n// ============================================================================\n// RLS Error Codes\n// ============================================================================\n\n/**\n * RLS-specific error codes\n *\n * These codes extend the unified error codes from @kysera/core with\n * RLS-specific error conditions.\n */\nexport const RLSErrorCodes = {\n /** RLS context is missing or not set */\n RLS_CONTEXT_MISSING: 'RLS_CONTEXT_MISSING' as ErrorCode,\n /** RLS policy violation occurred */\n RLS_POLICY_VIOLATION: 'RLS_POLICY_VIOLATION' as ErrorCode,\n /** RLS policy definition is invalid */\n RLS_POLICY_INVALID: 'RLS_POLICY_INVALID' as ErrorCode,\n /** RLS schema definition is invalid */\n RLS_SCHEMA_INVALID: 'RLS_SCHEMA_INVALID' as ErrorCode,\n /** RLS context validation failed */\n RLS_CONTEXT_INVALID: 'RLS_CONTEXT_INVALID' as ErrorCode,\n /** RLS policy evaluation threw an error */\n RLS_POLICY_EVALUATION_ERROR: 'RLS_POLICY_EVALUATION_ERROR' as ErrorCode\n} as const\n\n/**\n * Type for RLS error codes\n */\nexport type RLSErrorCode = (typeof RLSErrorCodes)[keyof typeof RLSErrorCodes]\n\n// ============================================================================\n// Base RLS Error\n// ============================================================================\n\n/**\n * Base class for all RLS-related errors\n *\n * Extends DatabaseError from @kysera/core for consistency with other Kysera packages.\n * Provides common error functionality including error codes and JSON serialization.\n *\n * @example\n * ```typescript\n * throw new RLSError('Something went wrong', RLSErrorCodes.RLS_POLICY_INVALID);\n * ```\n */\nexport class RLSError extends DatabaseError {\n /**\n * Creates a new RLS error\n *\n * @param message - Error message\n * @param code - RLS error code\n */\n constructor(message: string, code: RLSErrorCode) {\n super(message, code)\n this.name = 'RLSError'\n }\n}\n\n// ============================================================================\n// Context Errors\n// ============================================================================\n\n/**\n * Error thrown when RLS context is missing\n *\n * This error occurs when an operation requiring RLS context is executed\n * outside of a context scope (i.e., without calling withRLSContext()).\n *\n * @example\n * ```typescript\n * // This will throw RLSContextError\n * const result = await db.selectFrom('posts').execute();\n *\n * // Correct usage with context\n * await withRLSContext(rlsContext, async () => {\n * const result = await db.selectFrom('posts').execute();\n * });\n * ```\n */\nexport class RLSContextError extends RLSError {\n /**\n * Creates a new RLS context error\n *\n * @param message - Error message (defaults to standard message)\n */\n constructor(message = 'No RLS context found. Ensure code runs within withRLSContext()') {\n super(message, RLSErrorCodes.RLS_CONTEXT_MISSING)\n this.name = 'RLSContextError'\n }\n}\n\n/**\n * Error thrown when RLS context validation fails\n *\n * Extends RLSError as context validation failures are RLS-specific errors.\n *\n * This error occurs when the provided RLS context is invalid or missing\n * required fields.\n *\n * @example\n * ```typescript\n * // Missing required userId field\n * const invalidContext = {\n * auth: {\n * roles: ['user']\n * // userId is missing!\n * },\n * timestamp: new Date()\n * };\n *\n * // This will throw RLSContextValidationError\n * validateRLSContext(invalidContext);\n * ```\n */\nexport class RLSContextValidationError extends RLSError {\n public readonly field: string\n\n /**\n * Creates a new context validation error\n *\n * @param message - Error message\n * @param field - Field that failed validation\n */\n constructor(message: string, field: string) {\n super(message, RLSErrorCodes.RLS_CONTEXT_INVALID)\n this.name = 'RLSContextValidationError'\n this.field = field\n }\n\n override toJSON(): Record<string, unknown> {\n return {\n ...super.toJSON(),\n field: this.field\n }\n }\n}\n\n// ============================================================================\n// Policy Errors\n// ============================================================================\n\n/**\n * Error thrown when an RLS policy violation occurs\n *\n * This error is thrown when a database operation is denied by RLS policies.\n * It provides detailed information about the violation including the operation,\n * table, and reason for denial.\n *\n * @example\n * ```typescript\n * // User tries to update a post they don't own\n * throw new RLSPolicyViolation(\n * 'update',\n * 'posts',\n * 'User does not own this post',\n * 'ownership_policy'\n * );\n * ```\n */\nexport class RLSPolicyViolation extends RLSError {\n public readonly operation: string\n public readonly table: string\n public readonly reason: string\n public readonly policyName?: string\n\n /**\n * Creates a new policy violation error\n *\n * @param operation - Database operation that was denied (read, create, update, delete)\n * @param table - Table name where violation occurred\n * @param reason - Reason for the policy violation\n * @param policyName - Name of the policy that denied access (optional)\n */\n constructor(operation: string, table: string, reason: string, policyName?: string) {\n super(\n `RLS policy violation: ${operation} on ${table} - ${reason}`,\n RLSErrorCodes.RLS_POLICY_VIOLATION\n )\n this.name = 'RLSPolicyViolation'\n this.operation = operation\n this.table = table\n this.reason = reason\n if (policyName !== undefined) {\n this.policyName = policyName\n }\n }\n\n override toJSON(): Record<string, unknown> {\n const json: Record<string, unknown> = {\n ...super.toJSON(),\n operation: this.operation,\n table: this.table,\n reason: this.reason\n }\n if (this.policyName !== undefined) {\n json['policyName'] = this.policyName\n }\n return json\n }\n}\n\n// ============================================================================\n// Policy Evaluation Errors\n// ============================================================================\n\n/**\n * Error thrown when a policy condition throws an error during evaluation\n *\n * This error is distinct from RLSPolicyViolation - it indicates a bug in the\n * policy condition function itself, not a legitimate access denial.\n *\n * @example\n * ```typescript\n * // A policy with a bug\n * allow('read', ctx => {\n * return ctx.row.someField.value; // Throws if someField is undefined\n * });\n *\n * // This will throw RLSPolicyEvaluationError, not RLSPolicyViolation\n * ```\n */\nexport class RLSPolicyEvaluationError extends RLSError {\n public readonly operation: string\n public readonly table: string\n public readonly policyName?: string\n public readonly originalError?: Error\n\n /**\n * Creates a new policy evaluation error\n *\n * @param operation - Database operation being performed\n * @param table - Table name where error occurred\n * @param message - Error message from the policy\n * @param policyName - Name of the policy that threw\n * @param originalError - The original error thrown by the policy\n */\n constructor(\n operation: string,\n table: string,\n message: string,\n policyName?: string,\n originalError?: Error\n ) {\n super(\n `RLS policy evaluation error during ${operation} on ${table}: ${message}`,\n RLSErrorCodes.RLS_POLICY_EVALUATION_ERROR\n )\n this.name = 'RLSPolicyEvaluationError'\n this.operation = operation\n this.table = table\n if (policyName !== undefined) {\n this.policyName = policyName\n }\n if (originalError !== undefined) {\n this.originalError = originalError\n // Preserve the original stack trace for debugging\n if (originalError.stack) {\n this.stack = `${this.stack}\\n\\nCaused by:\\n${originalError.stack}`\n }\n }\n }\n\n override toJSON(): Record<string, unknown> {\n const json: Record<string, unknown> = {\n ...super.toJSON(),\n operation: this.operation,\n table: this.table\n }\n if (this.policyName !== undefined) {\n json['policyName'] = this.policyName\n }\n if (this.originalError !== undefined) {\n json['originalError'] = {\n name: this.originalError.name,\n message: this.originalError.message\n }\n }\n return json\n }\n}\n\n// ============================================================================\n// Schema Errors\n// ============================================================================\n\n/**\n * Error thrown when RLS schema validation fails\n *\n * Extends RLSError as schema validation failures are RLS-specific errors.\n *\n * This error occurs when the RLS schema definition is invalid or contains\n * configuration errors.\n *\n * @example\n * ```typescript\n * // Invalid policy definition\n * const invalidSchema = {\n * posts: {\n * policies: [\n * {\n * type: 'invalid-type', // Invalid policy type!\n * operation: 'read',\n * condition: (ctx) => true\n * }\n * ]\n * }\n * };\n *\n * // This will throw RLSSchemaError\n * validateRLSSchema(invalidSchema);\n * ```\n */\nexport class RLSSchemaError extends RLSError {\n public readonly details: Record<string, unknown>\n\n /**\n * Creates a new schema validation error\n *\n * @param message - Error message\n * @param details - Additional details about the validation failure\n */\n constructor(message: string, details: Record<string, unknown> = {}) {\n super(message, RLSErrorCodes.RLS_SCHEMA_INVALID)\n this.name = 'RLSSchemaError'\n this.details = details\n }\n\n override toJSON(): Record<string, unknown> {\n return {\n ...super.toJSON(),\n details: this.details\n }\n }\n}\n","import type { Kysely } from 'kysely'\nimport { sql } from 'kysely'\nimport type { RLSSchema, TableRLSConfig, PolicyDefinition, Operation } from '../policy/types.js'\nimport { RLSSchemaError } from '../errors.js'\n\n/**\n * Pattern for valid SQL identifiers (prevents injection in DDL statements)\n */\nconst SAFE_IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_]*$/\n\n/**\n * Validate and quote a SQL identifier to prevent injection.\n * Only allows alphanumeric + underscore characters.\n * @throws RLSSchemaError if identifier contains unsafe characters\n */\nfunction quoteIdent(name: string, context: string): string {\n if (!SAFE_IDENTIFIER.test(name)) {\n throw new RLSSchemaError(\n `Unsafe ${context}: \"${name}\". Only alphanumeric and underscore characters are allowed.`,\n { identifier: name, context }\n )\n }\n return '\"' + name.replace(/\"/g, '\"\"') + '\"'\n}\n\n/**\n * Quote a string literal for use in PL/pgSQL (single quotes, doubled for escaping)\n */\nfunction quoteLiteral(value: string, context: string): string {\n if (!SAFE_IDENTIFIER.test(value)) {\n throw new RLSSchemaError(\n `Unsafe ${context}: \"${value}\". Only alphanumeric and underscore characters are allowed.`,\n { identifier: value, context }\n )\n }\n return \"'\" + value.replace(/'/g, \"''\") + \"'\"\n}\n\n/**\n * Options for PostgreSQL RLS generation\n */\nexport interface PostgresRLSOptions {\n /** Force RLS on table owners */\n force?: boolean\n /** Schema name (default: public) */\n schemaName?: string\n /** Prefix for generated policy names */\n policyPrefix?: string\n}\n\n/**\n * PostgreSQL RLS Generator\n * Generates native PostgreSQL RLS statements from Kysera RLS schema\n */\nexport class PostgresRLSGenerator {\n /**\n * Generate all PostgreSQL RLS statements from schema\n */\n generateStatements<DB>(schema: RLSSchema<DB>, options: PostgresRLSOptions = {}): string[] {\n const { force = true, schemaName = 'public', policyPrefix = 'rls' } = options\n\n const statements: string[] = []\n const quotedSchema = quoteIdent(schemaName, 'schema name')\n\n for (const [table, config] of Object.entries(schema)) {\n if (!config) continue\n\n const quotedTable = quoteIdent(table, 'table name')\n const qualifiedTable = `${quotedSchema}.${quotedTable}`\n const tableConfig = config as TableRLSConfig\n\n // Enable RLS on table\n statements.push(`ALTER TABLE ${qualifiedTable} ENABLE ROW LEVEL SECURITY;`)\n\n if (force) {\n statements.push(`ALTER TABLE ${qualifiedTable} FORCE ROW LEVEL SECURITY;`)\n }\n\n // Generate policies\n let policyIndex = 0\n for (const policy of tableConfig.policies) {\n const rawPolicyName = policy.name ?? `${policyPrefix}_${table}_${policy.type}_${policyIndex++}`\n const policySQL = this.generatePolicy(qualifiedTable, rawPolicyName, policy)\n if (policySQL) {\n statements.push(policySQL)\n }\n }\n }\n\n return statements\n }\n\n /**\n * Generate a single policy statement\n */\n private generatePolicy(table: string, name: string, policy: PolicyDefinition): string | null {\n // Skip filter policies (they're ORM-only)\n if (policy.type === 'filter' || policy.type === 'validate') {\n return null\n }\n\n // Need USING or WITH CHECK clause for native RLS\n if (!policy.using && !policy.withCheck) {\n return null\n }\n\n const quotedName = quoteIdent(name, 'policy name')\n const parts: string[] = [`CREATE POLICY ${quotedName}`, `ON ${table}`]\n\n // Policy type\n if (policy.type === 'deny') {\n parts.push('AS RESTRICTIVE')\n } else {\n parts.push('AS PERMISSIVE')\n }\n\n // Target role — validate to prevent injection\n const role = policy.role ?? 'public'\n parts.push(`TO ${quoteIdent(role, 'role name')}`)\n\n // Operation\n parts.push(`FOR ${this.mapOperation(policy.operation)}`)\n\n // USING clause\n if (policy.using) {\n parts.push(`USING (${policy.using})`)\n }\n\n // WITH CHECK clause\n if (policy.withCheck) {\n parts.push(`WITH CHECK (${policy.withCheck})`)\n }\n\n return parts.join('\\n ') + ';'\n }\n\n /**\n * Map Kysera operation to PostgreSQL operation\n */\n private mapOperation(operation: Operation | Operation[]): string {\n if (Array.isArray(operation)) {\n if (operation.length === 0) {\n return 'ALL'\n }\n if (operation.length === 4 || operation.includes('all')) {\n return 'ALL'\n }\n // PostgreSQL doesn't support multiple operations in one policy\n // Return first operation\n return this.mapSingleOperation(operation[0]!)\n }\n return this.mapSingleOperation(operation)\n }\n\n /**\n * Map single operation\n */\n private mapSingleOperation(op: Operation): string {\n switch (op) {\n case 'read':\n return 'SELECT'\n case 'create':\n return 'INSERT'\n case 'update':\n return 'UPDATE'\n case 'delete':\n return 'DELETE'\n case 'all':\n return 'ALL'\n default:\n return 'ALL'\n }\n }\n\n /**\n * Generate context-setting functions for PostgreSQL\n * These functions should be STABLE for optimal performance\n */\n generateContextFunctions(): string {\n return `\n-- RLS Context Functions (STABLE for query planner optimization)\n-- These functions read session variables set by the application\n\nCREATE OR REPLACE FUNCTION rls_current_user_id()\nRETURNS text\nLANGUAGE SQL STABLE\nAS $$ SELECT current_setting('app.user_id', true) $$;\n\nCREATE OR REPLACE FUNCTION rls_current_tenant_id()\nRETURNS uuid\nLANGUAGE SQL STABLE\nAS $$ SELECT NULLIF(current_setting('app.tenant_id', true), '')::uuid $$;\n\nCREATE OR REPLACE FUNCTION rls_current_roles()\nRETURNS text[]\nLANGUAGE SQL STABLE\nAS $$ SELECT string_to_array(COALESCE(current_setting('app.roles', true), ''), ',') $$;\n\nCREATE OR REPLACE FUNCTION rls_has_role(role_name text)\nRETURNS boolean\nLANGUAGE SQL STABLE\nAS $$ SELECT role_name = ANY(rls_current_roles()) $$;\n\nCREATE OR REPLACE FUNCTION rls_current_permissions()\nRETURNS text[]\nLANGUAGE SQL STABLE\nAS $$ SELECT string_to_array(COALESCE(current_setting('app.permissions', true), ''), ',') $$;\n\nCREATE OR REPLACE FUNCTION rls_has_permission(permission_name text)\nRETURNS boolean\nLANGUAGE SQL STABLE\nAS $$ SELECT permission_name = ANY(rls_current_permissions()) $$;\n\nCREATE OR REPLACE FUNCTION rls_is_system()\nRETURNS boolean\nLANGUAGE SQL STABLE\nAS $$ SELECT COALESCE(current_setting('app.is_system', true), 'false')::boolean $$;\n`\n }\n\n /**\n * Generate DROP statements for cleaning up\n */\n generateDropStatements<DB>(schema: RLSSchema<DB>, options: PostgresRLSOptions = {}): string[] {\n const { schemaName = 'public', policyPrefix = 'rls' } = options\n const statements: string[] = []\n const quotedSchema = quoteIdent(schemaName, 'schema name')\n\n // Validate prefix (used in LIKE pattern)\n if (!SAFE_IDENTIFIER.test(policyPrefix)) {\n throw new RLSSchemaError(\n `Unsafe policy prefix: \"${policyPrefix}\". Only alphanumeric and underscore characters are allowed.`,\n { identifier: policyPrefix, context: 'policy prefix' }\n )\n }\n\n for (const table of Object.keys(schema)) {\n const quotedTable = quoteIdent(table, 'table name')\n const qualifiedTable = `${quotedSchema}.${quotedTable}`\n\n // Drop all policies with prefix — use dollar-quoting for safety\n // pg_policies.tablename/schemaname are system-provided, safe for comparison\n // policyPrefix is validated above\n statements.push(\n `DO $$ BEGIN\n EXECUTE (\n SELECT string_agg('DROP POLICY IF EXISTS ' || quote_ident(policyname) || ' ON ${qualifiedTable};', E'\\\\n')\n FROM pg_policies\n WHERE tablename = ${quoteLiteral(table, 'table name')}\n AND schemaname = ${quoteLiteral(schemaName, 'schema name')}\n AND policyname LIKE '${policyPrefix}_%'\n );\nEND $$;`\n )\n\n // Disable RLS\n statements.push(`ALTER TABLE ${qualifiedTable} DISABLE ROW LEVEL SECURITY;`)\n }\n\n return statements\n }\n}\n\n/**\n * Sync RLS context to PostgreSQL session settings\n * Call this at the start of each request/transaction\n */\nexport async function syncContextToPostgres<DB>(\n db: Kysely<DB>,\n context: {\n userId: string | number\n tenantId?: string | number\n roles?: string[]\n permissions?: string[]\n isSystem?: boolean\n }\n): Promise<void> {\n const { userId, tenantId, roles, permissions, isSystem } = context\n\n await sql`\n SELECT\n set_config('app.user_id', ${String(userId)}, true),\n set_config('app.tenant_id', ${tenantId ? String(tenantId) : ''}, true),\n set_config('app.roles', ${(roles ?? []).join(',')}, true),\n set_config('app.permissions', ${(permissions ?? []).join(',')}, true),\n set_config('app.is_system', ${isSystem ? 'true' : 'false'}, true)\n `.execute(db)\n}\n\n/**\n * Clear RLS context from PostgreSQL session\n */\nexport async function clearPostgresContext<DB>(db: Kysely<DB>): Promise<void> {\n await sql`\n SELECT\n set_config('app.user_id', '', true),\n set_config('app.tenant_id', '', true),\n set_config('app.roles', '', true),\n set_config('app.permissions', '', true),\n set_config('app.is_system', 'false', true)\n `.execute(db)\n}\n","import type { RLSSchema } from '../policy/types.js'\nimport { PostgresRLSGenerator, type PostgresRLSOptions } from './postgres.js'\n\n/**\n * Options for migration generation\n */\nexport interface MigrationOptions extends PostgresRLSOptions {\n /** Migration name */\n name?: string\n /** Include context functions in migration */\n includeContextFunctions?: boolean\n}\n\n/**\n * RLS Migration Generator\n * Generates Kysely migration files for RLS policies\n */\nexport class RLSMigrationGenerator {\n private generator = new PostgresRLSGenerator()\n\n /**\n * Generate migration file content\n */\n generateMigration<DB>(schema: RLSSchema<DB>, options: MigrationOptions = {}): string {\n const { name = 'rls_policies', includeContextFunctions = true, ...generatorOptions } = options\n\n const upStatements = this.generator.generateStatements(schema, generatorOptions)\n const downStatements = this.generator.generateDropStatements(schema, generatorOptions)\n\n const contextFunctions = includeContextFunctions\n ? this.generator.generateContextFunctions()\n : ''\n\n return `import { Kysely, sql } from 'kysely';\n\n/**\n * Migration: ${name}\n * Generated by @kysera/rls\n *\n * This migration sets up Row-Level Security policies for the database.\n */\n\nexport async function up(db: Kysely<any>): Promise<void> {\n${\n includeContextFunctions\n ? ` // Create RLS context functions\n await sql.raw(\\`${this.escapeTemplate(contextFunctions)}\\`).execute(db);\n\n`\n : ''\n} // Enable RLS and create policies\n${upStatements.map(s => ` await sql.raw(\\`${this.escapeTemplate(s)}\\`).execute(db);`).join('\\n')}\n}\n\nexport async function down(db: Kysely<any>): Promise<void> {\n${downStatements.map(s => ` await sql.raw(\\`${this.escapeTemplate(s)}\\`).execute(db);`).join('\\n')}\n${\n includeContextFunctions\n ? `\n // Drop RLS context functions\n await sql.raw(\\`\n DROP FUNCTION IF EXISTS rls_current_user_id();\n DROP FUNCTION IF EXISTS rls_current_tenant_id();\n DROP FUNCTION IF EXISTS rls_current_roles();\n DROP FUNCTION IF EXISTS rls_has_role(text);\n DROP FUNCTION IF EXISTS rls_current_permissions();\n DROP FUNCTION IF EXISTS rls_has_permission(text);\n DROP FUNCTION IF EXISTS rls_is_system();\n \\`).execute(db);`\n : ''\n}\n}\n`\n }\n\n /**\n * Escape template literal for embedding in string\n */\n private escapeTemplate(str: string): string {\n return str.replace(/`/g, '\\\\`').replace(/\\$/g, '\\\\$')\n }\n\n /**\n * Generate migration filename with timestamp\n */\n generateFilename(name = 'rls_policies'): string {\n const timestamp = new Date()\n .toISOString()\n .replace(/[-:]/g, '')\n .replace('T', '_')\n .replace(/\\..+/, '')\n return `${timestamp}_${name}.ts`\n }\n}\n"]}
|
|
@@ -186,8 +186,9 @@ interface RLSContext<TUser = unknown, TMeta = unknown> {
|
|
|
186
186
|
/**
|
|
187
187
|
* Context creation timestamp
|
|
188
188
|
* Used for temporal policies and audit trails
|
|
189
|
+
* @default new Date() (auto-set when not provided)
|
|
189
190
|
*/
|
|
190
|
-
timestamp
|
|
191
|
+
timestamp?: Date;
|
|
191
192
|
}
|
|
192
193
|
/**
|
|
193
194
|
* Context passed to policy evaluation functions
|
|
@@ -676,6 +677,15 @@ interface PolicyActivationContext {
|
|
|
676
677
|
* Custom metadata for activation decisions
|
|
677
678
|
*/
|
|
678
679
|
meta?: Record<string, unknown>;
|
|
680
|
+
/**
|
|
681
|
+
* Authentication context for auth-based policy activation
|
|
682
|
+
*/
|
|
683
|
+
auth?: {
|
|
684
|
+
userId?: string;
|
|
685
|
+
roles?: string[];
|
|
686
|
+
isSystem?: boolean;
|
|
687
|
+
[key: string]: unknown;
|
|
688
|
+
};
|
|
679
689
|
}
|
|
680
690
|
/**
|
|
681
691
|
* Condition function for policy activation
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kysera/rls",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.8",
|
|
4
4
|
"description": "Row-Level Security plugin for Kysely - declarative policies, query transformation, native RLS support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -20,10 +20,10 @@
|
|
|
20
20
|
"src"
|
|
21
21
|
],
|
|
22
22
|
"peerDependencies": {
|
|
23
|
-
"kysely": ">=0.28.
|
|
23
|
+
"kysely": ">=0.28.14",
|
|
24
24
|
"zod": "^4.3.6",
|
|
25
|
-
"@kysera/executor": "0.8.
|
|
26
|
-
"@kysera/repository": "0.8.
|
|
25
|
+
"@kysera/executor": "0.8.8",
|
|
26
|
+
"@kysera/repository": "0.8.8"
|
|
27
27
|
},
|
|
28
28
|
"peerDependenciesMeta": {
|
|
29
29
|
"@kysera/executor": {
|
|
@@ -37,23 +37,23 @@
|
|
|
37
37
|
}
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@kysera/core": "0.8.
|
|
40
|
+
"@kysera/core": "0.8.8"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/better-sqlite3": "^7.6.13",
|
|
44
|
-
"@types/node": "^25.5.
|
|
45
|
-
"@types/pg": "^8.
|
|
46
|
-
"@vitest/coverage-v8": "^4.1.
|
|
44
|
+
"@types/node": "^25.5.2",
|
|
45
|
+
"@types/pg": "^8.20.0",
|
|
46
|
+
"@vitest/coverage-v8": "^4.1.3",
|
|
47
47
|
"better-sqlite3": "^12.8.0",
|
|
48
|
-
"kysely": "^0.28.
|
|
48
|
+
"kysely": "^0.28.15",
|
|
49
49
|
"mysql2": "^3.20.0",
|
|
50
50
|
"pg": "^8.20.0",
|
|
51
51
|
"tsup": "^8.5.1",
|
|
52
|
-
"typescript": "^
|
|
53
|
-
"vitest": "^4.1.
|
|
52
|
+
"typescript": "^6.0.2",
|
|
53
|
+
"vitest": "^4.1.3",
|
|
54
54
|
"zod": "^4.3.6",
|
|
55
|
-
"@kysera/executor": "0.8.
|
|
56
|
-
"@kysera/repository": "0.8.
|
|
55
|
+
"@kysera/executor": "0.8.8",
|
|
56
|
+
"@kysera/repository": "0.8.8"
|
|
57
57
|
},
|
|
58
58
|
"keywords": [
|
|
59
59
|
"kysely",
|
|
@@ -89,7 +89,7 @@
|
|
|
89
89
|
"bun": ">=1.0.0"
|
|
90
90
|
},
|
|
91
91
|
"scripts": {
|
|
92
|
-
"build": "tsup",
|
|
92
|
+
"build": "tsup && node ../../scripts/fix-node-imports.cjs ./dist async_hooks",
|
|
93
93
|
"dev": "tsup --watch",
|
|
94
94
|
"test": "vitest run",
|
|
95
95
|
"test:watch": "vitest watch",
|
|
@@ -102,7 +102,7 @@
|
|
|
102
102
|
"docker:up": "docker compose -f test/docker/docker-compose.test.yml up -d",
|
|
103
103
|
"docker:down": "docker compose -f test/docker/docker-compose.test.yml down -v",
|
|
104
104
|
"docker:logs": "docker compose -f test/docker/docker-compose.test.yml logs -f",
|
|
105
|
-
"typecheck": "tsc --noEmit",
|
|
105
|
+
"typecheck": "tsc --noEmit -p tsconfig.build.json",
|
|
106
106
|
"lint": "eslint ."
|
|
107
107
|
}
|
|
108
108
|
}
|
package/src/native/postgres.ts
CHANGED
|
@@ -1,6 +1,40 @@
|
|
|
1
1
|
import type { Kysely } from 'kysely'
|
|
2
2
|
import { sql } from 'kysely'
|
|
3
3
|
import type { RLSSchema, TableRLSConfig, PolicyDefinition, Operation } from '../policy/types.js'
|
|
4
|
+
import { RLSSchemaError } from '../errors.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Pattern for valid SQL identifiers (prevents injection in DDL statements)
|
|
8
|
+
*/
|
|
9
|
+
const SAFE_IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_]*$/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Validate and quote a SQL identifier to prevent injection.
|
|
13
|
+
* Only allows alphanumeric + underscore characters.
|
|
14
|
+
* @throws RLSSchemaError if identifier contains unsafe characters
|
|
15
|
+
*/
|
|
16
|
+
function quoteIdent(name: string, context: string): string {
|
|
17
|
+
if (!SAFE_IDENTIFIER.test(name)) {
|
|
18
|
+
throw new RLSSchemaError(
|
|
19
|
+
`Unsafe ${context}: "${name}". Only alphanumeric and underscore characters are allowed.`,
|
|
20
|
+
{ identifier: name, context }
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
return '"' + name.replace(/"/g, '""') + '"'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Quote a string literal for use in PL/pgSQL (single quotes, doubled for escaping)
|
|
28
|
+
*/
|
|
29
|
+
function quoteLiteral(value: string, context: string): string {
|
|
30
|
+
if (!SAFE_IDENTIFIER.test(value)) {
|
|
31
|
+
throw new RLSSchemaError(
|
|
32
|
+
`Unsafe ${context}: "${value}". Only alphanumeric and underscore characters are allowed.`,
|
|
33
|
+
{ identifier: value, context }
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
return "'" + value.replace(/'/g, "''") + "'"
|
|
37
|
+
}
|
|
4
38
|
|
|
5
39
|
/**
|
|
6
40
|
* Options for PostgreSQL RLS generation
|
|
@@ -26,11 +60,13 @@ export class PostgresRLSGenerator {
|
|
|
26
60
|
const { force = true, schemaName = 'public', policyPrefix = 'rls' } = options
|
|
27
61
|
|
|
28
62
|
const statements: string[] = []
|
|
63
|
+
const quotedSchema = quoteIdent(schemaName, 'schema name')
|
|
29
64
|
|
|
30
65
|
for (const [table, config] of Object.entries(schema)) {
|
|
31
66
|
if (!config) continue
|
|
32
67
|
|
|
33
|
-
const
|
|
68
|
+
const quotedTable = quoteIdent(table, 'table name')
|
|
69
|
+
const qualifiedTable = `${quotedSchema}.${quotedTable}`
|
|
34
70
|
const tableConfig = config as TableRLSConfig
|
|
35
71
|
|
|
36
72
|
// Enable RLS on table
|
|
@@ -43,8 +79,8 @@ export class PostgresRLSGenerator {
|
|
|
43
79
|
// Generate policies
|
|
44
80
|
let policyIndex = 0
|
|
45
81
|
for (const policy of tableConfig.policies) {
|
|
46
|
-
const
|
|
47
|
-
const policySQL = this.generatePolicy(qualifiedTable,
|
|
82
|
+
const rawPolicyName = policy.name ?? `${policyPrefix}_${table}_${policy.type}_${policyIndex++}`
|
|
83
|
+
const policySQL = this.generatePolicy(qualifiedTable, rawPolicyName, policy)
|
|
48
84
|
if (policySQL) {
|
|
49
85
|
statements.push(policySQL)
|
|
50
86
|
}
|
|
@@ -68,7 +104,8 @@ export class PostgresRLSGenerator {
|
|
|
68
104
|
return null
|
|
69
105
|
}
|
|
70
106
|
|
|
71
|
-
const
|
|
107
|
+
const quotedName = quoteIdent(name, 'policy name')
|
|
108
|
+
const parts: string[] = [`CREATE POLICY ${quotedName}`, `ON ${table}`]
|
|
72
109
|
|
|
73
110
|
// Policy type
|
|
74
111
|
if (policy.type === 'deny') {
|
|
@@ -77,8 +114,9 @@ export class PostgresRLSGenerator {
|
|
|
77
114
|
parts.push('AS PERMISSIVE')
|
|
78
115
|
}
|
|
79
116
|
|
|
80
|
-
// Target role
|
|
81
|
-
|
|
117
|
+
// Target role — validate to prevent injection
|
|
118
|
+
const role = policy.role ?? 'public'
|
|
119
|
+
parts.push(`TO ${quoteIdent(role, 'role name')}`)
|
|
82
120
|
|
|
83
121
|
// Operation
|
|
84
122
|
parts.push(`FOR ${this.mapOperation(policy.operation)}`)
|
|
@@ -186,18 +224,30 @@ AS $$ SELECT COALESCE(current_setting('app.is_system', true), 'false')::boolean
|
|
|
186
224
|
generateDropStatements<DB>(schema: RLSSchema<DB>, options: PostgresRLSOptions = {}): string[] {
|
|
187
225
|
const { schemaName = 'public', policyPrefix = 'rls' } = options
|
|
188
226
|
const statements: string[] = []
|
|
227
|
+
const quotedSchema = quoteIdent(schemaName, 'schema name')
|
|
228
|
+
|
|
229
|
+
// Validate prefix (used in LIKE pattern)
|
|
230
|
+
if (!SAFE_IDENTIFIER.test(policyPrefix)) {
|
|
231
|
+
throw new RLSSchemaError(
|
|
232
|
+
`Unsafe policy prefix: "${policyPrefix}". Only alphanumeric and underscore characters are allowed.`,
|
|
233
|
+
{ identifier: policyPrefix, context: 'policy prefix' }
|
|
234
|
+
)
|
|
235
|
+
}
|
|
189
236
|
|
|
190
237
|
for (const table of Object.keys(schema)) {
|
|
191
|
-
const
|
|
238
|
+
const quotedTable = quoteIdent(table, 'table name')
|
|
239
|
+
const qualifiedTable = `${quotedSchema}.${quotedTable}`
|
|
192
240
|
|
|
193
|
-
// Drop all policies with prefix
|
|
241
|
+
// Drop all policies with prefix — use dollar-quoting for safety
|
|
242
|
+
// pg_policies.tablename/schemaname are system-provided, safe for comparison
|
|
243
|
+
// policyPrefix is validated above
|
|
194
244
|
statements.push(
|
|
195
245
|
`DO $$ BEGIN
|
|
196
246
|
EXECUTE (
|
|
197
247
|
SELECT string_agg('DROP POLICY IF EXISTS ' || quote_ident(policyname) || ' ON ${qualifiedTable};', E'\\n')
|
|
198
248
|
FROM pg_policies
|
|
199
|
-
WHERE tablename =
|
|
200
|
-
AND schemaname =
|
|
249
|
+
WHERE tablename = ${quoteLiteral(table, 'table name')}
|
|
250
|
+
AND schemaname = ${quoteLiteral(schemaName, 'schema name')}
|
|
201
251
|
AND policyname LIKE '${policyPrefix}_%'
|
|
202
252
|
);
|
|
203
253
|
END $$;`
|
package/src/plugin.ts
CHANGED
|
@@ -21,7 +21,7 @@ import { MutationGuard } from './transformer/mutation.js'
|
|
|
21
21
|
import { rlsContext } from './context/manager.js'
|
|
22
22
|
import { VERSION } from './version.js'
|
|
23
23
|
import { RLSContextError, RLSPolicyViolation, RLSError, RLSErrorCodes } from './errors.js'
|
|
24
|
-
import { silentLogger, type KyseraLogger } from '@kysera/core'
|
|
24
|
+
import { silentLogger, shouldApplyToTable, type KyseraLogger } from '@kysera/core'
|
|
25
25
|
import {
|
|
26
26
|
transformQueryBuilder,
|
|
27
27
|
selectFromDynamicTable,
|
|
@@ -39,8 +39,15 @@ export interface RLSPluginOptions<DB = unknown> {
|
|
|
39
39
|
schema: RLSSchema<DB>
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
42
|
+
* Whitelist of tables to apply RLS to.
|
|
43
|
+
* If provided, only these tables will have RLS enforced.
|
|
44
|
+
* Takes precedence over excludeTables when both are provided.
|
|
45
|
+
*/
|
|
46
|
+
tables?: string[]
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Tables to exclude from RLS (always bypass policies).
|
|
50
|
+
* Ignored if `tables` whitelist is provided.
|
|
44
51
|
*/
|
|
45
52
|
excludeTables?: string[]
|
|
46
53
|
|
|
@@ -103,6 +110,7 @@ export interface RLSPluginOptions<DB = unknown> {
|
|
|
103
110
|
* Note: 'schema' and 'onViolation' are not included as they are complex runtime objects.
|
|
104
111
|
*/
|
|
105
112
|
export const RLSPluginOptionsSchema = z.object({
|
|
113
|
+
tables: z.array(z.string()).optional(),
|
|
106
114
|
excludeTables: z.array(z.string()).optional(),
|
|
107
115
|
bypassRoles: z.array(z.string()).optional(),
|
|
108
116
|
requireContext: z.boolean().optional(),
|
|
@@ -169,7 +177,8 @@ type BaseRepository = BaseRepositoryLike<Record<string, unknown>>
|
|
|
169
177
|
export function rlsPlugin<DB>(options: RLSPluginOptions<DB>): Plugin {
|
|
170
178
|
const {
|
|
171
179
|
schema,
|
|
172
|
-
|
|
180
|
+
tables,
|
|
181
|
+
excludeTables,
|
|
173
182
|
bypassRoles = [],
|
|
174
183
|
logger = silentLogger,
|
|
175
184
|
requireContext = true, // SECURITY: Changed to true for secure-by-default (CRIT-2 fix)
|
|
@@ -188,8 +197,8 @@ export function rlsPlugin<DB>(options: RLSPluginOptions<DB>): Plugin {
|
|
|
188
197
|
name: '@kysera/rls',
|
|
189
198
|
version: VERSION,
|
|
190
199
|
|
|
191
|
-
//
|
|
192
|
-
priority:
|
|
200
|
+
// SECURITY plugin: must run FIRST to enforce access policies before other plugins
|
|
201
|
+
priority: 1000,
|
|
193
202
|
|
|
194
203
|
// No dependencies by default
|
|
195
204
|
dependencies: [],
|
|
@@ -200,7 +209,7 @@ export function rlsPlugin<DB>(options: RLSPluginOptions<DB>): Plugin {
|
|
|
200
209
|
onInit<TDB>(_executor: Kysely<TDB>): void {
|
|
201
210
|
logger.info?.('[RLS] Initializing RLS plugin', {
|
|
202
211
|
tables: Object.keys(schema).length,
|
|
203
|
-
excludeTables: excludeTables
|
|
212
|
+
excludeTables: excludeTables?.length ?? 0,
|
|
204
213
|
bypassRoles: bypassRoles.length
|
|
205
214
|
})
|
|
206
215
|
|
|
@@ -220,11 +229,9 @@ export function rlsPlugin<DB>(options: RLSPluginOptions<DB>): Plugin {
|
|
|
220
229
|
/**
|
|
221
230
|
* Cleanup resources when executor is destroyed
|
|
222
231
|
*/
|
|
223
|
-
onDestroy()
|
|
224
|
-
// Clear registry to free up memory
|
|
232
|
+
onDestroy() {
|
|
225
233
|
registry.clear()
|
|
226
234
|
logger.info?.('[RLS] RLS plugin destroyed, cleared policy registry')
|
|
227
|
-
return Promise.resolve()
|
|
228
235
|
},
|
|
229
236
|
|
|
230
237
|
/**
|
|
@@ -238,7 +245,7 @@ export function rlsPlugin<DB>(options: RLSPluginOptions<DB>): Plugin {
|
|
|
238
245
|
const { operation, table, metadata } = context
|
|
239
246
|
|
|
240
247
|
// Skip if table is excluded
|
|
241
|
-
if (
|
|
248
|
+
if (!shouldApplyToTable(table, { tables, excludeTables })) {
|
|
242
249
|
logger.debug?.(`[RLS] Skipping RLS for excluded table: ${table}`)
|
|
243
250
|
return qb
|
|
244
251
|
}
|
|
@@ -358,7 +365,7 @@ export function rlsPlugin<DB>(options: RLSPluginOptions<DB>): Plugin {
|
|
|
358
365
|
const table = baseRepo.tableName
|
|
359
366
|
|
|
360
367
|
// Skip excluded tables
|
|
361
|
-
if (
|
|
368
|
+
if (!shouldApplyToTable(table, { tables, excludeTables })) {
|
|
362
369
|
logger.debug?.(`[RLS] Skipping repository extension for excluded table: ${table}`)
|
|
363
370
|
return repo
|
|
364
371
|
}
|
package/src/policy/types.ts
CHANGED
|
@@ -219,8 +219,9 @@ export interface RLSContext<TUser = unknown, TMeta = unknown> {
|
|
|
219
219
|
/**
|
|
220
220
|
* Context creation timestamp
|
|
221
221
|
* Used for temporal policies and audit trails
|
|
222
|
+
* @default new Date() (auto-set when not provided)
|
|
222
223
|
*/
|
|
223
|
-
timestamp
|
|
224
|
+
timestamp?: Date
|
|
224
225
|
}
|
|
225
226
|
|
|
226
227
|
// ============================================================================
|
|
@@ -800,6 +801,16 @@ export interface PolicyActivationContext {
|
|
|
800
801
|
* Custom metadata for activation decisions
|
|
801
802
|
*/
|
|
802
803
|
meta?: Record<string, unknown>
|
|
804
|
+
|
|
805
|
+
/**
|
|
806
|
+
* Authentication context for auth-based policy activation
|
|
807
|
+
*/
|
|
808
|
+
auth?: {
|
|
809
|
+
userId?: string
|
|
810
|
+
roles?: string[]
|
|
811
|
+
isSystem?: boolean
|
|
812
|
+
[key: string]: unknown
|
|
813
|
+
}
|
|
803
814
|
}
|
|
804
815
|
|
|
805
816
|
/**
|
|
@@ -257,10 +257,12 @@ export class MutationGuard<DB = unknown> {
|
|
|
257
257
|
return
|
|
258
258
|
}
|
|
259
259
|
|
|
260
|
+
// Create evaluation context once and reuse for all policies
|
|
261
|
+
const evalCtx = this.createEvalContext(ctx, table, operation, row, data)
|
|
262
|
+
|
|
260
263
|
// Evaluate deny policies first (they override allows)
|
|
261
264
|
const denies = this.registry.getDenies(table, operation)
|
|
262
265
|
for (const deny of denies) {
|
|
263
|
-
const evalCtx = this.createEvalContext(ctx, table, operation, row, data)
|
|
264
266
|
const result = await this.evaluatePolicy(deny.evaluate, evalCtx, deny.name)
|
|
265
267
|
|
|
266
268
|
if (result) {
|
|
@@ -272,7 +274,6 @@ export class MutationGuard<DB = unknown> {
|
|
|
272
274
|
if ((operation === 'create' || operation === 'update') && data) {
|
|
273
275
|
const validates = this.registry.getValidates(table, operation)
|
|
274
276
|
for (const validate of validates) {
|
|
275
|
-
const evalCtx = this.createEvalContext(ctx, table, operation, row, data)
|
|
276
277
|
const result = await this.evaluatePolicy(validate.evaluate, evalCtx, validate.name)
|
|
277
278
|
|
|
278
279
|
if (!result) {
|
|
@@ -290,11 +291,9 @@ export class MutationGuard<DB = unknown> {
|
|
|
290
291
|
}
|
|
291
292
|
|
|
292
293
|
if (allows.length > 0) {
|
|
293
|
-
// At least one allow policy must pass
|
|
294
294
|
let allowed = false
|
|
295
295
|
|
|
296
296
|
for (const allow of allows) {
|
|
297
|
-
const evalCtx = this.createEvalContext(ctx, table, operation, row, data)
|
|
298
297
|
const result = await this.evaluatePolicy(allow.evaluate, evalCtx, allow.name)
|
|
299
298
|
|
|
300
299
|
if (result) {
|