@pgpmjs/core 3.0.9 → 3.1.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/roles/index.js ADDED
@@ -0,0 +1,540 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateCreateBaseRolesSQL = generateCreateBaseRolesSQL;
4
+ exports.generateCreateUserSQL = generateCreateUserSQL;
5
+ exports.generateCreateTestUsersSQL = generateCreateTestUsersSQL;
6
+ exports.generateGrantRoleSQL = generateGrantRoleSQL;
7
+ exports.generateRemoveUserSQL = generateRemoveUserSQL;
8
+ exports.generateCreateUserWithGrantsSQL = generateCreateUserWithGrantsSQL;
9
+ /**
10
+ * Safely escape a string for use as a SQL string literal.
11
+ * Doubles single quotes and wraps in single quotes.
12
+ */
13
+ function sqlLiteral(value) {
14
+ return `'${value.replace(/'/g, "''")}'`;
15
+ }
16
+ /**
17
+ * Generate SQL to create base roles (anonymous, authenticated, administrator).
18
+ * Callers should use getConnEnvOptions() from @pgpmjs/env to get merged values.
19
+ * @param roles - Role mapping from getConnEnvOptions().roles!
20
+ * @throws Error if roles is undefined or missing required properties
21
+ */
22
+ function generateCreateBaseRolesSQL(roles) {
23
+ if (!roles) {
24
+ throw new Error('generateCreateBaseRolesSQL: roles parameter is undefined. ' +
25
+ 'Ensure getConnEnvOptions().roles is defined. ' +
26
+ 'Check that pgpm.config.js or pgpm.json does not set db.roles to undefined.');
27
+ }
28
+ if (!roles.anonymous || !roles.authenticated || !roles.administrator) {
29
+ throw new Error('generateCreateBaseRolesSQL: roles is missing required properties. ' +
30
+ `Got: anonymous=${roles.anonymous}, authenticated=${roles.authenticated}, administrator=${roles.administrator}. ` +
31
+ 'Ensure all role names are defined in your configuration.');
32
+ }
33
+ const r = {
34
+ anonymous: roles.anonymous,
35
+ authenticated: roles.authenticated,
36
+ administrator: roles.administrator
37
+ };
38
+ return `
39
+ BEGIN;
40
+ DO $do$
41
+ DECLARE
42
+ v_anonymous text := ${sqlLiteral(r.anonymous)};
43
+ v_authenticated text := ${sqlLiteral(r.authenticated)};
44
+ v_administrator text := ${sqlLiteral(r.administrator)};
45
+ BEGIN
46
+ -- Create anonymous role: pre-check + exception handling for TOCTOU safety
47
+ IF NOT EXISTS (SELECT 1 FROM pg_catalog.pg_roles WHERE rolname = v_anonymous) THEN
48
+ BEGIN
49
+ EXECUTE format('CREATE ROLE %I', v_anonymous);
50
+ EXCEPTION
51
+ WHEN duplicate_object THEN
52
+ -- 42710: Role already exists (race condition); safe to ignore
53
+ NULL;
54
+ WHEN unique_violation THEN
55
+ -- 23505: Concurrent CREATE ROLE hit unique index; safe to ignore
56
+ NULL;
57
+ WHEN insufficient_privilege THEN
58
+ -- 42501: Must surface this error - caller lacks permission
59
+ RAISE;
60
+ END;
61
+ END IF;
62
+
63
+ -- Create authenticated role: pre-check + exception handling for TOCTOU safety
64
+ IF NOT EXISTS (SELECT 1 FROM pg_catalog.pg_roles WHERE rolname = v_authenticated) THEN
65
+ BEGIN
66
+ EXECUTE format('CREATE ROLE %I', v_authenticated);
67
+ EXCEPTION
68
+ WHEN duplicate_object THEN
69
+ -- 42710: Role already exists (race condition); safe to ignore
70
+ NULL;
71
+ WHEN unique_violation THEN
72
+ -- 23505: Concurrent CREATE ROLE hit unique index; safe to ignore
73
+ NULL;
74
+ WHEN insufficient_privilege THEN
75
+ -- 42501: Must surface this error - caller lacks permission
76
+ RAISE;
77
+ END;
78
+ END IF;
79
+
80
+ -- Create administrator role: pre-check + exception handling for TOCTOU safety
81
+ IF NOT EXISTS (SELECT 1 FROM pg_catalog.pg_roles WHERE rolname = v_administrator) THEN
82
+ BEGIN
83
+ EXECUTE format('CREATE ROLE %I', v_administrator);
84
+ EXCEPTION
85
+ WHEN duplicate_object THEN
86
+ -- 42710: Role already exists (race condition); safe to ignore
87
+ NULL;
88
+ WHEN unique_violation THEN
89
+ -- 23505: Concurrent CREATE ROLE hit unique index; safe to ignore
90
+ NULL;
91
+ WHEN insufficient_privilege THEN
92
+ -- 42501: Must surface this error - caller lacks permission
93
+ RAISE;
94
+ END;
95
+ END IF;
96
+
97
+ -- Set role attributes (safe to run even if role already exists)
98
+ EXECUTE format('ALTER ROLE %I WITH NOCREATEDB NOSUPERUSER NOCREATEROLE NOLOGIN NOREPLICATION NOBYPASSRLS', v_anonymous);
99
+ EXECUTE format('ALTER ROLE %I WITH NOCREATEDB NOSUPERUSER NOCREATEROLE NOLOGIN NOREPLICATION NOBYPASSRLS', v_authenticated);
100
+ EXECUTE format('ALTER ROLE %I WITH NOCREATEDB NOSUPERUSER NOCREATEROLE NOLOGIN NOREPLICATION BYPASSRLS', v_administrator);
101
+ END
102
+ $do$;
103
+ COMMIT;
104
+ `;
105
+ }
106
+ /**
107
+ * Generate SQL to create a user with password and grant base roles.
108
+ * Callers should use getConnEnvOptions() from @pgpmjs/env to get merged values.
109
+ * @param roles - Role mapping from getConnEnvOptions().roles!
110
+ * @param useLocksForRoles - Whether to use advisory locks (from getConnEnvOptions().useLocksForRoles)
111
+ */
112
+ function generateCreateUserSQL(username, password, roles, useLocksForRoles = false) {
113
+ if (!roles) {
114
+ throw new Error('generateCreateUserSQL: roles parameter is undefined. ' +
115
+ 'Ensure getConnEnvOptions().roles is defined.');
116
+ }
117
+ if (!roles.anonymous || !roles.authenticated) {
118
+ throw new Error('generateCreateUserSQL: roles is missing required properties. ' +
119
+ `Got: anonymous=${roles.anonymous}, authenticated=${roles.authenticated}.`);
120
+ }
121
+ const r = {
122
+ anonymous: roles.anonymous,
123
+ authenticated: roles.authenticated
124
+ };
125
+ const lockStatement = useLocksForRoles
126
+ ? `PERFORM pg_advisory_xact_lock(42, hashtext(v_username));`
127
+ : '';
128
+ return `
129
+ BEGIN;
130
+ DO $do$
131
+ DECLARE
132
+ v_username text := ${sqlLiteral(username)};
133
+ v_password text := ${sqlLiteral(password)};
134
+ v_anonymous text := ${sqlLiteral(r.anonymous)};
135
+ v_authenticated text := ${sqlLiteral(r.authenticated)};
136
+ BEGIN
137
+ ${lockStatement}
138
+ -- Pre-check + exception handling for TOCTOU safety
139
+ IF NOT EXISTS (SELECT 1 FROM pg_catalog.pg_roles WHERE rolname = v_username) THEN
140
+ BEGIN
141
+ EXECUTE format('CREATE ROLE %I LOGIN PASSWORD %L', v_username, v_password);
142
+ EXCEPTION
143
+ WHEN duplicate_object THEN
144
+ -- 42710: Role already exists (race condition); safe to ignore
145
+ NULL;
146
+ WHEN unique_violation THEN
147
+ -- 23505: Concurrent CREATE ROLE hit unique index; safe to ignore
148
+ NULL;
149
+ WHEN insufficient_privilege THEN
150
+ -- 42501: Must surface this error - caller lacks permission
151
+ RAISE;
152
+ END;
153
+ END IF;
154
+
155
+ -- Grant anonymous to user
156
+ IF NOT EXISTS (
157
+ SELECT 1 FROM pg_auth_members am
158
+ JOIN pg_roles r1 ON am.roleid = r1.oid
159
+ JOIN pg_roles r2 ON am.member = r2.oid
160
+ WHERE r1.rolname = v_anonymous AND r2.rolname = v_username
161
+ ) THEN
162
+ BEGIN
163
+ EXECUTE format('GRANT %I TO %I', v_anonymous, v_username);
164
+ EXCEPTION
165
+ WHEN unique_violation THEN
166
+ -- 23505: Membership was granted concurrently; safe to ignore
167
+ NULL;
168
+ WHEN undefined_object THEN
169
+ -- 42704: One of the roles doesn't exist; log notice and continue
170
+ RAISE NOTICE 'Missing role when granting % to %', v_anonymous, v_username;
171
+ WHEN insufficient_privilege THEN
172
+ -- 42501: Must surface this error - caller lacks permission
173
+ RAISE;
174
+ WHEN invalid_grant_operation THEN
175
+ -- 0LP01: Must surface this error - invalid grant operation
176
+ RAISE;
177
+ END;
178
+ END IF;
179
+
180
+ -- Grant authenticated to user
181
+ IF NOT EXISTS (
182
+ SELECT 1 FROM pg_auth_members am
183
+ JOIN pg_roles r1 ON am.roleid = r1.oid
184
+ JOIN pg_roles r2 ON am.member = r2.oid
185
+ WHERE r1.rolname = v_authenticated AND r2.rolname = v_username
186
+ ) THEN
187
+ BEGIN
188
+ EXECUTE format('GRANT %I TO %I', v_authenticated, v_username);
189
+ EXCEPTION
190
+ WHEN unique_violation THEN
191
+ -- 23505: Membership was granted concurrently; safe to ignore
192
+ NULL;
193
+ WHEN undefined_object THEN
194
+ -- 42704: One of the roles doesn't exist; log notice and continue
195
+ RAISE NOTICE 'Missing role when granting % to %', v_authenticated, v_username;
196
+ WHEN insufficient_privilege THEN
197
+ -- 42501: Must surface this error - caller lacks permission
198
+ RAISE;
199
+ WHEN invalid_grant_operation THEN
200
+ -- 0LP01: Must surface this error - invalid grant operation
201
+ RAISE;
202
+ END;
203
+ END IF;
204
+ END
205
+ $do$;
206
+ COMMIT;
207
+ `;
208
+ }
209
+ /**
210
+ * Generate SQL to create test users with grants to base roles.
211
+ * Callers should use getConnEnvOptions() from @pgpmjs/env to get merged values.
212
+ * @param roles - Role mapping from getConnEnvOptions().roles!
213
+ * @param connections - Test user credentials from getConnEnvOptions().connections!
214
+ */
215
+ function generateCreateTestUsersSQL(roles, connections) {
216
+ if (!roles) {
217
+ throw new Error('generateCreateTestUsersSQL: roles parameter is undefined. ' +
218
+ 'Ensure getConnEnvOptions().roles is defined.');
219
+ }
220
+ if (!roles.anonymous || !roles.authenticated || !roles.administrator) {
221
+ throw new Error('generateCreateTestUsersSQL: roles is missing required properties. ' +
222
+ `Got: anonymous=${roles.anonymous}, authenticated=${roles.authenticated}, administrator=${roles.administrator}.`);
223
+ }
224
+ if (!connections) {
225
+ throw new Error('generateCreateTestUsersSQL: connections parameter is undefined. ' +
226
+ 'Ensure getConnEnvOptions().connections is defined.');
227
+ }
228
+ if (!connections.app?.user || !connections.app?.password || !connections.admin?.user || !connections.admin?.password) {
229
+ throw new Error('generateCreateTestUsersSQL: connections is missing required properties. ' +
230
+ 'Ensure app.user, app.password, admin.user, and admin.password are defined.');
231
+ }
232
+ const r = {
233
+ anonymous: roles.anonymous,
234
+ authenticated: roles.authenticated,
235
+ administrator: roles.administrator
236
+ };
237
+ const users = {
238
+ app: { user: connections.app.user, password: connections.app.password },
239
+ admin: { user: connections.admin.user, password: connections.admin.password }
240
+ };
241
+ return `
242
+ BEGIN;
243
+ DO $do$
244
+ DECLARE
245
+ v_app_user text := ${sqlLiteral(users.app.user)};
246
+ v_app_user_password text := ${sqlLiteral(users.app.password)};
247
+ v_app_admin text := ${sqlLiteral(users.admin.user)};
248
+ v_app_admin_password text := ${sqlLiteral(users.admin.password)};
249
+ v_anonymous text := ${sqlLiteral(r.anonymous)};
250
+ v_authenticated text := ${sqlLiteral(r.authenticated)};
251
+ v_administrator text := ${sqlLiteral(r.administrator)};
252
+ BEGIN
253
+ -- Create app_user: pre-check + exception handling for TOCTOU safety
254
+ IF NOT EXISTS (SELECT 1 FROM pg_catalog.pg_roles WHERE rolname = v_app_user) THEN
255
+ BEGIN
256
+ EXECUTE format('CREATE ROLE %I LOGIN PASSWORD %L', v_app_user, v_app_user_password);
257
+ EXCEPTION
258
+ WHEN duplicate_object THEN NULL;
259
+ WHEN unique_violation THEN NULL;
260
+ WHEN insufficient_privilege THEN RAISE;
261
+ END;
262
+ END IF;
263
+
264
+ -- Create app_admin: pre-check + exception handling for TOCTOU safety
265
+ IF NOT EXISTS (SELECT 1 FROM pg_catalog.pg_roles WHERE rolname = v_app_admin) THEN
266
+ BEGIN
267
+ EXECUTE format('CREATE ROLE %I LOGIN PASSWORD %L', v_app_admin, v_app_admin_password);
268
+ EXCEPTION
269
+ WHEN duplicate_object THEN NULL;
270
+ WHEN unique_violation THEN NULL;
271
+ WHEN insufficient_privilege THEN RAISE;
272
+ END;
273
+ END IF;
274
+
275
+ -- Grant anonymous to app_user
276
+ IF NOT EXISTS (
277
+ SELECT 1 FROM pg_auth_members am
278
+ JOIN pg_roles r1 ON am.roleid = r1.oid
279
+ JOIN pg_roles r2 ON am.member = r2.oid
280
+ WHERE r1.rolname = v_anonymous AND r2.rolname = v_app_user
281
+ ) THEN
282
+ BEGIN
283
+ EXECUTE format('GRANT %I TO %I', v_anonymous, v_app_user);
284
+ EXCEPTION
285
+ WHEN unique_violation THEN NULL;
286
+ WHEN undefined_object THEN RAISE NOTICE 'Missing role when granting % to %', v_anonymous, v_app_user;
287
+ WHEN insufficient_privilege THEN RAISE;
288
+ WHEN invalid_grant_operation THEN RAISE;
289
+ END;
290
+ END IF;
291
+
292
+ -- Grant authenticated to app_user
293
+ IF NOT EXISTS (
294
+ SELECT 1 FROM pg_auth_members am
295
+ JOIN pg_roles r1 ON am.roleid = r1.oid
296
+ JOIN pg_roles r2 ON am.member = r2.oid
297
+ WHERE r1.rolname = v_authenticated AND r2.rolname = v_app_user
298
+ ) THEN
299
+ BEGIN
300
+ EXECUTE format('GRANT %I TO %I', v_authenticated, v_app_user);
301
+ EXCEPTION
302
+ WHEN unique_violation THEN NULL;
303
+ WHEN undefined_object THEN RAISE NOTICE 'Missing role when granting % to %', v_authenticated, v_app_user;
304
+ WHEN insufficient_privilege THEN RAISE;
305
+ WHEN invalid_grant_operation THEN RAISE;
306
+ END;
307
+ END IF;
308
+
309
+ -- Grant anonymous to administrator
310
+ IF NOT EXISTS (
311
+ SELECT 1 FROM pg_auth_members am
312
+ JOIN pg_roles r1 ON am.roleid = r1.oid
313
+ JOIN pg_roles r2 ON am.member = r2.oid
314
+ WHERE r1.rolname = v_anonymous AND r2.rolname = v_administrator
315
+ ) THEN
316
+ BEGIN
317
+ EXECUTE format('GRANT %I TO %I', v_anonymous, v_administrator);
318
+ EXCEPTION
319
+ WHEN unique_violation THEN NULL;
320
+ WHEN undefined_object THEN RAISE NOTICE 'Missing role when granting % to %', v_anonymous, v_administrator;
321
+ WHEN insufficient_privilege THEN RAISE;
322
+ WHEN invalid_grant_operation THEN RAISE;
323
+ END;
324
+ END IF;
325
+
326
+ -- Grant authenticated to administrator
327
+ IF NOT EXISTS (
328
+ SELECT 1 FROM pg_auth_members am
329
+ JOIN pg_roles r1 ON am.roleid = r1.oid
330
+ JOIN pg_roles r2 ON am.member = r2.oid
331
+ WHERE r1.rolname = v_authenticated AND r2.rolname = v_administrator
332
+ ) THEN
333
+ BEGIN
334
+ EXECUTE format('GRANT %I TO %I', v_authenticated, v_administrator);
335
+ EXCEPTION
336
+ WHEN unique_violation THEN NULL;
337
+ WHEN undefined_object THEN RAISE NOTICE 'Missing role when granting % to %', v_authenticated, v_administrator;
338
+ WHEN insufficient_privilege THEN RAISE;
339
+ WHEN invalid_grant_operation THEN RAISE;
340
+ END;
341
+ END IF;
342
+
343
+ -- Grant administrator to app_admin
344
+ IF NOT EXISTS (
345
+ SELECT 1 FROM pg_auth_members am
346
+ JOIN pg_roles r1 ON am.roleid = r1.oid
347
+ JOIN pg_roles r2 ON am.member = r2.oid
348
+ WHERE r1.rolname = v_administrator AND r2.rolname = v_app_admin
349
+ ) THEN
350
+ BEGIN
351
+ EXECUTE format('GRANT %I TO %I', v_administrator, v_app_admin);
352
+ EXCEPTION
353
+ WHEN unique_violation THEN NULL;
354
+ WHEN undefined_object THEN RAISE NOTICE 'Missing role when granting % to %', v_administrator, v_app_admin;
355
+ WHEN insufficient_privilege THEN RAISE;
356
+ WHEN invalid_grant_operation THEN RAISE;
357
+ END;
358
+ END IF;
359
+ END
360
+ $do$;
361
+ COMMIT;
362
+ `;
363
+ }
364
+ /**
365
+ * Generate SQL to grant a role to a user
366
+ */
367
+ function generateGrantRoleSQL(role, user) {
368
+ return `
369
+ DO $$
370
+ DECLARE
371
+ v_user text := ${sqlLiteral(user)};
372
+ v_role text := ${sqlLiteral(role)};
373
+ BEGIN
374
+ -- Pre-check to avoid unnecessary GRANTs; still catch TOCTOU under concurrency
375
+ IF NOT EXISTS (
376
+ SELECT 1 FROM pg_auth_members am
377
+ JOIN pg_roles r1 ON am.roleid = r1.oid
378
+ JOIN pg_roles r2 ON am.member = r2.oid
379
+ WHERE r1.rolname = v_role AND r2.rolname = v_user
380
+ ) THEN
381
+ BEGIN
382
+ EXECUTE format('GRANT %I TO %I', v_role, v_user);
383
+ EXCEPTION
384
+ WHEN unique_violation THEN
385
+ -- 23505: Concurrent membership grant; safe to ignore
386
+ NULL;
387
+ WHEN undefined_object THEN
388
+ -- 42704: Role or user missing; emit notice and continue
389
+ RAISE NOTICE 'Missing role when granting % to %', v_role, v_user;
390
+ WHEN insufficient_privilege THEN
391
+ -- 42501: Must surface this error - caller lacks permission
392
+ RAISE;
393
+ WHEN invalid_grant_operation THEN
394
+ -- 0LP01: Must surface this error - invalid grant operation
395
+ RAISE;
396
+ END;
397
+ END IF;
398
+ END
399
+ $$;
400
+ `;
401
+ }
402
+ /**
403
+ * Generate SQL to remove a user and revoke grants.
404
+ * Callers should use getConnEnvOptions() from @pgpmjs/env to get merged values.
405
+ * @param roles - Role mapping from getConnEnvOptions().roles!
406
+ * @param useLocksForRoles - Whether to use advisory locks (from getConnEnvOptions().useLocksForRoles)
407
+ */
408
+ function generateRemoveUserSQL(username, roles, useLocksForRoles = false) {
409
+ if (!roles) {
410
+ throw new Error('generateRemoveUserSQL: roles parameter is undefined. ' +
411
+ 'Ensure getConnEnvOptions().roles is defined.');
412
+ }
413
+ if (!roles.anonymous || !roles.authenticated) {
414
+ throw new Error('generateRemoveUserSQL: roles is missing required properties. ' +
415
+ `Got: anonymous=${roles.anonymous}, authenticated=${roles.authenticated}.`);
416
+ }
417
+ const r = {
418
+ anonymous: roles.anonymous,
419
+ authenticated: roles.authenticated
420
+ };
421
+ const lockStatement = useLocksForRoles
422
+ ? `PERFORM pg_advisory_xact_lock(42, hashtext(v_username));`
423
+ : '';
424
+ return `
425
+ BEGIN;
426
+ DO $do$
427
+ DECLARE
428
+ v_username text := ${sqlLiteral(username)};
429
+ v_anonymous text := ${sqlLiteral(r.anonymous)};
430
+ v_authenticated text := ${sqlLiteral(r.authenticated)};
431
+ BEGIN
432
+ ${lockStatement}
433
+ IF EXISTS (
434
+ SELECT 1
435
+ FROM pg_catalog.pg_roles
436
+ WHERE rolname = v_username
437
+ ) THEN
438
+ -- REVOKE anonymous membership
439
+ BEGIN
440
+ EXECUTE format('REVOKE %I FROM %I', v_anonymous, v_username);
441
+ EXCEPTION
442
+ WHEN undefined_object THEN
443
+ -- 42704: Role doesn't exist; safe to ignore
444
+ NULL;
445
+ WHEN insufficient_privilege THEN
446
+ -- 42501: Must surface this error - caller lacks permission
447
+ RAISE;
448
+ END;
449
+
450
+ -- REVOKE authenticated membership
451
+ BEGIN
452
+ EXECUTE format('REVOKE %I FROM %I', v_authenticated, v_username);
453
+ EXCEPTION
454
+ WHEN undefined_object THEN
455
+ -- 42704: Role doesn't exist; safe to ignore
456
+ NULL;
457
+ WHEN insufficient_privilege THEN
458
+ -- 42501: Must surface this error - caller lacks permission
459
+ RAISE;
460
+ END;
461
+
462
+ -- DROP ROLE
463
+ BEGIN
464
+ EXECUTE format('DROP ROLE %I', v_username);
465
+ EXCEPTION
466
+ WHEN undefined_object THEN
467
+ -- 42704: Role doesn't exist; safe to ignore
468
+ NULL;
469
+ WHEN dependent_objects_still_exist THEN
470
+ -- 2BP01: Must surface this error - role has dependent objects
471
+ RAISE;
472
+ WHEN object_in_use THEN
473
+ -- 55006: Must surface this error - role is in use
474
+ RAISE;
475
+ WHEN insufficient_privilege THEN
476
+ -- 42501: Must surface this error - caller lacks permission
477
+ RAISE;
478
+ END;
479
+ END IF;
480
+ END
481
+ $do$;
482
+ COMMIT;
483
+ `;
484
+ }
485
+ /**
486
+ * Generate SQL to create a user with grants to specified roles (for test harness)
487
+ * @param useLocksForRoles - Whether to use advisory locks (from getConnEnvOptions().useLocksForRoles)
488
+ */
489
+ function generateCreateUserWithGrantsSQL(username, password, rolesToGrant, useLocksForRoles = false) {
490
+ const lockStatement = useLocksForRoles
491
+ ? `PERFORM pg_advisory_xact_lock(42, hashtext(v_user));`
492
+ : '';
493
+ // Generate variable declarations for all roles
494
+ const roleVarDeclarations = rolesToGrant.map((role, i) => ` v_role_${i} text := ${sqlLiteral(role)};`).join('\n');
495
+ // Generate grant blocks using the variables
496
+ const grantBlocks = rolesToGrant.map((_, i) => `
497
+ -- Grant role ${i}
498
+ IF NOT EXISTS (
499
+ SELECT 1 FROM pg_auth_members am
500
+ JOIN pg_roles r1 ON am.roleid = r1.oid
501
+ JOIN pg_roles r2 ON am.member = r2.oid
502
+ WHERE r1.rolname = v_role_${i} AND r2.rolname = v_user
503
+ ) THEN
504
+ BEGIN
505
+ EXECUTE format('GRANT %I TO %I', v_role_${i}, v_user);
506
+ EXCEPTION
507
+ WHEN unique_violation THEN NULL;
508
+ WHEN undefined_object THEN RAISE NOTICE 'Missing role when granting % to %', v_role_${i}, v_user;
509
+ WHEN insufficient_privilege THEN RAISE;
510
+ WHEN invalid_grant_operation THEN RAISE;
511
+ END;
512
+ END IF;`).join('\n');
513
+ return `
514
+ DO $$
515
+ DECLARE
516
+ v_user text := ${sqlLiteral(username)};
517
+ v_password text := ${sqlLiteral(password)};
518
+ ${roleVarDeclarations}
519
+ BEGIN
520
+ ${lockStatement}
521
+ -- Create role: pre-check + exception handling for TOCTOU safety
522
+ IF NOT EXISTS (SELECT 1 FROM pg_catalog.pg_roles WHERE rolname = v_user) THEN
523
+ BEGIN
524
+ EXECUTE format('CREATE ROLE %I LOGIN PASSWORD %L', v_user, v_password);
525
+ EXCEPTION
526
+ WHEN duplicate_object THEN
527
+ -- 42710: Role already exists (race condition); safe to ignore
528
+ NULL;
529
+ WHEN unique_violation THEN
530
+ -- 23505: Concurrent CREATE ROLE hit unique index; safe to ignore
531
+ NULL;
532
+ WHEN insufficient_privilege THEN
533
+ -- 42501: Must surface this error - caller lacks permission
534
+ RAISE;
535
+ END;
536
+ END IF;
537
+ ${grantBlocks}
538
+ END $$;
539
+ `;
540
+ }
@@ -1,55 +0,0 @@
1
- BEGIN;
2
- DO $do$
3
- BEGIN
4
- -- anonymous
5
- BEGIN
6
- EXECUTE format('CREATE ROLE %I', 'anonymous');
7
- EXCEPTION
8
- WHEN duplicate_object THEN
9
- -- Role already exists; optionally sync attributes here with ALTER ROLE
10
- NULL;
11
- END;
12
-
13
- -- authenticated
14
- BEGIN
15
- EXECUTE format('CREATE ROLE %I', 'authenticated');
16
- EXCEPTION
17
- WHEN duplicate_object THEN
18
- -- Role already exists; optionally sync attributes here with ALTER ROLE
19
- NULL;
20
- END;
21
-
22
- -- administrator
23
- BEGIN
24
- EXECUTE format('CREATE ROLE %I', 'administrator');
25
- EXCEPTION
26
- WHEN duplicate_object THEN
27
- -- Role already exists; optionally sync attributes here with ALTER ROLE
28
- NULL;
29
- END;
30
- END
31
- $do$;
32
-
33
- -- Set role attributes (safe to run even if role already exists)
34
- ALTER USER anonymous WITH NOCREATEDB;
35
- ALTER USER anonymous WITH NOSUPERUSER;
36
- ALTER USER anonymous WITH NOCREATEROLE;
37
- ALTER USER anonymous WITH NOLOGIN;
38
- ALTER USER anonymous WITH NOREPLICATION;
39
- ALTER USER anonymous WITH NOBYPASSRLS;
40
-
41
- ALTER USER authenticated WITH NOCREATEDB;
42
- ALTER USER authenticated WITH NOSUPERUSER;
43
- ALTER USER authenticated WITH NOCREATEROLE;
44
- ALTER USER authenticated WITH NOLOGIN;
45
- ALTER USER authenticated WITH NOREPLICATION;
46
- ALTER USER authenticated WITH NOBYPASSRLS;
47
-
48
- ALTER USER administrator WITH NOCREATEDB;
49
- ALTER USER administrator WITH NOSUPERUSER;
50
- ALTER USER administrator WITH NOCREATEROLE;
51
- ALTER USER administrator WITH NOLOGIN;
52
- ALTER USER administrator WITH NOREPLICATION;
53
- -- they CAN bypass RLS
54
- ALTER USER administrator WITH BYPASSRLS;
55
- COMMIT;
@@ -1,72 +0,0 @@
1
- BEGIN;
2
- DO $do$
3
- BEGIN
4
- BEGIN
5
- EXECUTE format('CREATE ROLE %I LOGIN PASSWORD %L', 'app_user', 'app_password');
6
- EXCEPTION
7
- WHEN duplicate_object THEN
8
- -- Role already exists; optionally sync attributes here with ALTER ROLE
9
- NULL;
10
- END;
11
-
12
- BEGIN
13
- EXECUTE format('CREATE ROLE %I LOGIN PASSWORD %L', 'app_admin', 'admin_password');
14
- EXCEPTION
15
- WHEN duplicate_object THEN
16
- -- Role already exists; optionally sync attributes here with ALTER ROLE
17
- NULL;
18
- END;
19
- END
20
- $do$;
21
-
22
- DO $do$
23
- BEGIN
24
- BEGIN
25
- EXECUTE format('GRANT %I TO %I', 'anonymous', 'app_user');
26
- EXCEPTION
27
- WHEN unique_violation THEN
28
- -- Membership was granted concurrently; ignore.
29
- NULL;
30
- WHEN undefined_object THEN
31
- -- One of the roles doesn't exist yet; order operations as needed.
32
- RAISE NOTICE 'Missing role when granting % to %', 'anonymous', 'app_user';
33
- END;
34
-
35
- BEGIN
36
- EXECUTE format('GRANT %I TO %I', 'authenticated', 'app_user');
37
- EXCEPTION
38
- WHEN unique_violation THEN
39
- NULL;
40
- WHEN undefined_object THEN
41
- RAISE NOTICE 'Missing role when granting % to %', 'authenticated', 'app_user';
42
- END;
43
-
44
- BEGIN
45
- EXECUTE format('GRANT %I TO %I', 'anonymous', 'administrator');
46
- EXCEPTION
47
- WHEN unique_violation THEN
48
- NULL;
49
- WHEN undefined_object THEN
50
- RAISE NOTICE 'Missing role when granting % to %', 'anonymous', 'administrator';
51
- END;
52
-
53
- BEGIN
54
- EXECUTE format('GRANT %I TO %I', 'authenticated', 'administrator');
55
- EXCEPTION
56
- WHEN unique_violation THEN
57
- NULL;
58
- WHEN undefined_object THEN
59
- RAISE NOTICE 'Missing role when granting % to %', 'authenticated', 'administrator';
60
- END;
61
-
62
- BEGIN
63
- EXECUTE format('GRANT %I TO %I', 'administrator', 'app_admin');
64
- EXCEPTION
65
- WHEN unique_violation THEN
66
- NULL;
67
- WHEN undefined_object THEN
68
- RAISE NOTICE 'Missing role when granting % to %', 'administrator', 'app_admin';
69
- END;
70
- END
71
- $do$;
72
- COMMIT;