From 1811b7fd7d0458ae386fff11aaac7710a23a348f Mon Sep 17 00:00:00 2001 From: Justin Beckwith Date: Mon, 29 Apr 2019 13:40:19 -0700 Subject: [PATCH] feat: support scopes on compute credentials (#642) --- package.json | 2 ++ src/auth/computeclient.ts | 19 ++++++++++++++++++- src/auth/googleauth.ts | 5 +++-- test/test.compute.ts | 21 ++++++++++++++++++--- test/test.googleauth.ts | 9 +++++++++ 5 files changed, 50 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index d289c657..75c54f70 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "client library" ], "dependencies": { + "arrify": "^1.0.1", "base64-js": "^1.3.0", "fast-text-encoding": "^1.0.0", "gaxios": "^1.2.1", @@ -29,6 +30,7 @@ }, "devDependencies": { "@compodoc/compodoc": "^1.1.7", + "@types/arrify": "^1.0.4", "@types/base64-js": "^1.2.5", "@types/chai": "^4.1.7", "@types/execa": "^0.9.0", diff --git a/src/auth/computeclient.ts b/src/auth/computeclient.ts index 52fb8c07..d06343a1 100644 --- a/src/auth/computeclient.ts +++ b/src/auth/computeclient.ts @@ -14,9 +14,12 @@ * limitations under the License. */ +import * as arrify from '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); } /** @@ -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}`; this.wrapError(e); diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index 7aeaa4a8..86288312 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 244af33d..7f340964 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); } @@ -62,6 +66,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 ccf5543c..2bdac0ff 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -29,6 +29,7 @@ const assertRejects = require('assert-rejects'); import {GoogleAuth, JWT, UserRefreshClient} from '../src'; import {CredentialBody} from '../src/auth/credentials'; import * as envDetect from '../src/auth/envDetect'; +import {Compute} from '../src/auth/computeclient'; import * as messages from '../src/messages'; nock.disableNetConnect(); @@ -1136,6 +1137,14 @@ 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']; + const nockScopes = [nockIsGCE(), createGetProjectIdNock()]; + const client = await auth.getClient({scopes}) as Compute; + assert.strictEqual(client.scopes, scopes); + nockScopes.forEach(x => x.done()); + }); + it('should get an access token', async () => { const {auth, scopes} = mockGCE(); scopes.push(createGetProjectIdNock());