Skip to content

Commit

Permalink
feat: support scopes on compute credentials
Browse files Browse the repository at this point in the history
  • Loading branch information
JustinBeckwith committed Apr 8, 2019
1 parent cd0bf45 commit c21304a
Show file tree
Hide file tree
Showing 5 changed files with 51 additions and 6 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
19 changes: 18 additions & 1 deletion src/auth/computeclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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.
Expand All @@ -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[];
}

/**
Expand All @@ -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;
Expand Down
5 changes: 3 additions & 2 deletions src/auth/googleauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -219,7 +219,7 @@ export class GoogleAuth {
}
}

private async getApplicationDefaultAsync(options?: RefreshOptions):
private async getApplicationDefaultAsync(options: RefreshOptions = {}):
Promise<ADCResponse> {
// If we've already got a cached credential, just return it.
if (this.cachedCredential) {
Expand Down Expand Up @@ -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};
Expand Down
21 changes: 18 additions & 3 deletions test/test.compute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down Expand Up @@ -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';
Expand Down
11 changes: 11 additions & 0 deletions test/test.googleauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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());
Expand Down

0 comments on commit c21304a

Please sign in to comment.