From 4fe12a605bff0755e938dc98e9c32885c9267fa6 Mon Sep 17 00:00:00 2001 From: Oleksandr Andriienko Date: Mon, 9 Sep 2024 15:36:15 +0300 Subject: [PATCH] fix(rbac): support RejectUnauthorized ssl option for postgres (#2122) * fix(rbac): added support for rejectUnauthorized in PG SSL options connection * fix(rbac): make sure undefined properties are accepted * fix(rbac): fix compilation and unit tests Signed-off-by: Oleksandr Andriienko * fix(rbac): refactor SSL handling to reuse existing types Signed-off-by: Oleksandr Andriienko --------- Signed-off-by: Oleksandr Andriienko Co-authored-by: Boris Gershanik --- plugins/rbac-backend/package.json | 1 + .../database/casbin-adapter-factory.test.ts | 70 +++++++++++++++++++ .../src/database/casbin-adapter-factory.ts | 40 +++++++---- .../src/service/policy-builder.ts | 2 +- 4 files changed, 100 insertions(+), 13 deletions(-) diff --git a/plugins/rbac-backend/package.json b/plugins/rbac-backend/package.json index 64efd321fa..ee0cb2d0f8 100644 --- a/plugins/rbac-backend/package.json +++ b/plugins/rbac-backend/package.json @@ -40,6 +40,7 @@ "@backstage/errors": "^1.2.4", "@backstage/plugin-auth-node": "^0.4.17", "@backstage/plugin-permission-backend": "^0.5.46", + "@backstage/backend-defaults": "^0.4.1", "@backstage/plugin-permission-common": "^0.8.0", "@backstage/plugin-permission-node": "^0.8.0", "@backstage/types": "^1.1.1", diff --git a/plugins/rbac-backend/src/database/casbin-adapter-factory.test.ts b/plugins/rbac-backend/src/database/casbin-adapter-factory.test.ts index 7147e12d79..0f8a136c47 100644 --- a/plugins/rbac-backend/src/database/casbin-adapter-factory.test.ts +++ b/plugins/rbac-backend/src/database/casbin-adapter-factory.test.ts @@ -179,6 +179,76 @@ describe('CasbinAdapterFactory', () => { }, }); }); + + it('test building an adapter using a PostgreSQL configuration with intentionally ssl and TLS options.', async () => { + const config = new ConfigReader({ + backend: { + database: { + client: 'pg', + connection: { + host: 'localhost', + port: '5432', + user: 'postgresUser', + password: process.env.TEST, + ssl: { + ca: 'abc', + rejectUnauthorized: false, + }, + }, + }, + }, + }); + const factory = new CasbinDBAdapterFactory(config, db); + const adapter = await factory.createAdapter(); + expect(adapter).not.toBeNull(); + expect(newAdapterMock).toHaveBeenCalledWith({ + type: 'postgres', + host: 'localhost', + port: 5432, + schema: 'public', + username: 'postgresUser', + password: process.env.TEST, + database: 'test-database', + ssl: { + ca: 'abc', + rejectUnauthorized: false, + }, + }); + }); + + it('test building an adapter using a PostgreSQL configuration with intentionally ssl without CA.', async () => { + const config = new ConfigReader({ + backend: { + database: { + client: 'pg', + connection: { + host: 'localhost', + port: '5432', + user: 'postgresUser', + password: process.env.TEST, + ssl: { + rejectUnauthorized: false, + }, + }, + }, + }, + }); + const factory = new CasbinDBAdapterFactory(config, db); + const adapter = await factory.createAdapter(); + expect(adapter).not.toBeNull(); + expect(newAdapterMock).toHaveBeenCalledWith({ + type: 'postgres', + host: 'localhost', + port: 5432, + schema: 'public', + username: 'postgresUser', + password: process.env.TEST, + database: 'test-database', + ssl: { + rejectUnauthorized: false, + }, + }); + }); }); it('ensure that building an adapter with an unknown configuration fails.', async () => { diff --git a/plugins/rbac-backend/src/database/casbin-adapter-factory.ts b/plugins/rbac-backend/src/database/casbin-adapter-factory.ts index b5e3733ccb..6976e9db2b 100644 --- a/plugins/rbac-backend/src/database/casbin-adapter-factory.ts +++ b/plugins/rbac-backend/src/database/casbin-adapter-factory.ts @@ -5,7 +5,9 @@ import { Knex } from 'knex'; import TypeORMAdapter from 'typeorm-adapter'; import { resolve } from 'path'; -import { TlsOptions } from 'tls'; +import { ConnectionOptions, TlsOptions } from 'tls'; + +import '@backstage/backend-defaults/database'; const DEFAULT_SQLITE3_STORAGE_FILE_NAME = 'rbac.sqlite'; @@ -26,7 +28,7 @@ export class CasbinDBAdapterFactory { const schema = (await this.databaseClient.client.searchPath?.[0]) ?? 'public'; - const ssl = this.handleSSL(databaseConfig!); + const ssl = this.handlePostgresSSL(databaseConfig!); adapter = await TypeORMAdapter.newAdapter({ type: 'postgres', @@ -63,28 +65,42 @@ export class CasbinDBAdapterFactory { return adapter; } - private handleSSL(dbConfig: Config): boolean | TlsOptions | undefined { - const connection = dbConfig.getOptional('connection'); + private handlePostgresSSL( + dbConfig: Config, + ): boolean | TlsOptions | undefined { + const connection = dbConfig.getOptional( + 'connection', + ); if (!connection) { return undefined; } - const ssl = (connection as { ssl: Object | boolean | undefined }).ssl; + + if (typeof connection === 'string' || connection instanceof String) { + throw new Error( + `rbac backend plugin doesn't support postgres connection in a string format yet`, + ); + } + + const ssl: boolean | ConnectionOptions | undefined = connection.ssl; if (ssl === undefined) { return undefined; } - if (typeof ssl.valueOf() === 'boolean') { + if (typeof ssl === 'boolean') { return ssl; } - if (typeof ssl.valueOf() === 'object') { - const ca = (ssl as { ca: string }).ca; - if (ca) { - return { ca }; - } + if (typeof ssl === 'object') { + const { ca, rejectUnauthorized } = ssl as ConnectionOptions; + const tlsOpts = { ca, rejectUnauthorized }; + // SSL object was defined with some options that we don't support yet. - return true; + if (Object.values(tlsOpts).every(el => el === undefined)) { + return true; + } + + return tlsOpts; } return undefined; diff --git a/plugins/rbac-backend/src/service/policy-builder.ts b/plugins/rbac-backend/src/service/policy-builder.ts index 73d4bae00a..0ddfe9efb7 100644 --- a/plugins/rbac-backend/src/service/policy-builder.ts +++ b/plugins/rbac-backend/src/service/policy-builder.ts @@ -1,8 +1,8 @@ import { createLegacyAuthAdapters, - DatabaseManager, PluginEndpointDiscovery, } from '@backstage/backend-common'; +import { DatabaseManager } from '@backstage/backend-defaults/database'; import { AuthService, HttpAuthService,