@kysera/rls 0.5.1
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/LICENSE +21 -0
- package/README.md +1341 -0
- package/dist/index.d.ts +705 -0
- package/dist/index.js +1471 -0
- package/dist/index.js.map +1 -0
- package/dist/native/index.d.ts +91 -0
- package/dist/native/index.js +253 -0
- package/dist/native/index.js.map +1 -0
- package/dist/types-Dtg6Lt1k.d.ts +633 -0
- package/package.json +93 -0
- package/src/context/index.ts +9 -0
- package/src/context/manager.ts +203 -0
- package/src/context/storage.ts +8 -0
- package/src/context/types.ts +5 -0
- package/src/errors.ts +280 -0
- package/src/index.ts +95 -0
- package/src/native/README.md +315 -0
- package/src/native/index.ts +11 -0
- package/src/native/migration.ts +92 -0
- package/src/native/postgres.ts +263 -0
- package/src/plugin.ts +464 -0
- package/src/policy/builder.ts +215 -0
- package/src/policy/index.ts +10 -0
- package/src/policy/registry.ts +403 -0
- package/src/policy/schema.ts +257 -0
- package/src/policy/types.ts +742 -0
- package/src/transformer/index.ts +2 -0
- package/src/transformer/mutation.ts +372 -0
- package/src/transformer/select.ts +150 -0
- package/src/utils/helpers.ts +139 -0
- package/src/utils/index.ts +12 -0
|
@@ -0,0 +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>(\n schema: RLSSchema<DB>,\n options: PostgresRLSOptions = {}\n ): string[] {\n const {\n force = true,\n schemaName = 'public',\n policyPrefix = 'rls',\n } = 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(\n table: string,\n name: string,\n policy: PolicyDefinition\n ): 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[] = [\n `CREATE POLICY \"${name}\"`,\n `ON ${table}`,\n ];\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': return 'SELECT';\n case 'create': return 'INSERT';\n case 'update': return 'UPDATE';\n case 'delete': return 'DELETE';\n case 'all': return 'ALL';\n default: 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>(\n schema: RLSSchema<DB>,\n options: PostgresRLSOptions = {}\n ): 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>(\n schema: RLSSchema<DB>,\n options: MigrationOptions = {}\n ): string {\n const {\n name = 'rls_policies',\n includeContextFunctions = true,\n ...generatorOptions\n } = 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${includeContextFunctions ? ` // Create RLS context functions\n await sql.raw(\\`${this.escapeTemplate(contextFunctions)}\\`).execute(db);\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${includeContextFunctions ? `\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 * 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: string = 'rls_policies'): string {\n const timestamp = new Date().toISOString()\n .replace(/[-:]/g, '')\n .replace('T', '_')\n .replace(/\\..+/, '');\n return `${timestamp}_${name}.ts`;\n }\n}\n"]}
|
|
@@ -0,0 +1,633 @@
|
|
|
1
|
+
import { Kysely } from 'kysely';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Core type definitions for RLS (Row-Level Security) policies
|
|
5
|
+
*
|
|
6
|
+
* This module provides comprehensive type definitions for defining and evaluating
|
|
7
|
+
* row-level security policies in Kysera ORM. It supports multiple policy types,
|
|
8
|
+
* flexible context management, and type-safe policy definitions.
|
|
9
|
+
*
|
|
10
|
+
* @module @kysera/rls/policy/types
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Database operations that can be controlled by RLS policies
|
|
15
|
+
*
|
|
16
|
+
* - `read`: SELECT operations
|
|
17
|
+
* - `create`: INSERT operations
|
|
18
|
+
* - `update`: UPDATE operations
|
|
19
|
+
* - `delete`: DELETE operations
|
|
20
|
+
* - `all`: All operations (wildcard)
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```typescript
|
|
24
|
+
* const policy: PolicyDefinition = {
|
|
25
|
+
* type: 'allow',
|
|
26
|
+
* operation: ['read', 'update'], // Multiple operations
|
|
27
|
+
* condition: (ctx) => ctx.auth.userId === ctx.row.userId
|
|
28
|
+
* };
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
type Operation = 'read' | 'create' | 'update' | 'delete' | 'all';
|
|
32
|
+
/**
|
|
33
|
+
* Authentication context containing user identity and authorization information
|
|
34
|
+
*
|
|
35
|
+
* This context is passed to all policy evaluation functions and contains
|
|
36
|
+
* information about the authenticated user, their roles, and permissions.
|
|
37
|
+
*
|
|
38
|
+
* @typeParam TUser - Custom user type for additional user properties
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```typescript
|
|
42
|
+
* const authContext: RLSAuthContext = {
|
|
43
|
+
* userId: 123,
|
|
44
|
+
* roles: ['user', 'editor'],
|
|
45
|
+
* tenantId: 'acme-corp',
|
|
46
|
+
* organizationIds: ['org-1', 'org-2'],
|
|
47
|
+
* permissions: ['posts:read', 'posts:write'],
|
|
48
|
+
* isSystem: false
|
|
49
|
+
* };
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
interface RLSAuthContext<TUser = unknown> {
|
|
53
|
+
/**
|
|
54
|
+
* Unique identifier for the authenticated user
|
|
55
|
+
* Can be a string or number depending on your ID strategy
|
|
56
|
+
*/
|
|
57
|
+
userId: string | number;
|
|
58
|
+
/**
|
|
59
|
+
* List of roles assigned to the user
|
|
60
|
+
* Used for role-based access control (RBAC)
|
|
61
|
+
*/
|
|
62
|
+
roles: string[];
|
|
63
|
+
/**
|
|
64
|
+
* Optional tenant identifier for multi-tenancy
|
|
65
|
+
* Use for tenant isolation in SaaS applications
|
|
66
|
+
*/
|
|
67
|
+
tenantId?: string | number;
|
|
68
|
+
/**
|
|
69
|
+
* Optional list of organization IDs the user belongs to
|
|
70
|
+
* Use for hierarchical multi-tenancy or organization-based access
|
|
71
|
+
*/
|
|
72
|
+
organizationIds?: (string | number)[];
|
|
73
|
+
/**
|
|
74
|
+
* Optional list of granular permissions
|
|
75
|
+
* Use for fine-grained access control
|
|
76
|
+
*/
|
|
77
|
+
permissions?: string[];
|
|
78
|
+
/**
|
|
79
|
+
* Optional custom attributes for advanced policy logic
|
|
80
|
+
* Can contain any additional context needed for policy evaluation
|
|
81
|
+
*/
|
|
82
|
+
attributes?: Record<string, unknown>;
|
|
83
|
+
/**
|
|
84
|
+
* Optional full user object for accessing user properties
|
|
85
|
+
* Useful when policies need to check user-specific attributes
|
|
86
|
+
*/
|
|
87
|
+
user?: TUser;
|
|
88
|
+
/**
|
|
89
|
+
* Flag indicating if this is a system/admin context
|
|
90
|
+
* System contexts typically bypass RLS policies
|
|
91
|
+
*
|
|
92
|
+
* @default false
|
|
93
|
+
*/
|
|
94
|
+
isSystem?: boolean;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* HTTP request context for audit and policy evaluation
|
|
98
|
+
*
|
|
99
|
+
* Contains information about the current request, useful for logging,
|
|
100
|
+
* audit trails, and IP-based or time-based access policies.
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* ```typescript
|
|
104
|
+
* const requestContext: RLSRequestContext = {
|
|
105
|
+
* requestId: 'req-123abc',
|
|
106
|
+
* ipAddress: '192.168.1.100',
|
|
107
|
+
* userAgent: 'Mozilla/5.0...',
|
|
108
|
+
* timestamp: new Date(),
|
|
109
|
+
* headers: { 'x-api-key': 'secret' }
|
|
110
|
+
* };
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
interface RLSRequestContext {
|
|
114
|
+
/**
|
|
115
|
+
* Unique identifier for the request
|
|
116
|
+
* Useful for tracing and debugging
|
|
117
|
+
*/
|
|
118
|
+
requestId?: string;
|
|
119
|
+
/**
|
|
120
|
+
* Client IP address
|
|
121
|
+
* Can be used for IP-based access policies
|
|
122
|
+
*/
|
|
123
|
+
ipAddress?: string;
|
|
124
|
+
/**
|
|
125
|
+
* Client user agent string
|
|
126
|
+
* Useful for device-based access policies
|
|
127
|
+
*/
|
|
128
|
+
userAgent?: string;
|
|
129
|
+
/**
|
|
130
|
+
* Request timestamp
|
|
131
|
+
* Required for time-based policies and audit logs
|
|
132
|
+
*/
|
|
133
|
+
timestamp: Date;
|
|
134
|
+
/**
|
|
135
|
+
* HTTP headers
|
|
136
|
+
* Can contain custom authentication or context headers
|
|
137
|
+
*/
|
|
138
|
+
headers?: Record<string, string>;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Complete RLS context containing all information for policy evaluation
|
|
142
|
+
*
|
|
143
|
+
* This is the main context object passed to policy functions and used
|
|
144
|
+
* throughout the RLS system.
|
|
145
|
+
*
|
|
146
|
+
* @typeParam TUser - Custom user type
|
|
147
|
+
* @typeParam TMeta - Custom metadata type for additional context
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* ```typescript
|
|
151
|
+
* const rlsContext: RLSContext = {
|
|
152
|
+
* auth: {
|
|
153
|
+
* userId: 123,
|
|
154
|
+
* roles: ['user'],
|
|
155
|
+
* tenantId: 'acme-corp'
|
|
156
|
+
* },
|
|
157
|
+
* request: {
|
|
158
|
+
* requestId: 'req-123',
|
|
159
|
+
* ipAddress: '192.168.1.1',
|
|
160
|
+
* timestamp: new Date()
|
|
161
|
+
* },
|
|
162
|
+
* meta: {
|
|
163
|
+
* feature_flags: ['new_ui', 'beta_access']
|
|
164
|
+
* },
|
|
165
|
+
* timestamp: new Date()
|
|
166
|
+
* };
|
|
167
|
+
* ```
|
|
168
|
+
*/
|
|
169
|
+
interface RLSContext<TUser = unknown, TMeta = unknown> {
|
|
170
|
+
/**
|
|
171
|
+
* Authentication context (required)
|
|
172
|
+
* Contains user identity and authorization information
|
|
173
|
+
*/
|
|
174
|
+
auth: RLSAuthContext<TUser>;
|
|
175
|
+
/**
|
|
176
|
+
* Request context (optional)
|
|
177
|
+
* Contains HTTP request information
|
|
178
|
+
*/
|
|
179
|
+
request?: RLSRequestContext;
|
|
180
|
+
/**
|
|
181
|
+
* Custom metadata (optional)
|
|
182
|
+
* Can contain any additional context needed for policy evaluation
|
|
183
|
+
* Examples: feature flags, A/B test groups, regional settings
|
|
184
|
+
*/
|
|
185
|
+
meta?: TMeta;
|
|
186
|
+
/**
|
|
187
|
+
* Context creation timestamp
|
|
188
|
+
* Used for temporal policies and audit trails
|
|
189
|
+
*/
|
|
190
|
+
timestamp: Date;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Context passed to policy evaluation functions
|
|
194
|
+
*
|
|
195
|
+
* This extended context includes the authentication/request context plus
|
|
196
|
+
* additional information about the current row being evaluated and the
|
|
197
|
+
* data being operated on.
|
|
198
|
+
*
|
|
199
|
+
* @typeParam TAuth - Custom user type for auth context
|
|
200
|
+
* @typeParam TRow - Type of the database row being evaluated
|
|
201
|
+
* @typeParam TData - Type of the data being inserted/updated
|
|
202
|
+
* @typeParam TDB - Database schema type for Kysely
|
|
203
|
+
*
|
|
204
|
+
* @example
|
|
205
|
+
* ```typescript
|
|
206
|
+
* // Policy function using evaluation context
|
|
207
|
+
* const ownershipPolicy = (ctx: PolicyEvaluationContext<User, Post>) => {
|
|
208
|
+
* // Check if user owns the post
|
|
209
|
+
* return ctx.auth.userId === ctx.row.authorId;
|
|
210
|
+
* };
|
|
211
|
+
*
|
|
212
|
+
* // Filter policy using evaluation context
|
|
213
|
+
* const tenantFilter = (ctx: PolicyEvaluationContext) => {
|
|
214
|
+
* return {
|
|
215
|
+
* tenant_id: ctx.auth.tenantId
|
|
216
|
+
* };
|
|
217
|
+
* };
|
|
218
|
+
* ```
|
|
219
|
+
*/
|
|
220
|
+
interface PolicyEvaluationContext<TAuth = unknown, TRow = unknown, TData = unknown, TDB = unknown> {
|
|
221
|
+
/**
|
|
222
|
+
* Authentication context
|
|
223
|
+
* Contains user identity and authorization information
|
|
224
|
+
*/
|
|
225
|
+
auth: RLSAuthContext<TAuth>;
|
|
226
|
+
/**
|
|
227
|
+
* Current row being evaluated (optional)
|
|
228
|
+
* Available during read/update/delete operations
|
|
229
|
+
* Used for row-level policies that check row attributes
|
|
230
|
+
*/
|
|
231
|
+
row?: TRow;
|
|
232
|
+
/**
|
|
233
|
+
* Data being inserted or updated (optional)
|
|
234
|
+
* Available during create/update operations
|
|
235
|
+
* Used for validation policies
|
|
236
|
+
*/
|
|
237
|
+
data?: TData;
|
|
238
|
+
/**
|
|
239
|
+
* Request context (optional)
|
|
240
|
+
* Contains HTTP request information
|
|
241
|
+
*/
|
|
242
|
+
request?: RLSRequestContext;
|
|
243
|
+
/**
|
|
244
|
+
* Kysely database instance (optional)
|
|
245
|
+
* Available for policies that need to perform additional queries
|
|
246
|
+
* Use sparingly as it can impact performance
|
|
247
|
+
*/
|
|
248
|
+
db?: Kysely<TDB>;
|
|
249
|
+
/**
|
|
250
|
+
* Custom metadata (optional)
|
|
251
|
+
* Can contain any additional context needed for policy evaluation
|
|
252
|
+
*/
|
|
253
|
+
meta?: Record<string, unknown>;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Policy condition function or expression
|
|
257
|
+
*
|
|
258
|
+
* Can be either:
|
|
259
|
+
* - A function that returns a boolean (or Promise<boolean>)
|
|
260
|
+
* - A string expression for native RLS (PostgreSQL)
|
|
261
|
+
*
|
|
262
|
+
* @typeParam TCtx - Policy evaluation context type
|
|
263
|
+
*
|
|
264
|
+
* @example
|
|
265
|
+
* ```typescript
|
|
266
|
+
* // Function-based condition
|
|
267
|
+
* const condition: PolicyCondition = (ctx) => {
|
|
268
|
+
* return ctx.auth.roles.includes('admin') ||
|
|
269
|
+
* ctx.auth.userId === ctx.row.ownerId;
|
|
270
|
+
* };
|
|
271
|
+
*
|
|
272
|
+
* // Async condition
|
|
273
|
+
* const asyncCondition: PolicyCondition = async (ctx) => {
|
|
274
|
+
* const hasPermission = await checkPermission(ctx.auth.userId, 'posts:read');
|
|
275
|
+
* return hasPermission;
|
|
276
|
+
* };
|
|
277
|
+
*
|
|
278
|
+
* // String expression for native RLS
|
|
279
|
+
* const nativeCondition: PolicyCondition = 'user_id = current_user_id()';
|
|
280
|
+
* ```
|
|
281
|
+
*/
|
|
282
|
+
type PolicyCondition<TCtx extends PolicyEvaluationContext = PolicyEvaluationContext> = ((ctx: TCtx) => boolean | Promise<boolean>) | string;
|
|
283
|
+
/**
|
|
284
|
+
* Filter condition that returns WHERE clause conditions
|
|
285
|
+
*
|
|
286
|
+
* Used for filter-type policies that add WHERE conditions to queries.
|
|
287
|
+
* Can be either:
|
|
288
|
+
* - A function that returns an object with column-value pairs
|
|
289
|
+
* - An object mapping column names to context property paths
|
|
290
|
+
*
|
|
291
|
+
* @typeParam TCtx - Policy evaluation context type
|
|
292
|
+
*
|
|
293
|
+
* @example
|
|
294
|
+
* ```typescript
|
|
295
|
+
* // Function-based filter
|
|
296
|
+
* const filter: FilterCondition = (ctx) => ({
|
|
297
|
+
* tenant_id: ctx.auth.tenantId,
|
|
298
|
+
* deleted_at: null
|
|
299
|
+
* });
|
|
300
|
+
*
|
|
301
|
+
* // Static filter mapping
|
|
302
|
+
* const staticFilter: FilterCondition = {
|
|
303
|
+
* tenant_id: 'auth.tenantId',
|
|
304
|
+
* status: 'meta.defaultStatus'
|
|
305
|
+
* };
|
|
306
|
+
* ```
|
|
307
|
+
*/
|
|
308
|
+
type FilterCondition<TCtx extends PolicyEvaluationContext = PolicyEvaluationContext> = ((ctx: TCtx) => Record<string, unknown>) | Record<string, string>;
|
|
309
|
+
/**
|
|
310
|
+
* Policy behavior type
|
|
311
|
+
*
|
|
312
|
+
* - `allow`: Grants access if condition is true
|
|
313
|
+
* - `deny`: Denies access if condition is true (takes precedence)
|
|
314
|
+
* - `filter`: Adds WHERE conditions to automatically filter rows
|
|
315
|
+
* - `validate`: Validates data during create/update operations
|
|
316
|
+
*
|
|
317
|
+
* @example
|
|
318
|
+
* ```typescript
|
|
319
|
+
* // Allow policy: grants access to owners
|
|
320
|
+
* const allowPolicy: PolicyDefinition = {
|
|
321
|
+
* type: 'allow',
|
|
322
|
+
* operation: 'update',
|
|
323
|
+
* condition: (ctx) => ctx.auth.userId === ctx.row.ownerId
|
|
324
|
+
* };
|
|
325
|
+
*
|
|
326
|
+
* // Deny policy: prevents access to deleted items
|
|
327
|
+
* const denyPolicy: PolicyDefinition = {
|
|
328
|
+
* type: 'deny',
|
|
329
|
+
* operation: 'read',
|
|
330
|
+
* condition: (ctx) => ctx.row.deletedAt !== null
|
|
331
|
+
* };
|
|
332
|
+
*
|
|
333
|
+
* // Filter policy: automatically filters by tenant
|
|
334
|
+
* const filterPolicy: PolicyDefinition = {
|
|
335
|
+
* type: 'filter',
|
|
336
|
+
* operation: 'read',
|
|
337
|
+
* condition: (ctx) => ({ tenant_id: ctx.auth.tenantId })
|
|
338
|
+
* };
|
|
339
|
+
*
|
|
340
|
+
* // Validate policy: ensures valid status transitions
|
|
341
|
+
* const validatePolicy: PolicyDefinition = {
|
|
342
|
+
* type: 'validate',
|
|
343
|
+
* operation: 'update',
|
|
344
|
+
* condition: (ctx) => isValidStatusTransition(ctx.row.status, ctx.data.status)
|
|
345
|
+
* };
|
|
346
|
+
* ```
|
|
347
|
+
*/
|
|
348
|
+
type PolicyType = 'allow' | 'deny' | 'filter' | 'validate';
|
|
349
|
+
/**
|
|
350
|
+
* Policy definition for RLS enforcement
|
|
351
|
+
*
|
|
352
|
+
* Defines a single security policy with its behavior, operations, and conditions.
|
|
353
|
+
*
|
|
354
|
+
* @typeParam TOperation - Operation type(s) the policy applies to
|
|
355
|
+
* @typeParam TCondition - Condition type (function or string)
|
|
356
|
+
*
|
|
357
|
+
* @example
|
|
358
|
+
* ```typescript
|
|
359
|
+
* // Basic ownership policy
|
|
360
|
+
* const ownershipPolicy: PolicyDefinition = {
|
|
361
|
+
* type: 'allow',
|
|
362
|
+
* operation: ['read', 'update', 'delete'],
|
|
363
|
+
* condition: (ctx) => ctx.auth.userId === ctx.row.ownerId,
|
|
364
|
+
* name: 'ownership_policy',
|
|
365
|
+
* priority: 100
|
|
366
|
+
* };
|
|
367
|
+
*
|
|
368
|
+
* // Native PostgreSQL RLS policy
|
|
369
|
+
* const nativePolicy: PolicyDefinition = {
|
|
370
|
+
* type: 'allow',
|
|
371
|
+
* operation: 'read',
|
|
372
|
+
* condition: '',
|
|
373
|
+
* using: 'user_id = current_user_id()',
|
|
374
|
+
* withCheck: 'user_id = current_user_id()',
|
|
375
|
+
* role: 'authenticated',
|
|
376
|
+
* name: 'user_isolation'
|
|
377
|
+
* };
|
|
378
|
+
*
|
|
379
|
+
* // Multi-tenancy filter
|
|
380
|
+
* const tenantFilter: PolicyDefinition = {
|
|
381
|
+
* type: 'filter',
|
|
382
|
+
* operation: 'read',
|
|
383
|
+
* condition: (ctx) => ({ tenant_id: ctx.auth.tenantId }),
|
|
384
|
+
* name: 'tenant_isolation',
|
|
385
|
+
* priority: 1000 // High priority for tenant isolation
|
|
386
|
+
* };
|
|
387
|
+
* ```
|
|
388
|
+
*/
|
|
389
|
+
interface PolicyDefinition<TOperation extends Operation = Operation, TCondition = PolicyCondition> {
|
|
390
|
+
/**
|
|
391
|
+
* Policy behavior type
|
|
392
|
+
* Determines how the policy affects access control
|
|
393
|
+
*/
|
|
394
|
+
type: PolicyType;
|
|
395
|
+
/**
|
|
396
|
+
* Operation(s) this policy applies to
|
|
397
|
+
* Can be a single operation or an array of operations
|
|
398
|
+
*/
|
|
399
|
+
operation: TOperation | TOperation[];
|
|
400
|
+
/**
|
|
401
|
+
* Condition function or expression
|
|
402
|
+
* - For 'allow'/'deny'/'validate': returns boolean
|
|
403
|
+
* - For 'filter': returns WHERE conditions object
|
|
404
|
+
* - For native RLS: SQL expression string
|
|
405
|
+
*/
|
|
406
|
+
condition: TCondition;
|
|
407
|
+
/**
|
|
408
|
+
* Optional policy name for debugging and logging
|
|
409
|
+
* Recommended for easier policy management
|
|
410
|
+
*/
|
|
411
|
+
name?: string;
|
|
412
|
+
/**
|
|
413
|
+
* Optional priority for policy evaluation order
|
|
414
|
+
* Higher priority policies are evaluated first
|
|
415
|
+
* Deny policies typically have higher priority
|
|
416
|
+
*
|
|
417
|
+
* @default 0
|
|
418
|
+
*/
|
|
419
|
+
priority?: number;
|
|
420
|
+
/**
|
|
421
|
+
* Optional SQL expression for native RLS USING clause
|
|
422
|
+
* PostgreSQL only - used for row visibility checks
|
|
423
|
+
*
|
|
424
|
+
* @example 'user_id = current_user_id()'
|
|
425
|
+
*/
|
|
426
|
+
using?: string;
|
|
427
|
+
/**
|
|
428
|
+
* Optional SQL expression for native RLS WITH CHECK clause
|
|
429
|
+
* PostgreSQL only - used for insert/update validation
|
|
430
|
+
*
|
|
431
|
+
* @example 'tenant_id = current_tenant_id()'
|
|
432
|
+
*/
|
|
433
|
+
withCheck?: string;
|
|
434
|
+
/**
|
|
435
|
+
* Optional database role this policy applies to
|
|
436
|
+
* PostgreSQL only - restricts policy to specific roles
|
|
437
|
+
*
|
|
438
|
+
* @example 'authenticated'
|
|
439
|
+
*/
|
|
440
|
+
role?: string;
|
|
441
|
+
/**
|
|
442
|
+
* Optional performance optimization hints
|
|
443
|
+
* Used for query optimization and index suggestions
|
|
444
|
+
*/
|
|
445
|
+
hints?: PolicyHints;
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* RLS configuration for a single database table
|
|
449
|
+
*
|
|
450
|
+
* Defines all policies and settings for row-level security on a table.
|
|
451
|
+
*
|
|
452
|
+
* @example
|
|
453
|
+
* ```typescript
|
|
454
|
+
* const postsConfig: TableRLSConfig = {
|
|
455
|
+
* policies: [
|
|
456
|
+
* {
|
|
457
|
+
* type: 'filter',
|
|
458
|
+
* operation: 'read',
|
|
459
|
+
* condition: (ctx) => ({ tenant_id: ctx.auth.tenantId }),
|
|
460
|
+
* name: 'tenant_isolation',
|
|
461
|
+
* priority: 1000
|
|
462
|
+
* },
|
|
463
|
+
* {
|
|
464
|
+
* type: 'allow',
|
|
465
|
+
* operation: ['update', 'delete'],
|
|
466
|
+
* condition: (ctx) => ctx.auth.userId === ctx.row.authorId,
|
|
467
|
+
* name: 'author_access'
|
|
468
|
+
* }
|
|
469
|
+
* ],
|
|
470
|
+
* defaultDeny: true,
|
|
471
|
+
* skipFor: ['system', 'admin']
|
|
472
|
+
* };
|
|
473
|
+
* ```
|
|
474
|
+
*/
|
|
475
|
+
interface TableRLSConfig {
|
|
476
|
+
/**
|
|
477
|
+
* List of policies to enforce on this table
|
|
478
|
+
* Policies are evaluated in priority order (highest first)
|
|
479
|
+
*/
|
|
480
|
+
policies: PolicyDefinition[];
|
|
481
|
+
/**
|
|
482
|
+
* Default behavior when no policies match
|
|
483
|
+
* - true: Deny access by default (secure default)
|
|
484
|
+
* - false: Allow access by default (open default)
|
|
485
|
+
*
|
|
486
|
+
* @default true
|
|
487
|
+
*/
|
|
488
|
+
defaultDeny?: boolean;
|
|
489
|
+
/**
|
|
490
|
+
* List of roles that bypass RLS policies
|
|
491
|
+
* Useful for system operations or admin accounts
|
|
492
|
+
*
|
|
493
|
+
* @example ['system', 'admin', 'superuser']
|
|
494
|
+
*/
|
|
495
|
+
skipFor?: string[];
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Complete RLS schema for all tables in the database
|
|
499
|
+
*
|
|
500
|
+
* Maps table names to their RLS configurations.
|
|
501
|
+
*
|
|
502
|
+
* @typeParam DB - Database schema type (Kysely DB type)
|
|
503
|
+
*
|
|
504
|
+
* @example
|
|
505
|
+
* ```typescript
|
|
506
|
+
* interface Database {
|
|
507
|
+
* posts: Post;
|
|
508
|
+
* comments: Comment;
|
|
509
|
+
* users: User;
|
|
510
|
+
* }
|
|
511
|
+
*
|
|
512
|
+
* const rlsSchema: RLSSchema<Database> = {
|
|
513
|
+
* posts: {
|
|
514
|
+
* policies: [
|
|
515
|
+
* {
|
|
516
|
+
* type: 'filter',
|
|
517
|
+
* operation: 'read',
|
|
518
|
+
* condition: (ctx) => ({ tenant_id: ctx.auth.tenantId })
|
|
519
|
+
* }
|
|
520
|
+
* ],
|
|
521
|
+
* defaultDeny: true
|
|
522
|
+
* },
|
|
523
|
+
* comments: {
|
|
524
|
+
* policies: [
|
|
525
|
+
* {
|
|
526
|
+
* type: 'allow',
|
|
527
|
+
* operation: 'all',
|
|
528
|
+
* condition: (ctx) => ctx.auth.roles.includes('moderator')
|
|
529
|
+
* }
|
|
530
|
+
* ]
|
|
531
|
+
* }
|
|
532
|
+
* };
|
|
533
|
+
* ```
|
|
534
|
+
*/
|
|
535
|
+
type RLSSchema<DB> = {
|
|
536
|
+
[K in keyof DB]?: TableRLSConfig;
|
|
537
|
+
};
|
|
538
|
+
/**
|
|
539
|
+
* Compiled policy ready for runtime evaluation
|
|
540
|
+
*
|
|
541
|
+
* Internal representation of a policy after compilation and optimization.
|
|
542
|
+
*
|
|
543
|
+
* @typeParam TCtx - Policy evaluation context type
|
|
544
|
+
*/
|
|
545
|
+
interface CompiledPolicy<TCtx = PolicyEvaluationContext> {
|
|
546
|
+
/**
|
|
547
|
+
* Policy behavior type
|
|
548
|
+
*/
|
|
549
|
+
type: PolicyType;
|
|
550
|
+
/**
|
|
551
|
+
* Operations this policy applies to
|
|
552
|
+
* Always an array after compilation
|
|
553
|
+
*/
|
|
554
|
+
operation: Operation[];
|
|
555
|
+
/**
|
|
556
|
+
* Compiled evaluation function
|
|
557
|
+
* Returns boolean or Promise<boolean>
|
|
558
|
+
*/
|
|
559
|
+
evaluate: (ctx: TCtx) => boolean | Promise<boolean>;
|
|
560
|
+
/**
|
|
561
|
+
* Policy name for debugging
|
|
562
|
+
*/
|
|
563
|
+
name: string;
|
|
564
|
+
/**
|
|
565
|
+
* Priority for evaluation order
|
|
566
|
+
*/
|
|
567
|
+
priority: number;
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Compiled filter policy for query transformation
|
|
571
|
+
*
|
|
572
|
+
* Specialized compiled policy type for filter operations.
|
|
573
|
+
*
|
|
574
|
+
* @typeParam TCtx - Policy evaluation context type
|
|
575
|
+
*/
|
|
576
|
+
interface CompiledFilterPolicy<TCtx = PolicyEvaluationContext> {
|
|
577
|
+
/**
|
|
578
|
+
* Always 'read' for filter policies
|
|
579
|
+
*/
|
|
580
|
+
operation: 'read';
|
|
581
|
+
/**
|
|
582
|
+
* Function to get WHERE conditions
|
|
583
|
+
* Returns object with column-value pairs
|
|
584
|
+
*/
|
|
585
|
+
getConditions: (ctx: TCtx) => Record<string, unknown>;
|
|
586
|
+
/**
|
|
587
|
+
* Policy name for debugging
|
|
588
|
+
*/
|
|
589
|
+
name: string;
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Optimization hints for policy execution
|
|
593
|
+
*
|
|
594
|
+
* Provides metadata to help optimize policy evaluation and query generation.
|
|
595
|
+
* These hints can be used by query optimizers and execution planners.
|
|
596
|
+
*
|
|
597
|
+
* @example
|
|
598
|
+
* ```typescript
|
|
599
|
+
* const hints: PolicyHints = {
|
|
600
|
+
* indexColumns: ['tenant_id', 'user_id'],
|
|
601
|
+
* selectivity: 'high',
|
|
602
|
+
* leakproof: true,
|
|
603
|
+
* stable: true
|
|
604
|
+
* };
|
|
605
|
+
* ```
|
|
606
|
+
*/
|
|
607
|
+
interface PolicyHints {
|
|
608
|
+
/**
|
|
609
|
+
* Columns that should be indexed for optimal performance
|
|
610
|
+
* Suggests which columns are frequently used in policy conditions
|
|
611
|
+
*/
|
|
612
|
+
indexColumns?: string[];
|
|
613
|
+
/**
|
|
614
|
+
* Expected selectivity of the policy
|
|
615
|
+
* - 'high': Filters out most rows (good for early evaluation)
|
|
616
|
+
* - 'medium': Filters moderate number of rows
|
|
617
|
+
* - 'low': Filters few rows (evaluate later)
|
|
618
|
+
*/
|
|
619
|
+
selectivity?: 'high' | 'medium' | 'low';
|
|
620
|
+
/**
|
|
621
|
+
* Whether the policy is leakproof (PostgreSQL concept)
|
|
622
|
+
* Leakproof functions don't reveal information about their inputs
|
|
623
|
+
* Safe to execute before other security checks
|
|
624
|
+
*/
|
|
625
|
+
leakproof?: boolean;
|
|
626
|
+
/**
|
|
627
|
+
* Whether the policy result is stable for the same inputs
|
|
628
|
+
* Stable policies can be cached during a query execution
|
|
629
|
+
*/
|
|
630
|
+
stable?: boolean;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
export type { CompiledPolicy as C, FilterCondition as F, Operation as O, PolicyCondition as P, RLSSchema as R, TableRLSConfig as T, PolicyHints as a, PolicyDefinition as b, CompiledFilterPolicy as c, RLSContext as d, RLSAuthContext as e, RLSRequestContext as f, PolicyEvaluationContext as g, PolicyType as h };
|