@pgpm/defaults 0.4.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.
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Dan Lynch <pyramation@gmail.com>
4
+ Copyright (c) 2025 Interweb, Inc.
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
package/Makefile ADDED
@@ -0,0 +1,6 @@
1
+ EXTENSION = launchql-defaults
2
+ DATA = sql/launchql-defaults--0.4.6.sql
3
+
4
+ PG_CONFIG = pg_config
5
+ PGXS := $(shell $(PG_CONFIG) --pgxs)
6
+ include $(PGXS)
package/README.md ADDED
@@ -0,0 +1,5 @@
1
+ # @pgpm/defaults
2
+
3
+ Security defaults and baseline configurations.
4
+
5
+ Provides default security settings, configurations, and baseline security policies for PostgreSQL applications.
@@ -0,0 +1,17 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`defaults security configurations configuration verification should create snapshot of security configuration 1`] = `
4
+ {
5
+ "securityState": [
6
+ {
7
+ "current_user": "postgres",
8
+ "database_name": "test-database",
9
+ "default_func_acl_count": "1",
10
+ "public_db_connect": false,
11
+ "public_db_create": false,
12
+ "public_schema_create": false,
13
+ "public_schema_usage": true,
14
+ },
15
+ ],
16
+ }
17
+ `;
@@ -0,0 +1,327 @@
1
+ import { getConnections, PgTestClient } from 'pgsql-test';
2
+ import { snapshot } from 'graphile-test';
3
+
4
+ let pg: PgTestClient;
5
+ let teardown: () => Promise<void>;
6
+
7
+ describe('defaults security configurations', () => {
8
+ beforeAll(async () => {
9
+ ({ pg, teardown } = await getConnections());
10
+ });
11
+
12
+ afterAll(async () => {
13
+ await teardown();
14
+ });
15
+
16
+ beforeEach(async () => {
17
+ await pg.beforeEach();
18
+ });
19
+
20
+ afterEach(async () => {
21
+ await pg.afterEach();
22
+ });
23
+
24
+ describe('database privileges', () => {
25
+ it('should revoke all database privileges from PUBLIC', async () => {
26
+ // Get current database name
27
+ const [dbInfo] = await pg.any(`SELECT current_database() as db_name`);
28
+
29
+ // Check database privileges for PUBLIC role
30
+ const privileges = await pg.any(`
31
+ SELECT
32
+ datacl
33
+ FROM pg_database
34
+ WHERE datname = $1
35
+ `, [dbInfo.db_name]);
36
+
37
+ expect(privileges).toHaveLength(1);
38
+
39
+ // If datacl is null, it means no explicit privileges (good)
40
+ // If datacl exists, check that PUBLIC doesn't appear or has no privileges
41
+ if (privileges[0].datacl) {
42
+ const aclString = privileges[0].datacl.toString();
43
+ // PUBLIC should not appear in the ACL with any privileges
44
+ // The ACL format is {user=privileges/grantor,...}
45
+ // PUBLIC would appear as "=privileges/grantor" or "PUBLIC=privileges/grantor"
46
+ expect(aclString).not.toMatch(/(?:^|,)(?:PUBLIC)?=[^,}]*[CTc][^,}]*(?:,|$)/);
47
+ }
48
+ // If datacl is null, that's actually what we want - no explicit privileges
49
+ });
50
+
51
+ it('should verify database connection still works for current user', async () => {
52
+ // Verify we can still connect and run queries
53
+ const [result] = await pg.any(`SELECT 1 as test`);
54
+ expect(result.test).toBe(1);
55
+
56
+ // Verify we can see current user
57
+ const [userResult] = await pg.any(`SELECT current_user as username`);
58
+ expect(userResult.username).toBeDefined();
59
+ });
60
+ });
61
+
62
+ describe('function execution privileges', () => {
63
+ beforeEach(async () => {
64
+ // Create a test function to check default privileges
65
+ await pg.any(`
66
+ CREATE OR REPLACE FUNCTION test_default_function()
67
+ RETURNS text AS $$
68
+ BEGIN
69
+ RETURN 'test result';
70
+ END;
71
+ $$ LANGUAGE plpgsql;
72
+ `);
73
+ });
74
+
75
+ it('should have revoked default EXECUTE privileges from PUBLIC on functions', async () => {
76
+ // Check if PUBLIC has execute privileges on our test function
77
+ const privileges = await pg.any(`
78
+ SELECT
79
+ has_function_privilege('public', 'test_default_function()', 'execute') as public_can_execute
80
+ `);
81
+
82
+ expect(privileges[0].public_can_execute).toBe(false);
83
+ });
84
+
85
+ it('should allow current user to execute functions they create', async () => {
86
+ // Current user should still be able to execute their own functions
87
+ const [result] = await pg.any(`SELECT test_default_function() as result`);
88
+ expect(result.result).toBe('test result');
89
+ });
90
+
91
+ it('should verify default privileges are set for new functions', async () => {
92
+ // Check the default privileges configuration
93
+ const defaultPrivs = await pg.any(`
94
+ SELECT
95
+ defaclrole,
96
+ defaclnamespace,
97
+ defaclobjtype,
98
+ defaclacl
99
+ FROM pg_default_acl
100
+ WHERE defaclobjtype = 'f' -- functions
101
+ `);
102
+
103
+ // Should have at least one default ACL entry for functions
104
+ expect(defaultPrivs.length).toBeGreaterThanOrEqual(0);
105
+
106
+ // The key test is that PUBLIC cannot execute functions by default
107
+ // We already verified this in the previous test, so this is more about
108
+ // confirming the configuration exists
109
+ if (defaultPrivs.length > 0) {
110
+ for (const priv of defaultPrivs) {
111
+ if (priv.defaclacl) {
112
+ const aclString = priv.defaclacl.toString();
113
+ // Check that PUBLIC doesn't have execute (X) privileges
114
+ // PUBLIC would appear as "=X/grantor" or "PUBLIC=X/grantor"
115
+ expect(aclString).not.toMatch(/(?:^|,)(?:PUBLIC)?=[^,}]*X[^,}]*(?:,|$)/);
116
+ }
117
+ }
118
+ }
119
+ });
120
+ });
121
+
122
+ describe('schema privileges', () => {
123
+ it('should revoke CREATE privileges on public schema from PUBLIC', async () => {
124
+ // Check if PUBLIC can create objects in public schema
125
+ const privileges = await pg.any(`
126
+ SELECT
127
+ has_schema_privilege('public', 'public', 'create') as public_can_create
128
+ `);
129
+
130
+ expect(privileges[0].public_can_create).toBe(false);
131
+ });
132
+
133
+ it('should allow current user to create objects in public schema', async () => {
134
+ // Current user should still be able to create tables
135
+ await expect(
136
+ pg.any(`
137
+ CREATE TABLE test_table_creation (
138
+ id serial PRIMARY KEY,
139
+ name text
140
+ )
141
+ `)
142
+ ).resolves.not.toThrow();
143
+
144
+ // Clean up
145
+ await pg.any(`DROP TABLE test_table_creation`);
146
+ });
147
+
148
+ it('should verify public schema still exists and is usable', async () => {
149
+ // Verify public schema exists
150
+ const schemas = await pg.any(`
151
+ SELECT schema_name
152
+ FROM information_schema.schemata
153
+ WHERE schema_name = 'public'
154
+ `);
155
+
156
+ expect(schemas).toHaveLength(1);
157
+ expect(schemas[0].schema_name).toBe('public');
158
+ });
159
+ });
160
+
161
+ describe('security verification', () => {
162
+ it('should have secure default configuration applied', async () => {
163
+ // Test that we can create objects but PUBLIC cannot access them by default
164
+ await pg.any(`
165
+ CREATE TABLE test_security_table (
166
+ id serial PRIMARY KEY,
167
+ secret_data text
168
+ )
169
+ `);
170
+
171
+ // Insert test data
172
+ await pg.any(`
173
+ INSERT INTO test_security_table (secret_data)
174
+ VALUES ('confidential information')
175
+ `);
176
+
177
+ // Current user should be able to access
178
+ const [result] = await pg.any(`
179
+ SELECT secret_data FROM test_security_table LIMIT 1
180
+ `);
181
+ expect(result.secret_data).toBe('confidential information');
182
+
183
+ // Check table privileges for PUBLIC
184
+ const tablePrivs = await pg.any(`
185
+ SELECT
186
+ has_table_privilege('public', 'test_security_table', 'select') as public_can_select,
187
+ has_table_privilege('public', 'test_security_table', 'insert') as public_can_insert
188
+ `);
189
+
190
+ // PUBLIC should not have privileges unless explicitly granted
191
+ expect(tablePrivs[0].public_can_select).toBe(false);
192
+ expect(tablePrivs[0].public_can_insert).toBe(false);
193
+ });
194
+
195
+ it('should verify current user retains necessary privileges', async () => {
196
+ const [currentUser] = await pg.any(`SELECT current_user as username`);
197
+
198
+ // Current user should have necessary privileges to work
199
+ const userPrivs = await pg.any(`
200
+ SELECT
201
+ has_schema_privilege($1, 'public', 'usage') as can_use_public,
202
+ has_schema_privilege($1, 'public', 'create') as can_create_in_public
203
+ `, [currentUser.username]);
204
+
205
+ expect(userPrivs[0].can_use_public).toBe(true);
206
+ expect(userPrivs[0].can_create_in_public).toBe(true);
207
+ });
208
+ });
209
+
210
+ describe('privilege inheritance', () => {
211
+ it('should verify that new schemas inherit secure defaults', async () => {
212
+ // Create a new schema
213
+ await pg.any(`CREATE SCHEMA test_new_schema`);
214
+
215
+ // Create a function in the new schema
216
+ await pg.any(`
217
+ CREATE OR REPLACE FUNCTION test_new_schema.test_inherited_function()
218
+ RETURNS text AS $$
219
+ BEGIN
220
+ RETURN 'inherited test';
221
+ END;
222
+ $$ LANGUAGE plpgsql;
223
+ `);
224
+
225
+ // PUBLIC should not have execute privileges due to default privilege settings
226
+ const privileges = await pg.any(`
227
+ SELECT
228
+ has_function_privilege('public', 'test_new_schema.test_inherited_function()', 'execute') as public_can_execute
229
+ `);
230
+
231
+ expect(privileges[0].public_can_execute).toBe(false);
232
+
233
+ // Clean up
234
+ await pg.any(`DROP SCHEMA test_new_schema CASCADE`);
235
+ });
236
+
237
+ it('should allow explicit privilege grants to work', async () => {
238
+ // Create a test table
239
+ await pg.any(`
240
+ CREATE TABLE test_explicit_grants (
241
+ id serial PRIMARY KEY,
242
+ data text
243
+ )
244
+ `);
245
+
246
+ // Explicitly grant SELECT to PUBLIC
247
+ await pg.any(`GRANT SELECT ON test_explicit_grants TO PUBLIC`);
248
+
249
+ // Now PUBLIC should have SELECT privilege
250
+ const privileges = await pg.any(`
251
+ SELECT
252
+ has_table_privilege('public', 'test_explicit_grants', 'select') as public_can_select
253
+ `);
254
+
255
+ expect(privileges[0].public_can_select).toBe(true);
256
+
257
+ // But not other privileges
258
+ const otherPrivs = await pg.any(`
259
+ SELECT
260
+ has_table_privilege('public', 'test_explicit_grants', 'insert') as public_can_insert,
261
+ has_table_privilege('public', 'test_explicit_grants', 'update') as public_can_update
262
+ `);
263
+
264
+ expect(otherPrivs[0].public_can_insert).toBe(false);
265
+ expect(otherPrivs[0].public_can_update).toBe(false);
266
+ });
267
+ });
268
+
269
+ describe('configuration verification', () => {
270
+ it('should have applied all expected security configurations', async () => {
271
+ // Verify the three main security configurations are in place:
272
+
273
+ // 1. Database privileges revoked from PUBLIC
274
+ const [dbInfo] = await pg.any(`SELECT current_database() as db_name`);
275
+ const [dbPrivs] = await pg.any(`
276
+ SELECT
277
+ has_database_privilege('public', $1, 'connect') as public_can_connect,
278
+ has_database_privilege('public', $1, 'create') as public_can_create_db
279
+ `, [dbInfo.db_name]);
280
+
281
+ // Note: PUBLIC might still have CONNECT (that's usually okay)
282
+ // but should not have CREATE privileges
283
+ expect(dbPrivs.public_can_create_db).toBe(false);
284
+
285
+ // 2. Function execution revoked from PUBLIC by default
286
+ const defaultFuncPrivs = await pg.any(`
287
+ SELECT count(*) as count
288
+ FROM pg_default_acl
289
+ WHERE defaclobjtype = 'f'
290
+ `);
291
+ // Should have some default ACL configuration
292
+ expect(parseInt(defaultFuncPrivs[0].count)).toBeGreaterThanOrEqual(0);
293
+
294
+ // 3. CREATE on public schema revoked from PUBLIC
295
+ const [schemaPrivs] = await pg.any(`
296
+ SELECT
297
+ has_schema_privilege('public', 'public', 'create') as public_can_create_in_public
298
+ `);
299
+ expect(schemaPrivs.public_can_create_in_public).toBe(false);
300
+ });
301
+
302
+ it('should create snapshot of security configuration', async () => {
303
+ // Get current security state for snapshot testing
304
+ const [currentUser] = await pg.any(`SELECT current_user as username`);
305
+ const [dbInfo] = await pg.any(`SELECT current_database() as db_name`);
306
+
307
+ const securityState = await pg.any(`
308
+ SELECT
309
+ $1 as current_user,
310
+ $2 as database_name,
311
+ has_database_privilege('public', $2, 'connect') as public_db_connect,
312
+ has_database_privilege('public', $2, 'create') as public_db_create,
313
+ has_schema_privilege('public', 'public', 'usage') as public_schema_usage,
314
+ has_schema_privilege('public', 'public', 'create') as public_schema_create,
315
+ (SELECT count(*) FROM pg_default_acl WHERE defaclobjtype = 'f') as default_func_acl_count
316
+ `, [currentUser.username, dbInfo.db_name]);
317
+
318
+ // Normalize the database name for consistent snapshots
319
+ const normalizedSecurityState = securityState.map(state => ({
320
+ ...state,
321
+ database_name: 'test-database' // Replace dynamic name with static value
322
+ }));
323
+
324
+ expect(snapshot({ securityState: normalizedSecurityState })).toMatchSnapshot();
325
+ });
326
+ });
327
+ });
@@ -0,0 +1,17 @@
1
+ -- Deploy launchql-defaults:defaults/public to pg
2
+
3
+ BEGIN;
4
+ DO $$
5
+ DECLARE
6
+ sql text;
7
+ BEGIN
8
+ SELECT
9
+ format('REVOKE ALL ON DATABASE %I FROM PUBLIC', current_database()) INTO sql;
10
+ EXECUTE sql;
11
+ END
12
+ $$;
13
+ -- NOTE: don't alter this as new schemas inherit this behavior
14
+ ALTER DEFAULT PRIVILEGES REVOKE EXECUTE ON FUNCTIONS FROM PUBLIC;
15
+ REVOKE CREATE ON SCHEMA public FROM PUBLIC;
16
+ COMMIT;
17
+
package/jest.config.js ADDED
@@ -0,0 +1,15 @@
1
+ /** @type {import('ts-jest').JestConfigWithTsJest} */
2
+ module.exports = {
3
+ preset: 'ts-jest',
4
+ testEnvironment: 'node',
5
+
6
+ // Match both __tests__ and colocated test files
7
+ testMatch: ['**/?(*.)+(test|spec).{ts,tsx,js,jsx}'],
8
+
9
+ // Ignore build artifacts and type declarations
10
+ testPathIgnorePatterns: ['/dist/', '\\.d\\.ts$'],
11
+ modulePathIgnorePatterns: ['<rootDir>/dist/'],
12
+ watchPathIgnorePatterns: ['/dist/'],
13
+
14
+ moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
15
+ };
@@ -0,0 +1,8 @@
1
+ # launchql-defaults extension
2
+ comment = 'launchql-defaults extension'
3
+ default_version = '0.4.6'
4
+ module_pathname = '$libdir/launchql-defaults'
5
+ requires = 'plpgsql,launchql-verify'
6
+ relocatable = false
7
+ superuser = false
8
+
package/launchql.plan ADDED
@@ -0,0 +1,5 @@
1
+ %syntax-version=1.0.0
2
+ %project=launchql-defaults
3
+ %uri=launchql-defaults
4
+
5
+ defaults/public 2017-08-11T08:11:51Z skitch <skitch@5b0c196eeb62> # add defaults/public
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@pgpm/defaults",
3
+ "version": "0.4.0",
4
+ "description": "Security defaults and baseline configurations",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "scripts": {
9
+ "bundle": "lql package",
10
+ "test": "jest",
11
+ "test:watch": "jest --watch"
12
+ },
13
+ "dependencies": {
14
+ "@pgpm/verify": "0.4.0"
15
+ },
16
+ "devDependencies": {
17
+ "@launchql/cli": "^4.9.0"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "https://github.com/launchql/extensions"
22
+ },
23
+ "homepage": "https://github.com/launchql/extensions",
24
+ "bugs": {
25
+ "url": "https://github.com/launchql/extensions/issues"
26
+ },
27
+ "gitHead": "cc9f52a335caa6e21ee7751b04b77c84ce6cb809"
28
+ }
@@ -0,0 +1,7 @@
1
+ -- Revert launchql-defaults:defaults/public from pg
2
+
3
+ BEGIN;
4
+
5
+ -- XXX Add DDLs here.
6
+
7
+ COMMIT;
@@ -0,0 +1,15 @@
1
+ \echo Use "CREATE EXTENSION launchql-defaults" to load this file. \quit
2
+ DO $EOFCODE$
3
+ DECLARE
4
+ sql text;
5
+ BEGIN
6
+ SELECT
7
+ format('REVOKE ALL ON DATABASE %I FROM PUBLIC', current_database()) INTO sql;
8
+ EXECUTE sql;
9
+ END
10
+ $EOFCODE$;
11
+
12
+ ALTER DEFAULT PRIVILEGES
13
+ REVOKE EXECUTE ON FUNCTIONS FROM PUBLIC RESTRICT;
14
+
15
+ REVOKE CREATE ON SCHEMA public FROM PUBLIC RESTRICT;
@@ -0,0 +1,7 @@
1
+ -- Verify launchql-defaults:defaults/public on pg
2
+
3
+ BEGIN;
4
+
5
+ -- XXX Add verifications here.
6
+
7
+ ROLLBACK;