@pgpmjs/core 6.26.1 → 6.27.0

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.
@@ -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.
@@ -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.26.1",
3
+ "version": "6.27.0",
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.0",
51
+ "@pgpmjs/env": "^2.26.1",
52
52
  "@pgpmjs/logger": "^2.13.0",
53
- "@pgpmjs/server-utils": "^3.14.0",
54
- "@pgpmjs/types": "^2.30.0",
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.13.0",
61
+ "pg-cache": "^3.13.1",
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": "8b9a410cb57ddbd0fa685c6177b979a97178bd4e"
67
+ "gitHead": "5a7e92e9849aa715367a33fa78a23c5f9adaefa0"
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.