diff --git a/.changeset/mean-toys-itch.md b/.changeset/mean-toys-itch.md new file mode 100644 index 0000000000..75252b3482 --- /dev/null +++ b/.changeset/mean-toys-itch.md @@ -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 + ... +``` diff --git a/packages/backend-common/config.d.ts b/packages/backend-common/config.d.ts index 6c473af3c0..8dd2c2db84 100644 --- a/packages/backend-common/config.d.ts +++ b/packages/backend-common/config.d.ts @@ -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; }; }; }; diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index 905d78ba13..9cba97a481 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -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", diff --git a/packages/backend-common/src/database/DatabaseManager.test.ts b/packages/backend-common/src/database/DatabaseManager.test.ts index 95511d5aba..336e5d499c 100644 --- a/packages/backend-common/src/database/DatabaseManager.test.ts +++ b/packages/backend-common/src/database/DatabaseManager.test.ts @@ -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'); + }); }); }); diff --git a/packages/backend-common/src/database/DatabaseManager.ts b/packages/backend-common/src/database/DatabaseManager.ts index 08979a92f8..02f2c6b172 100644 --- a/packages/backend-common/src/database/DatabaseManager.ts +++ b/packages/backend-common/src/database/DatabaseManager.ts @@ -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 }), }; } diff --git a/packages/backend-common/src/database/connectors/postgres.ts b/packages/backend-common/src/database/connectors/postgres.ts index c780fb829b..afc8c207be 100644 --- a/packages/backend-common/src/database/connectors/postgres.ts +++ b/packages/backend-common/src/database/connectors/postgres.ts @@ -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 ): Promise { 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)); diff --git a/yarn.lock b/yarn.lock index f6e994f0b2..855ab8785b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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