Merge pull request #16424 from joeppeeters/16355-add-pg-schema-ownership-config

Add `role` config to control postgres ownership of schemas and tables
This commit is contained in:
Ben Lambert
2023-03-07 09:19:41 +01:00
committed by GitHub
7 changed files with 131 additions and 10 deletions
+19
View File
@@ -0,0 +1,19 @@
---
'@backstage/backend-common': patch
---
Adds config option `backend.database.role` to set ownership for newly created schemas and tables in Postgres
The example config below connects to the database as user `v-backstage-123` but sets the ownership of
the create schemas and tables to `backstage`
```yaml
backend:
database:
client: pg
pluginDivisionMode: schema
role: backstage
connection:
user: v-backstage-123
...
```
+4
View File
@@ -96,6 +96,8 @@ export interface Config {
* @default database
*/
pluginDivisionMode?: 'database' | 'schema';
/** Configures the ownership of newly created schemas in pg databases. */
role?: string;
/**
* Arbitrary config object to pass to knex when initializing
* (https://knexjs.org/#Installation-client). Most notable is the debug
@@ -125,6 +127,8 @@ export interface Config {
* This is merged recursively into the base knexConfig
*/
knexConfig?: object;
/** Configures the ownership of newly created schemas in pg databases. */
role?: string;
};
};
};
+2
View File
@@ -90,6 +90,7 @@
"morgan": "^1.10.0",
"node-fetch": "^2.6.7",
"node-forge": "^1.3.1",
"pg": "^8.3.0",
"raw-body": "^2.4.1",
"request": "^2.88.2",
"selfsigned": "^2.0.0",
@@ -122,6 +123,7 @@
"@types/mock-fs": "^4.13.0",
"@types/morgan": "^1.9.0",
"@types/node-forge": "^1.3.0",
"@types/pg": "^8.6.6",
"@types/recursive-readdir": "^2.2.0",
"@types/stoppable": "^1.1.0",
"@types/supertest": "^2.0.8",
@@ -687,5 +687,59 @@ describe('DatabaseManager', () => {
}),
);
});
it('sets the owner config for plugin using default config', async () => {
const testManager = DatabaseManager.fromConfig(
new ConfigReader({
backend: {
database: {
client: 'pg',
connection: {
host: 'localhost',
database: 'foodb',
},
role: 'backstage',
plugin: {
testowner: {},
},
},
},
}),
);
await testManager.forPlugin('testowner').getClient();
const mockCalls = mocked(createDatabaseClient).mock.calls.splice(-1);
const [baseConfig] = mockCalls[0];
expect(baseConfig.data.role).toEqual('backstage');
});
it('sets the owner config for plugin using plugin config', async () => {
const testManager = DatabaseManager.fromConfig(
new ConfigReader({
backend: {
database: {
client: 'pg',
connection: {
host: 'localhost',
database: 'foodb',
},
role: 'backstage',
plugin: {
testowner: {
role: 'backstage-plugin',
},
},
},
},
}),
);
await testManager.forPlugin('testowner').getClient();
const mockCalls = mocked(createDatabaseClient).mock.calls.splice(-1);
const [baseConfig] = mockCalls[0];
expect(baseConfig.data.role).toEqual('backstage-plugin');
});
});
});
@@ -186,6 +186,13 @@ export class DatabaseManager {
};
}
private getRoleConfig(pluginId: string): string | undefined {
return (
this.config.getOptionalString(`${pluginPath(pluginId)}.role`) ??
this.config.getOptionalString('role')
);
}
/**
* Provides the knexConfig which should be used for a given plugin.
*
@@ -278,11 +285,13 @@ export class DatabaseManager {
*/
private getConfigForPlugin(pluginId: string): Knex.Config {
const { client } = this.getClientType(pluginId);
const role = this.getRoleConfig(pluginId);
return {
...this.getAdditionalKnexConfig(pluginId),
client,
connection: this.getConnectionConfig(pluginId),
...(role && { role }),
};
}
@@ -22,6 +22,7 @@ import { mergeDatabaseConfig } from '../config';
import { DatabaseConnector } from '../types';
import defaultNameOverride from './defaultNameOverride';
import defaultSchemaOverride from './defaultSchemaOverride';
import { Client } from 'pg';
/**
* Creates a knex postgres database connection
@@ -35,6 +36,17 @@ export function createPgDatabaseClient(
) {
const knexConfig = buildPgDatabaseConfig(dbConfig, overrides);
const database = knexFactory(knexConfig);
const role = dbConfig.getOptionalString('role');
if (role) {
database.client.pool.on(
'createSuccess',
async (_event: number, pgClient: Client) => {
await pgClient.query(`SET ROLE ${role}`);
},
);
}
return database;
}
@@ -147,10 +159,18 @@ export async function ensurePgSchemaExists(
...schemas: Array<string>
): Promise<void> {
const admin = createPgDatabaseClient(dbConfig);
const role = dbConfig.getOptionalString('role');
try {
const ensureSchema = async (database: string) => {
await admin.raw(`CREATE SCHEMA IF NOT EXISTS ??`, [database]);
if (role) {
await admin.raw(`CREATE SCHEMA IF NOT EXISTS ?? AUTHORIZATION ??`, [
database,
role,
]);
} else {
await admin.raw(`CREATE SCHEMA IF NOT EXISTS ??`, [database]);
}
};
await Promise.all(schemas.map(ensureSchema));
+22 -9
View File
@@ -3453,6 +3453,7 @@ __metadata:
"@types/mock-fs": ^4.13.0
"@types/morgan": ^1.9.0
"@types/node-forge": ^1.3.0
"@types/pg": ^8.6.6
"@types/recursive-readdir": ^2.2.0
"@types/stoppable": ^1.1.0
"@types/supertest": ^2.0.8
@@ -3489,6 +3490,7 @@ __metadata:
mysql2: ^2.2.5
node-fetch: ^2.6.7
node-forge: ^1.3.1
pg: ^8.3.0
raw-body: ^2.4.1
recursive-readdir: ^2.2.2
request: ^2.88.2
@@ -15508,6 +15510,17 @@ __metadata:
languageName: node
linkType: hard
"@types/pg@npm:^8.6.6":
version: 8.6.6
resolution: "@types/pg@npm:8.6.6"
dependencies:
"@types/node": "*"
pg-protocol: "*"
pg-types: ^2.2.0
checksum: ac145553a8ad2f357feacad1bceaf5d6ce904eb9d66233b84c469a2b4fa3738d4ebdf29b7ea45387be2d07f915fd873a229f90a2f766d7c377afa7c41fbcf8d1
languageName: node
linkType: hard
"@types/pluralize@npm:^0.0.29":
version: 0.0.29
resolution: "@types/pluralize@npm:0.0.29"
@@ -32034,14 +32047,14 @@ __metadata:
languageName: node
linkType: hard
"pg-protocol@npm:^1.5.0":
version: 1.5.0
resolution: "pg-protocol@npm:1.5.0"
checksum: b839d12cafe942ef9cbc5b13c174eb2356804fb4fe8ead8279f46a36be90722d19a91409955beb8a3d5301639c44854e49749de4aef02dc361fee3e2a61fb1e4
"pg-protocol@npm:*, pg-protocol@npm:^1.6.0":
version: 1.6.0
resolution: "pg-protocol@npm:1.6.0"
checksum: e12662d2de2011e0c3a03f6a09f435beb1025acdc860f181f18a600a5495dc38a69d753bbde1ace279c8c442536af9c1a7c11e1d0fe3fad3aa1348b28d9d2683
languageName: node
linkType: hard
"pg-types@npm:^2.1.0":
"pg-types@npm:^2.1.0, pg-types@npm:^2.2.0":
version: 2.2.0
resolution: "pg-types@npm:2.2.0"
dependencies:
@@ -32055,14 +32068,14 @@ __metadata:
linkType: hard
"pg@npm:^8.3.0, pg@npm:^8.4.0":
version: 8.8.0
resolution: "pg@npm:8.8.0"
version: 8.9.0
resolution: "pg@npm:8.9.0"
dependencies:
buffer-writer: 2.0.0
packet-reader: 1.0.0
pg-connection-string: ^2.5.0
pg-pool: ^3.5.2
pg-protocol: ^1.5.0
pg-protocol: ^1.6.0
pg-types: ^2.1.0
pgpass: 1.x
peerDependencies:
@@ -32070,7 +32083,7 @@ __metadata:
peerDependenciesMeta:
pg-native:
optional: true
checksum: fa30a85814dd7238b582c3bc6c0b9e2b0ae38dd0a6bb485ef480e64bb5ce589de6cb873ce4d3cd10c37a3e0a1e1281ba75dc7d80b1a68bae91999cd5b70d398b
checksum: dfd158955318f9ffb9428eaada29f3ee98b9eb07e87ed4b56589a19984d109f23bb8f88db78b7d7f870553e5b75ca0d58d0ed55755a8c6aed5df44e038c1d529
languageName: node
linkType: hard