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