From c21304a9c43f51ab950b977bdb83ca1ae84d635c Mon Sep 17 00:00:00 2001 From: Justin Beckwith Date: Sat, 16 Mar 2019 23:11:13 -0700 Subject: [PATCH] feat: support scopes on compute credentials --- package.json | 1 + src/auth/computeclient.ts | 19 ++++++++++++++++++- src/auth/googleauth.ts | 5 +++-- test/test.compute.ts | 21 ++++++++++++++++++--- test/test.googleauth.ts | 11 +++++++++++ 5 files changed, 51 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index ecb5b2bb..3c41e03e 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "client library" ], "dependencies": { + "arrify": "^2.0.0", "base64-js": "^1.3.0", "fast-text-encoding": "^1.0.0", "gaxios": "^1.2.1", diff --git a/src/auth/computeclient.ts b/src/auth/computeclient.ts index a4c3710c..420945c1 100644 --- a/src/auth/computeclient.ts +++ b/src/auth/computeclient.ts @@ -14,9 +14,12 @@ * limitations under the License. */ +import arrify = require('arrify'); import {GaxiosError, GaxiosOptions, GaxiosPromise} from 'gaxios'; import * as gcpMetadata from 'gcp-metadata'; + import * as messages from '../messages'; + import {CredentialRequest, Credentials} from './credentials'; import {GetTokenResponse, OAuth2Client, RefreshOptions} from './oauth2client'; @@ -26,10 +29,17 @@ export interface ComputeOptions extends RefreshOptions { * may have multiple service accounts. */ serviceAccountEmail?: string; + /** + * The scopes that will be requested when acquiring service account + * credentials. Only applicable to modern App Engine and Cloud Function + * runtimes as of March 2019. + */ + scopes?: string|string[]; } export class Compute extends OAuth2Client { private serviceAccountEmail: string; + scopes: string[]; /** * Google Compute Engine service account credentials. @@ -43,6 +53,7 @@ export class Compute extends OAuth2Client { // refreshed before the first API call is made. this.credentials = {expiry_date: 1, refresh_token: 'compute-placeholder'}; this.serviceAccountEmail = options.serviceAccountEmail || 'default'; + this.scopes = arrify(options.scopes) as string[]; } /** @@ -68,7 +79,13 @@ export class Compute extends OAuth2Client { const tokenPath = `service-accounts/${this.serviceAccountEmail}/token`; let data: CredentialRequest; try { - data = await gcpMetadata.instance(tokenPath); + data = await gcpMetadata.instance({ + property: tokenPath, + params: { + scopes: this.scopes + // TODO: clean up before submit, fix upstream type bug + } as {} + }); } catch (e) { e.message = `Could not refresh access token: ${e.message}`; throw e; diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index 60a0a197..14ea1119 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -27,7 +27,7 @@ import {isBrowser} from '../isbrowser'; import * as messages from '../messages'; import {DefaultTransporter, Transporter} from '../transporters'; -import {Compute} from './computeclient'; +import {Compute, ComputeOptions} from './computeclient'; import {CredentialBody, JWTInput} from './credentials'; import {GCPEnv, getEnv} from './envDetect'; import {JWT, JWTOptions} from './jwtclient'; @@ -219,7 +219,7 @@ export class GoogleAuth { } } - private async getApplicationDefaultAsync(options?: RefreshOptions): + private async getApplicationDefaultAsync(options: RefreshOptions = {}): Promise { // If we've already got a cached credential, just return it. if (this.cachedCredential) { @@ -276,6 +276,7 @@ export class GoogleAuth { // For GCE, just return a default ComputeClient. It will take care of // the rest. + (options as ComputeOptions).scopes = this.scopes; this.cachedCredential = new Compute(options); projectId = await this.getProjectId(); return {projectId, credential: this.cachedCredential}; diff --git a/test/test.compute.ts b/test/test.compute.ts index 0e30cd7c..53e9d286 100644 --- a/test/test.compute.ts +++ b/test/test.compute.ts @@ -20,15 +20,19 @@ import * as nock from 'nock'; import * as sinon from 'sinon'; import {Compute} from '../src'; const assertRejects = require('assert-rejects'); +import * as qs from 'querystring'; nock.disableNetConnect(); const url = 'http://example.com'; - const tokenPath = `${BASE_PATH}/instance/service-accounts/default/token`; -function mockToken(statusCode = 200) { +function mockToken(statusCode = 200, scopes?: string[]) { + let path = tokenPath; + if (scopes && scopes.length > 0) { + path += '?' + qs.stringify({scopes}); + } return nock(HOST_ADDRESS) - .get(tokenPath, undefined, {reqheaders: HEADERS}) + .get(path, undefined, {reqheaders: HEADERS}) .reply(statusCode, {access_token: 'abc123', expires_in: 10000}, HEADERS); } @@ -63,6 +67,17 @@ it('should get an access token for the first request', async () => { assert.strictEqual(compute.credentials.access_token, 'abc123'); }); +it('should include scopes when asking for the token', async () => { + const scopes = [ + 'https://www.googleapis.com/reader', 'https://www.googleapis.com/auth/plus' + ]; + const nockScopes = [mockToken(200, scopes), mockExample()]; + const compute = new Compute({scopes}); + await compute.request({url}); + nockScopes.forEach(s => s.done()); + assert.strictEqual(compute.credentials.access_token, 'abc123'); +}); + it('should refresh if access token has expired', async () => { const scopes = [mockToken(), mockExample()]; compute.credentials.access_token = 'initial-access-token'; diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index 09324ef0..14c557aa 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -28,6 +28,7 @@ import {GoogleAuth, JWT, UserRefreshClient} from '../src'; import {CredentialBody} from '../src/auth/credentials'; import * as envDetect from '../src/auth/envDetect'; import {CLOUD_SDK_CLIENT_ID} from '../src/auth/googleauth'; +import {Compute} from '../src/auth/computeclient'; import * as messages from '../src/messages'; nock.disableNetConnect(); @@ -1299,6 +1300,16 @@ describe('googleauth', () => { assert.strictEqual(client.scopes, scopes); }); + it('should allow passing a scope to get a Compute client', async () => { + const scopes = ['http://examples.com/is/a/scope']; + blockGoogleApplicationCredentialEnvironmentVariable(); + auth._fileExists = () => false; + const scope = nockIsGCE(); + const client = await auth.getClient({scopes}) as Compute; + assert.strictEqual(client.scopes, scopes); + scope.done(); + }); + it('should get an access token', async () => { const {auth, scopes} = mockGCE(); scopes.push(createGetProjectIdNock());