Skip to content

Commit 19a2184

Browse files
authored
Merge pull request #244 from launchql/feat/roles
config roles
2 parents 812cd0e + 0d2d4d0 commit 19a2184

6 files changed

Lines changed: 83 additions & 9 deletions

File tree

packages/pgsql-test/__tests__/postgres-test.connections.test.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
process.env.LOG_SCOPE = 'pgsql-test';
22

33
import { getConnections } from '../src/connect';
4+
import { getRoleName } from '../src/roles';
45
import { PgTestClient } from '../src/test-client';
56

67
let db: PgTestClient;
@@ -25,14 +26,16 @@ describe('anonymous', () => {
2526

2627
it('runs under anonymous context', async () => {
2728
const result = await db.query('SELECT current_setting(\'role\', true) AS role');
28-
expect(result.rows[0].role).toBe('anonymous');
29+
const expectedRole = getRoleName('anonymous');
30+
expect(result.rows[0].role).toBe(expectedRole);
2931
});
3032
});
3133

3234
describe('authenticated', () => {
3335
beforeEach(async () => {
36+
const authRole = getRoleName('authenticated');
3437
db.setContext({
35-
role: 'authenticated',
38+
role: authRole,
3639
'jwt.claims.user_agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36',
3740
'jwt.claims.ip_address': '127.0.0.1',
3841
'jwt.claims.database_id': 'jwt.database_id',
@@ -47,7 +50,8 @@ describe('authenticated', () => {
4750

4851
it('runs under authenticated context', async () => {
4952
const role = await db.query('SELECT current_setting(\'role\', true) AS role');
50-
expect(role.rows[0].role).toBe('authenticated');
53+
const expectedRole = getRoleName('authenticated');
54+
expect(role.rows[0].role).toBe(expectedRole);
5155
const ip = await db.query('SELECT current_setting(\'jwt.claims.ip_address\', true) AS role');
5256
expect(ip.rows[0].role).toBe('127.0.0.1');
5357
});

packages/pgsql-test/src/admin.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { Logger } from '@launchql/logger';
2+
import { PgTestConnectionOptions } from '@launchql/types';
23
import { execSync } from 'child_process';
34
import { existsSync } from 'fs';
45
import { getPgEnvOptions, PgConfig } from 'pg-env';
56

7+
import { getRoleName } from './roles';
68
import { SeedAdapter } from './seed/types';
79
import { streamSql as stream } from './stream';
810

@@ -11,7 +13,8 @@ const log = new Logger('db-admin');
1113
export class DbAdmin {
1214
constructor(
1315
private config: PgConfig,
14-
private verbose: boolean = false
16+
private verbose: boolean = false,
17+
private roleConfig?: PgTestConnectionOptions
1518
) {
1619
this.config = getPgEnvOptions(config);
1720
}
@@ -113,13 +116,16 @@ export class DbAdmin {
113116
}
114117

115118
async createUserRole(user: string, password: string, dbName: string): Promise<void> {
119+
const anonRole = getRoleName('anonymous', this.roleConfig);
120+
const authRole = getRoleName('authenticated', this.roleConfig);
121+
116122
const sql = `
117123
DO $$
118124
BEGIN
119125
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '${user}') THEN
120126
CREATE ROLE ${user} LOGIN PASSWORD '${password}';
121-
GRANT anonymous TO ${user};
122-
GRANT authenticated TO ${user};
127+
GRANT ${anonRole} TO ${user};
128+
GRANT ${authRole} TO ${user};
123129
END IF;
124130
END $$;
125131
`.trim();

packages/pgsql-test/src/connect.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99

1010
import { DbAdmin } from './admin';
1111
import { PgTestConnector } from './manager';
12+
import { getDefaultRole } from './roles';
1213
import { seed } from './seed';
1314
import { SeedAdapter } from './seed/types';
1415
import { PgTestClient } from './test-client';
@@ -19,7 +20,7 @@ export const getPgRootAdmin = (connOpts: PgTestConnectionOptions = {}) => {
1920
const opts = getPgEnvOptions({
2021
database: connOpts.rootDb
2122
});
22-
const admin = new DbAdmin(opts);
23+
const admin = new DbAdmin(opts, false, connOpts);
2324
return admin;
2425
};
2526

@@ -65,7 +66,7 @@ export const getConnections = async (
6566
connOpts.rootDb
6667
);
6768

68-
const admin = new DbAdmin(config as PgConfig);
69+
const admin = new DbAdmin(config as PgConfig, false, connOpts);
6970

7071
if (process.env.TEST_DB) {
7172
config.database = process.env.TEST_DB;
@@ -106,7 +107,7 @@ export const getConnections = async (
106107
user: connOpts.connection.user,
107108
password: connOpts.connection.password
108109
});
109-
db.setContext({ role: 'anonymous' });
110+
db.setContext({ role: getDefaultRole(connOpts) });
110111

111112
return { pg, db, teardown, manager, admin };
112113
};

packages/pgsql-test/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from './admin';
22
export * from './connect';
33
export * from './manager';
4+
export * from './roles';
45
export * from './seed';
56
export * from './test-client';

packages/pgsql-test/src/roles.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { PgTestConnectionOptions, RoleMapping } from '@launchql/types';
2+
3+
/**
4+
* Default role mapping configuration
5+
*/
6+
export const DEFAULT_ROLE_MAPPING: Required<RoleMapping> = {
7+
anonymous: 'anonymous',
8+
authenticated: 'authenticated',
9+
administrator: 'administrator',
10+
default: 'anonymous'
11+
};
12+
13+
/**
14+
* Get resolved role mapping with defaults
15+
*/
16+
export const getRoleMapping = (options?: PgTestConnectionOptions): Required<RoleMapping> => {
17+
return {
18+
...DEFAULT_ROLE_MAPPING,
19+
...(options?.roles || {})
20+
};
21+
};
22+
23+
/**
24+
* Get role name by key with fallback to default mapping
25+
*/
26+
export const getRoleName = (
27+
roleKey: keyof Omit<RoleMapping, 'default'>,
28+
options?: PgTestConnectionOptions
29+
): string => {
30+
const mapping = getRoleMapping(options);
31+
return mapping[roleKey];
32+
};
33+
34+
/**
35+
* Get default role name
36+
*/
37+
export const getDefaultRole = (options?: PgTestConnectionOptions): string => {
38+
const mapping = getRoleMapping(options);
39+
return mapping.default;
40+
};

packages/types/src/launchql.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,22 @@ export interface PgTestConnectionOptions {
1919
cwd?: string;
2020
/** Database connection credentials */
2121
connection?: DatabaseConnectionOptions;
22+
/** Role mapping configuration */
23+
roles?: RoleMapping;
24+
}
25+
26+
/**
27+
* Role mapping configuration for database security
28+
*/
29+
export interface RoleMapping {
30+
/** Anonymous (unauthenticated) role name */
31+
anonymous?: string;
32+
/** Authenticated user role name */
33+
authenticated?: string;
34+
/** Administrator role name */
35+
administrator?: string;
36+
/** Default role for new connections */
37+
default?: string;
2238
}
2339

2440
/**
@@ -205,6 +221,12 @@ export const launchqlDefaults: LaunchQLOptions = {
205221
user: 'app_user',
206222
password: 'app_password',
207223
role: 'anonymous'
224+
},
225+
roles: {
226+
anonymous: 'anonymous',
227+
authenticated: 'authenticated',
228+
administrator: 'administrator',
229+
default: 'anonymous'
208230
}
209231
},
210232
pg: {

0 commit comments

Comments
 (0)