Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support scopes on compute credentials #642

Merged
merged 2 commits into from Apr 29, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions package.json
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
19 changes: 18 additions & 1 deletion src/auth/computeclient.ts
Expand Up @@ -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';

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[];
alexander-fenster marked this conversation as resolved.
Show resolved Hide resolved
}

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);
JustinBeckwith marked this conversation as resolved.
Show resolved Hide resolved
}

/**
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this still a TODO prior to landing?

} as {}
});
} catch (e) {
e.message = `Could not refresh access token: ${e.message}`;
this.wrapError(e);
Expand Down
5 changes: 3 additions & 2 deletions src/auth/googleauth.ts
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
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 @@ -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';
Expand Down
9 changes: 9 additions & 0 deletions test/test.googleauth.ts
Expand Up @@ -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();
Expand Down Expand Up @@ -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());
Expand Down