From e1a50a5e3847aeefdeeedf8af4e5e1a5c36fc809 Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Mon, 13 Nov 2023 10:21:21 -0800 Subject: [PATCH] feat: Add support to an auth config (#238) Adds a new `auth` property to the connector constructor that can be used by `SQLAdminFetcher` to extend from or provide support to a custom auth object defined by the user. Fixes: https://github.com/GoogleCloudPlatform/cloud-sql-nodejs-connector/issues/200 --- README.md | 91 ++++++++++++++++++++++++++-------------- src/connector.ts | 9 +++- src/sqladmin-fetcher.ts | 33 ++++++++++----- test/sqladmin-fetcher.ts | 14 ++++++- 4 files changed, 102 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index 6ec12637..29f2dc90 100644 --- a/README.md +++ b/README.md @@ -20,12 +20,12 @@ alternative to the [Cloud SQL Auth Proxy](https://cloud.google.com/sql/docs/mysq while providing the following benefits: - **IAM Authorization:** uses IAM permissions to control who/what can connect to -your Cloud SQL instances + your Cloud SQL instances - **Improved Security:** uses robust, updated TLS 1.3 encryption and identity -verification between the client connector and the server-side proxy, -independent of the database protocol. + verification between the client connector and the server-side proxy, + independent of the database protocol. - **Convenience:** removes the requirement to use and distribute SSL -certificates, as well as manage firewalls or source/destination IP addresses. + certificates, as well as manage firewalls or source/destination IP addresses. - (optionally) **IAM DB Authentication:** provides support for [Cloud SQL’s automatic IAM DB AuthN][iam-db-authn] feature. @@ -90,14 +90,14 @@ const {Pool} = pg; const connector = new Connector(); const clientOpts = await connector.getOptions({ instanceConnectionName: 'my-project:region:my-instance', - ipType: 'PUBLIC', + ipType: 'PUBLIC', }); const pool = new Pool({ ...clientOpts, user: 'my-user', password: 'my-password', database: 'db-name', - max: 5 + max: 5, }); const {rows} = await pool.query('SELECT NOW()'); console.table(rows); // prints returned time value from server @@ -127,7 +127,7 @@ const pool = await mysql.createPool({ database: 'db-name', }); const conn = await pool.getConnection(); -const [result] = await conn.query( `SELECT NOW();`); +const [result] = await conn.query(`SELECT NOW();`); console.table(result); // prints returned time value from server await pool.end(); @@ -146,7 +146,7 @@ const {Connector} = require('@google-cloud/cloud-sql-connector'); const connector = new Connector(); const clientOpts = await connector.getTediousOptions({ instanceConnectionName: process.env.SQLSERVER_CONNECTION_NAME, - ipType: 'PUBLIC' + ipType: 'PUBLIC', }); const connection = new Connection({ // Please note that the `server` property here is not used and is only defined @@ -172,21 +172,29 @@ const connection = new Connection({ port: 9999, database: 'my-database', }, -}) +}); connection.connect(err => { - if (err) { throw err; } + if (err) { + throw err; + } let result; - const req = new Request('SELECT GETUTCDATE()', (err) => { - if (err) { throw err; } - }) - req.on('error', (err) => { throw err; }); - req.on('row', (columns) => { result = columns; }); + const req = new Request('SELECT GETUTCDATE()', err => { + if (err) { + throw err; + } + }); + req.on('error', err => { + throw err; + }); + req.on('row', columns => { + result = columns; + }); req.on('requestCompleted', () => { console.table(result); }); connection.execSql(req); -}) +}); connection.close(); connector.close(); @@ -197,8 +205,8 @@ connector.close(); The Cloud SQL Connector for Node.js can be used to connect to Cloud SQL instances using both public and private IP addresses, as well as [Private Service Connect](https://cloud.google.com/vpc/docs/private-service-connect) - (PSC). Specifying which IP address type to connect to can be configured within - `getOptions` through the `ipType` argument. +(PSC). Specifying which IP address type to connect to can be configured within +`getOptions` through the `ipType` argument. By default, connections will be configured to `'PUBLIC'` and connect over public IP, to configure connections to use an instance's private IP, @@ -230,7 +238,7 @@ const clientOpts = await connector.getOptions({ #### Example on how to use `IpAddressTypes` in TypeScript ```js -import { Connector, IpAddressTypes } from '@google-cloud/cloud-sql-connector'; +import {Connector, IpAddressTypes} from '@google-cloud/cloud-sql-connector'; const clientOpts = await connector.getOptions({ instanceConnectionName: 'my-project:region:my-instance', ipType: IpAddressTypes.PSC, @@ -252,12 +260,13 @@ automatic IAM database authentication with `getOptions` through the ```js const clientOpts = await connector.getOptions({ instanceConnectionName: 'my-project:region:my-instance', - authType: 'IAM' + authType: 'IAM', }); ``` When configuring a connection for IAM authentication, the `password` argument can be omitted and the `user` argument should be formatted as follows: + > Postgres: For an IAM user account, this is the user's email address. > For a service account, it is the service account's email without the > `.gserviceaccount.com` domain suffix. @@ -285,13 +294,13 @@ const {Pool} = pg; const connector = new Connector(); const clientOpts = await connector.getOptions({ instanceConnectionName: 'my-project:region:my-instance', - authType: 'IAM' + authType: 'IAM', }); const pool = new Pool({ ...clientOpts, user: 'test-sa@test-project.iam', database: 'db-name', - max: 5 + max: 5, }); const {rows} = await pool.query('SELECT NOW()'); console.table(rows); // prints returned time value from server @@ -309,7 +318,7 @@ import {Connector} from '@google-cloud/cloud-sql-connector'; const connector = new Connector(); const clientOpts = await connector.getOptions({ instanceConnectionName: 'my-project:region:my-instance', - authType: 'IAM' + authType: 'IAM', }); const pool = await mysql.createPool({ ...clientOpts, @@ -317,7 +326,7 @@ const pool = await mysql.createPool({ database: 'db-name', }); const conn = await pool.getConnection(); -const [result] = await conn.query( `SELECT NOW();`); +const [result] = await conn.query(`SELECT NOW();`); console.table(result); // prints returned time value from server await pool.end(); @@ -330,24 +339,42 @@ For TypeScript users, the `AuthTypes` type can be imported and used directly for automatic IAM database authentication. ```js -import { AuthTypes, Connector } from '@google-cloud/cloud-sql-connector'; +import {AuthTypes, Connector} from '@google-cloud/cloud-sql-connector'; const clientOpts = await connector.getOptions({ instanceConnectionName: 'my-project:region:my-instance', authType: AuthTypes.IAM, }); ``` +## Using With `Google Auth Library: Node.js Client` Credentials + +One can use [`google-auth-library`](https://github.com/googleapis/google-auth-library-nodejs/) credentials +with this library by providing an `AuthClient` or `GoogleAuth` instance to the `Connector`. + +```sh +npm install google-auth-library +``` + +```js +import {GoogleAuth} from 'google-auth-library'; +import {Connector} from '@google-cloud/cloud-sql-connector'; + +const connector = new Connector({ + auth: new GoogleAuth(), +}); +``` + ## Additional customization via Environment Variables It is possible to change some of the library default behavior via environment variables. Here is a quick reference to supported values and their effect: - `GOOGLE_APPLICATION_CREDENTIALS`: If defined the connector will use this -file as a custom credential files to authenticate to Cloud SQL APIs. Should be -a path to a JSON file. You can -[find more on how to get a valid credentials file here][credentials-json-file]. + file as a custom credential files to authenticate to Cloud SQL APIs. Should be + a path to a JSON file. You can + [find more on how to get a valid credentials file here][credentials-json-file]. - `GOOGLE_CLOUD_QUOTA_PROJECT`: Used to set a custom quota project to Cloud SQL -APIs when defined. + APIs when defined. ## Support policy @@ -379,9 +406,9 @@ update as soon as possible to an actively supported LTS version. Google's client libraries support legacy versions of Node.js runtimes on a best-efforts basis with the following warnings: -* Legacy versions are not tested in continuous integration. -* Some security patches and features cannot be backported. -* Dependencies cannot be kept up-to-date. +- Legacy versions are not tested in continuous integration. +- Some security patches and features cannot be backported. +- Dependencies cannot be kept up-to-date. ### Release cadence diff --git a/src/connector.ts b/src/connector.ts index 3fcb7029..86a9c24a 100644 --- a/src/connector.ts +++ b/src/connector.ts @@ -13,6 +13,7 @@ // limitations under the License. import tls from 'node:tls'; +import {AuthClient, GoogleAuth} from 'google-auth-library'; import {CloudSQLInstance} from './cloud-sql-instance'; import {getSocket} from './socket'; import {IpAddressTypes} from './ip-addresses'; @@ -147,6 +148,7 @@ class CloudSQLInstanceMap extends Map { } interface ConnectorOptions { + auth?: GoogleAuth | AuthClient; sqlAdminAPIEndpoint?: string; } @@ -156,9 +158,12 @@ export class Connector { private readonly instances: CloudSQLInstanceMap; private readonly sqlAdminFetcher: SQLAdminFetcher; - constructor({sqlAdminAPIEndpoint}: ConnectorOptions = {}) { + constructor(opts: ConnectorOptions = {}) { this.instances = new CloudSQLInstanceMap(); - this.sqlAdminFetcher = new SQLAdminFetcher({sqlAdminAPIEndpoint}); + this.sqlAdminFetcher = new SQLAdminFetcher({ + loginAuth: opts.auth, + sqlAdminAPIEndpoint: opts.sqlAdminAPIEndpoint, + }); } // Connector.getOptions is a method that accepts a Cloud SQL instance diff --git a/src/sqladmin-fetcher.ts b/src/sqladmin-fetcher.ts index 484dc4cb..c0613ec0 100644 --- a/src/sqladmin-fetcher.ts +++ b/src/sqladmin-fetcher.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import {GoogleAuth} from 'google-auth-library'; +import {AuthClient, GoogleAuth} from 'google-auth-library'; import {sqladmin_v1beta4} from '@googleapis/sqladmin'; import {instance as gaxios} from 'gaxios'; const {Sqladmin} = sqladmin_v1beta4; @@ -69,21 +69,31 @@ function cleanGaxiosConfig() { } export interface SQLAdminFetcherOptions { - loginAuth?: GoogleAuth; + loginAuth?: GoogleAuth | AuthClient; sqlAdminAPIEndpoint?: string; } export class SQLAdminFetcher { private readonly client: sqladmin_v1beta4.Sqladmin; - private readonly auth: GoogleAuth; + private readonly auth: GoogleAuth; constructor({loginAuth, sqlAdminAPIEndpoint}: SQLAdminFetcherOptions = {}) { - const auth = new GoogleAuth({ - scopes: ['https://www.googleapis.com/auth/sqlservice.admin'], - }); + let auth: GoogleAuth; + + if (loginAuth instanceof GoogleAuth) { + auth = new GoogleAuth({ + scopes: ['https://www.googleapis.com/auth/sqlservice.admin'], + }); + } else { + auth = new GoogleAuth({ + authClient: loginAuth, // either an `AuthClient` or undefined + scopes: ['https://www.googleapis.com/auth/sqlservice.admin'], + }); + } + this.client = new Sqladmin({ rootUrl: sqlAdminAPIEndpoint, - auth, + auth: auth as GoogleAuth, userAgentDirectives: [ { product: 'cloud-sql-nodejs-connector', @@ -92,11 +102,14 @@ export class SQLAdminFetcher { ], }); - this.auth = - loginAuth || - new GoogleAuth({ + if (loginAuth instanceof GoogleAuth) { + this.auth = loginAuth; + } else { + this.auth = new GoogleAuth({ + authClient: loginAuth, // either an `AuthClient` or undefined scopes: ['https://www.googleapis.com/auth/sqlservice.login'], }); + } } async getInstanceMetadata({ diff --git a/test/sqladmin-fetcher.ts b/test/sqladmin-fetcher.ts index 2b317a91..00029fb7 100644 --- a/test/sqladmin-fetcher.ts +++ b/test/sqladmin-fetcher.ts @@ -15,7 +15,7 @@ import {resolve} from 'node:path'; import t from 'tap'; import nock from 'nock'; -import {GoogleAuth} from 'google-auth-library'; +import {GoogleAuth, OAuth2Client} from 'google-auth-library'; import {sqladmin_v1beta4} from '@googleapis/sqladmin'; import {SQLAdminFetcher} from '../src/sqladmin-fetcher'; import {InstanceConnectionInfo} from '../src/instance-connection-info'; @@ -71,6 +71,18 @@ const mockRequest = ( }); }; +t.test('constructor', async t => { + await t.test('should support GoogleAuth for `loginAuth`', async t => { + const auth = new GoogleAuth(); + t.ok(new SQLAdminFetcher({loginAuth: auth})); + }); + + await t.test('should support `AuthClient` for `loginAuth`', async t => { + const authClient = new OAuth2Client(); + t.ok(new SQLAdminFetcher({loginAuth: authClient})); + }); +}); + t.test('getInstanceMetadata', async t => { setupCredentials(t); const instanceConnectionInfo: InstanceConnectionInfo = {