@pgpmjs/core 6.26.1 → 6.27.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/init/client.js +19 -1
- package/esm/roles/index.js +87 -0
- package/init/client.d.ts +7 -0
- package/init/client.js +18 -0
- package/package.json +6 -6
- package/roles/index.d.ts +14 -0
- package/roles/index.js +88 -0
package/esm/init/client.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Logger } from '@pgpmjs/logger';
|
|
2
2
|
import { getPgPool } from 'pg-cache';
|
|
3
|
-
import { generateCreateBaseRolesSQL, generateCreateUserSQL, generateCreateTestUsersSQL, generateRemoveUserSQL } from '../roles';
|
|
3
|
+
import { generateCreateBaseRolesSQL, generateCreateClientRoleSQL, generateCreateUserSQL, generateCreateTestUsersSQL, generateRemoveUserSQL } from '../roles';
|
|
4
4
|
const log = new Logger('init');
|
|
5
5
|
export class PgpmInit {
|
|
6
6
|
pool;
|
|
@@ -26,6 +26,24 @@ export class PgpmInit {
|
|
|
26
26
|
throw error;
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Bootstrap the restricted client role (authenticated_client) for SQL-level
|
|
31
|
+
* proxy clients. Opt-in via `pgpm admin-users bootstrap --client`.
|
|
32
|
+
* Callers should use getConnEnvOptions() from @pgpmjs/env to get merged values.
|
|
33
|
+
* @param roles - Role mapping from getConnEnvOptions().roles!
|
|
34
|
+
*/
|
|
35
|
+
async bootstrapClientRole(roles) {
|
|
36
|
+
try {
|
|
37
|
+
log.info('Bootstrapping PGPM client role...');
|
|
38
|
+
const sql = generateCreateClientRoleSQL(roles);
|
|
39
|
+
await this.pool.query(sql);
|
|
40
|
+
log.success('Successfully bootstrapped PGPM client role');
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
log.error('Failed to bootstrap client role:', error);
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
29
47
|
/**
|
|
30
48
|
* Bootstrap test roles (app_user, app_admin with grants to base roles).
|
|
31
49
|
* Callers should use getConnEnvOptions() from @pgpmjs/env to get merged values.
|
package/esm/roles/index.js
CHANGED
|
@@ -95,6 +95,93 @@ $do$;
|
|
|
95
95
|
COMMIT;
|
|
96
96
|
`;
|
|
97
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* Generate SQL to create the restricted `authenticated_client` role used by
|
|
100
|
+
* SQL-level proxy clients (opt-in via `admin-users bootstrap --client`).
|
|
101
|
+
*
|
|
102
|
+
* The role inherits table/schema grants from `authenticated` and gets a
|
|
103
|
+
* server-enforced statement_timeout baseline. Grant-level hardening such as
|
|
104
|
+
* revoking set_config()/pg_notify() from PUBLIC is intentionally left to the
|
|
105
|
+
* proxy deployment config, since those revokes affect every non-superuser
|
|
106
|
+
* role in the database (the app server and job workers depend on both).
|
|
107
|
+
*
|
|
108
|
+
* @param roles - Role mapping from getConnEnvOptions().roles!
|
|
109
|
+
* @param statementTimeout - Baseline statement_timeout for the role (default '15s')
|
|
110
|
+
*/
|
|
111
|
+
export function generateCreateClientRoleSQL(roles, statementTimeout = '15s') {
|
|
112
|
+
if (!roles) {
|
|
113
|
+
throw new Error('generateCreateClientRoleSQL: roles parameter is undefined. ' +
|
|
114
|
+
'Ensure getConnEnvOptions().roles is defined.');
|
|
115
|
+
}
|
|
116
|
+
if (!roles.authenticated || !roles.authenticatedClient) {
|
|
117
|
+
throw new Error('generateCreateClientRoleSQL: roles is missing required properties. ' +
|
|
118
|
+
`Got: authenticated=${roles.authenticated}, authenticatedClient=${roles.authenticatedClient}. ` +
|
|
119
|
+
'Ensure all role names are defined in your configuration.');
|
|
120
|
+
}
|
|
121
|
+
const r = {
|
|
122
|
+
authenticated: roles.authenticated,
|
|
123
|
+
authenticatedClient: roles.authenticatedClient
|
|
124
|
+
};
|
|
125
|
+
return `
|
|
126
|
+
BEGIN;
|
|
127
|
+
DO $do$
|
|
128
|
+
DECLARE
|
|
129
|
+
v_authenticated text := ${sqlLiteral(r.authenticated)};
|
|
130
|
+
v_client text := ${sqlLiteral(r.authenticatedClient)};
|
|
131
|
+
v_statement_timeout text := ${sqlLiteral(statementTimeout)};
|
|
132
|
+
BEGIN
|
|
133
|
+
-- Create client role: pre-check + exception handling for TOCTOU safety
|
|
134
|
+
IF NOT EXISTS (SELECT 1 FROM pg_catalog.pg_roles WHERE rolname = v_client) THEN
|
|
135
|
+
BEGIN
|
|
136
|
+
EXECUTE format('CREATE ROLE %I', v_client);
|
|
137
|
+
EXCEPTION
|
|
138
|
+
WHEN duplicate_object THEN
|
|
139
|
+
-- 42710: Role already exists (race condition); safe to ignore
|
|
140
|
+
NULL;
|
|
141
|
+
WHEN unique_violation THEN
|
|
142
|
+
-- 23505: Concurrent CREATE ROLE hit unique index; safe to ignore
|
|
143
|
+
NULL;
|
|
144
|
+
WHEN insufficient_privilege THEN
|
|
145
|
+
-- 42501: Must surface this error - caller lacks permission
|
|
146
|
+
RAISE;
|
|
147
|
+
END;
|
|
148
|
+
END IF;
|
|
149
|
+
|
|
150
|
+
-- Strip escalation attributes (safe to run even if role already exists)
|
|
151
|
+
EXECUTE format('ALTER ROLE %I WITH NOCREATEDB NOSUPERUSER NOCREATEROLE NOLOGIN NOREPLICATION NOBYPASSRLS', v_client);
|
|
152
|
+
|
|
153
|
+
-- Inherit table/schema grants from authenticated
|
|
154
|
+
IF NOT EXISTS (
|
|
155
|
+
SELECT 1 FROM pg_auth_members am
|
|
156
|
+
JOIN pg_roles r1 ON am.roleid = r1.oid
|
|
157
|
+
JOIN pg_roles r2 ON am.member = r2.oid
|
|
158
|
+
WHERE r1.rolname = v_authenticated AND r2.rolname = v_client
|
|
159
|
+
) THEN
|
|
160
|
+
BEGIN
|
|
161
|
+
EXECUTE format('GRANT %I TO %I', v_authenticated, v_client);
|
|
162
|
+
EXCEPTION
|
|
163
|
+
WHEN unique_violation THEN
|
|
164
|
+
-- 23505: Membership was granted concurrently; safe to ignore
|
|
165
|
+
NULL;
|
|
166
|
+
WHEN undefined_object THEN
|
|
167
|
+
-- 42704: One of the roles doesn't exist; log notice and continue
|
|
168
|
+
RAISE NOTICE 'Missing role when granting % to %', v_authenticated, v_client;
|
|
169
|
+
WHEN insufficient_privilege THEN
|
|
170
|
+
-- 42501: Must surface this error - caller lacks permission
|
|
171
|
+
RAISE;
|
|
172
|
+
WHEN invalid_grant_operation THEN
|
|
173
|
+
-- 0LP01: Must surface this error - invalid grant operation
|
|
174
|
+
RAISE;
|
|
175
|
+
END;
|
|
176
|
+
END IF;
|
|
177
|
+
|
|
178
|
+
-- Server-enforced baseline statement timeout for the client role
|
|
179
|
+
EXECUTE format('ALTER ROLE %I SET statement_timeout = %L', v_client, v_statement_timeout);
|
|
180
|
+
END
|
|
181
|
+
$do$;
|
|
182
|
+
COMMIT;
|
|
183
|
+
`;
|
|
184
|
+
}
|
|
98
185
|
/**
|
|
99
186
|
* Generate SQL to create a user with password and grant base roles.
|
|
100
187
|
* Callers should use getConnEnvOptions() from @pgpmjs/env to get merged values.
|
package/init/client.d.ts
CHANGED
|
@@ -10,6 +10,13 @@ export declare class PgpmInit {
|
|
|
10
10
|
* @param roles - Role mapping from getConnEnvOptions().roles!
|
|
11
11
|
*/
|
|
12
12
|
bootstrapRoles(roles: RoleMapping): Promise<void>;
|
|
13
|
+
/**
|
|
14
|
+
* Bootstrap the restricted client role (authenticated_client) for SQL-level
|
|
15
|
+
* proxy clients. Opt-in via `pgpm admin-users bootstrap --client`.
|
|
16
|
+
* Callers should use getConnEnvOptions() from @pgpmjs/env to get merged values.
|
|
17
|
+
* @param roles - Role mapping from getConnEnvOptions().roles!
|
|
18
|
+
*/
|
|
19
|
+
bootstrapClientRole(roles: RoleMapping): Promise<void>;
|
|
13
20
|
/**
|
|
14
21
|
* Bootstrap test roles (app_user, app_admin with grants to base roles).
|
|
15
22
|
* Callers should use getConnEnvOptions() from @pgpmjs/env to get merged values.
|
package/init/client.js
CHANGED
|
@@ -29,6 +29,24 @@ class PgpmInit {
|
|
|
29
29
|
throw error;
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Bootstrap the restricted client role (authenticated_client) for SQL-level
|
|
34
|
+
* proxy clients. Opt-in via `pgpm admin-users bootstrap --client`.
|
|
35
|
+
* Callers should use getConnEnvOptions() from @pgpmjs/env to get merged values.
|
|
36
|
+
* @param roles - Role mapping from getConnEnvOptions().roles!
|
|
37
|
+
*/
|
|
38
|
+
async bootstrapClientRole(roles) {
|
|
39
|
+
try {
|
|
40
|
+
log.info('Bootstrapping PGPM client role...');
|
|
41
|
+
const sql = (0, roles_1.generateCreateClientRoleSQL)(roles);
|
|
42
|
+
await this.pool.query(sql);
|
|
43
|
+
log.success('Successfully bootstrapped PGPM client role');
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
log.error('Failed to bootstrap client role:', error);
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
32
50
|
/**
|
|
33
51
|
* Bootstrap test roles (app_user, app_admin with grants to base roles).
|
|
34
52
|
* Callers should use getConnEnvOptions() from @pgpmjs/env to get merged values.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pgpmjs/core",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.27.1",
|
|
4
4
|
"author": "Constructive <developers@constructive.io>",
|
|
5
5
|
"description": "PGPM Package and Migration Tools",
|
|
6
6
|
"main": "index.js",
|
|
@@ -48,21 +48,21 @@
|
|
|
48
48
|
"makage": "^0.3.0"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@pgpmjs/env": "^2.26.
|
|
51
|
+
"@pgpmjs/env": "^2.26.1",
|
|
52
52
|
"@pgpmjs/logger": "^2.13.0",
|
|
53
|
-
"@pgpmjs/server-utils": "^3.14.
|
|
54
|
-
"@pgpmjs/types": "^2.
|
|
53
|
+
"@pgpmjs/server-utils": "^3.14.1",
|
|
54
|
+
"@pgpmjs/types": "^2.31.0",
|
|
55
55
|
"csv-to-pg": "^3.20.1",
|
|
56
56
|
"genomic": "^5.6.2",
|
|
57
57
|
"glob": "^13.0.6",
|
|
58
58
|
"minimatch": "^10.2.5",
|
|
59
59
|
"parse-package-name": "^1.0.0",
|
|
60
60
|
"pg": "^8.21.0",
|
|
61
|
-
"pg-cache": "^3.
|
|
61
|
+
"pg-cache": "^3.14.0",
|
|
62
62
|
"pg-env": "^1.17.0",
|
|
63
63
|
"pgsql-deparser": "^17.18.3",
|
|
64
64
|
"pgsql-parser": "^17.9.15",
|
|
65
65
|
"yanse": "^0.2.1"
|
|
66
66
|
},
|
|
67
|
-
"gitHead": "
|
|
67
|
+
"gitHead": "7cbe15fac21a9345b169203ba9542ba9a50c866c"
|
|
68
68
|
}
|
package/roles/index.d.ts
CHANGED
|
@@ -6,6 +6,20 @@ import { RoleMapping, TestUserCredentials } from '@pgpmjs/types';
|
|
|
6
6
|
* @throws Error if roles is undefined or missing required properties
|
|
7
7
|
*/
|
|
8
8
|
export declare function generateCreateBaseRolesSQL(roles: RoleMapping): string;
|
|
9
|
+
/**
|
|
10
|
+
* Generate SQL to create the restricted `authenticated_client` role used by
|
|
11
|
+
* SQL-level proxy clients (opt-in via `admin-users bootstrap --client`).
|
|
12
|
+
*
|
|
13
|
+
* The role inherits table/schema grants from `authenticated` and gets a
|
|
14
|
+
* server-enforced statement_timeout baseline. Grant-level hardening such as
|
|
15
|
+
* revoking set_config()/pg_notify() from PUBLIC is intentionally left to the
|
|
16
|
+
* proxy deployment config, since those revokes affect every non-superuser
|
|
17
|
+
* role in the database (the app server and job workers depend on both).
|
|
18
|
+
*
|
|
19
|
+
* @param roles - Role mapping from getConnEnvOptions().roles!
|
|
20
|
+
* @param statementTimeout - Baseline statement_timeout for the role (default '15s')
|
|
21
|
+
*/
|
|
22
|
+
export declare function generateCreateClientRoleSQL(roles: RoleMapping, statementTimeout?: string): string;
|
|
9
23
|
/**
|
|
10
24
|
* Generate SQL to create a user with password and grant base roles.
|
|
11
25
|
* Callers should use getConnEnvOptions() from @pgpmjs/env to get merged values.
|
package/roles/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.generateCreateBaseRolesSQL = generateCreateBaseRolesSQL;
|
|
4
|
+
exports.generateCreateClientRoleSQL = generateCreateClientRoleSQL;
|
|
4
5
|
exports.generateCreateUserSQL = generateCreateUserSQL;
|
|
5
6
|
exports.generateCreateTestUsersSQL = generateCreateTestUsersSQL;
|
|
6
7
|
exports.generateGrantRoleSQL = generateGrantRoleSQL;
|
|
@@ -103,6 +104,93 @@ $do$;
|
|
|
103
104
|
COMMIT;
|
|
104
105
|
`;
|
|
105
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* Generate SQL to create the restricted `authenticated_client` role used by
|
|
109
|
+
* SQL-level proxy clients (opt-in via `admin-users bootstrap --client`).
|
|
110
|
+
*
|
|
111
|
+
* The role inherits table/schema grants from `authenticated` and gets a
|
|
112
|
+
* server-enforced statement_timeout baseline. Grant-level hardening such as
|
|
113
|
+
* revoking set_config()/pg_notify() from PUBLIC is intentionally left to the
|
|
114
|
+
* proxy deployment config, since those revokes affect every non-superuser
|
|
115
|
+
* role in the database (the app server and job workers depend on both).
|
|
116
|
+
*
|
|
117
|
+
* @param roles - Role mapping from getConnEnvOptions().roles!
|
|
118
|
+
* @param statementTimeout - Baseline statement_timeout for the role (default '15s')
|
|
119
|
+
*/
|
|
120
|
+
function generateCreateClientRoleSQL(roles, statementTimeout = '15s') {
|
|
121
|
+
if (!roles) {
|
|
122
|
+
throw new Error('generateCreateClientRoleSQL: roles parameter is undefined. ' +
|
|
123
|
+
'Ensure getConnEnvOptions().roles is defined.');
|
|
124
|
+
}
|
|
125
|
+
if (!roles.authenticated || !roles.authenticatedClient) {
|
|
126
|
+
throw new Error('generateCreateClientRoleSQL: roles is missing required properties. ' +
|
|
127
|
+
`Got: authenticated=${roles.authenticated}, authenticatedClient=${roles.authenticatedClient}. ` +
|
|
128
|
+
'Ensure all role names are defined in your configuration.');
|
|
129
|
+
}
|
|
130
|
+
const r = {
|
|
131
|
+
authenticated: roles.authenticated,
|
|
132
|
+
authenticatedClient: roles.authenticatedClient
|
|
133
|
+
};
|
|
134
|
+
return `
|
|
135
|
+
BEGIN;
|
|
136
|
+
DO $do$
|
|
137
|
+
DECLARE
|
|
138
|
+
v_authenticated text := ${sqlLiteral(r.authenticated)};
|
|
139
|
+
v_client text := ${sqlLiteral(r.authenticatedClient)};
|
|
140
|
+
v_statement_timeout text := ${sqlLiteral(statementTimeout)};
|
|
141
|
+
BEGIN
|
|
142
|
+
-- Create client role: pre-check + exception handling for TOCTOU safety
|
|
143
|
+
IF NOT EXISTS (SELECT 1 FROM pg_catalog.pg_roles WHERE rolname = v_client) THEN
|
|
144
|
+
BEGIN
|
|
145
|
+
EXECUTE format('CREATE ROLE %I', v_client);
|
|
146
|
+
EXCEPTION
|
|
147
|
+
WHEN duplicate_object THEN
|
|
148
|
+
-- 42710: Role already exists (race condition); safe to ignore
|
|
149
|
+
NULL;
|
|
150
|
+
WHEN unique_violation THEN
|
|
151
|
+
-- 23505: Concurrent CREATE ROLE hit unique index; safe to ignore
|
|
152
|
+
NULL;
|
|
153
|
+
WHEN insufficient_privilege THEN
|
|
154
|
+
-- 42501: Must surface this error - caller lacks permission
|
|
155
|
+
RAISE;
|
|
156
|
+
END;
|
|
157
|
+
END IF;
|
|
158
|
+
|
|
159
|
+
-- Strip escalation attributes (safe to run even if role already exists)
|
|
160
|
+
EXECUTE format('ALTER ROLE %I WITH NOCREATEDB NOSUPERUSER NOCREATEROLE NOLOGIN NOREPLICATION NOBYPASSRLS', v_client);
|
|
161
|
+
|
|
162
|
+
-- Inherit table/schema grants from authenticated
|
|
163
|
+
IF NOT EXISTS (
|
|
164
|
+
SELECT 1 FROM pg_auth_members am
|
|
165
|
+
JOIN pg_roles r1 ON am.roleid = r1.oid
|
|
166
|
+
JOIN pg_roles r2 ON am.member = r2.oid
|
|
167
|
+
WHERE r1.rolname = v_authenticated AND r2.rolname = v_client
|
|
168
|
+
) THEN
|
|
169
|
+
BEGIN
|
|
170
|
+
EXECUTE format('GRANT %I TO %I', v_authenticated, v_client);
|
|
171
|
+
EXCEPTION
|
|
172
|
+
WHEN unique_violation THEN
|
|
173
|
+
-- 23505: Membership was granted concurrently; safe to ignore
|
|
174
|
+
NULL;
|
|
175
|
+
WHEN undefined_object THEN
|
|
176
|
+
-- 42704: One of the roles doesn't exist; log notice and continue
|
|
177
|
+
RAISE NOTICE 'Missing role when granting % to %', v_authenticated, v_client;
|
|
178
|
+
WHEN insufficient_privilege THEN
|
|
179
|
+
-- 42501: Must surface this error - caller lacks permission
|
|
180
|
+
RAISE;
|
|
181
|
+
WHEN invalid_grant_operation THEN
|
|
182
|
+
-- 0LP01: Must surface this error - invalid grant operation
|
|
183
|
+
RAISE;
|
|
184
|
+
END;
|
|
185
|
+
END IF;
|
|
186
|
+
|
|
187
|
+
-- Server-enforced baseline statement timeout for the client role
|
|
188
|
+
EXECUTE format('ALTER ROLE %I SET statement_timeout = %L', v_client, v_statement_timeout);
|
|
189
|
+
END
|
|
190
|
+
$do$;
|
|
191
|
+
COMMIT;
|
|
192
|
+
`;
|
|
193
|
+
}
|
|
106
194
|
/**
|
|
107
195
|
* Generate SQL to create a user with password and grant base roles.
|
|
108
196
|
* Callers should use getConnEnvOptions() from @pgpmjs/env to get merged values.
|