Skip to content

fix: Set the Quota Project ID only for ADC human accounts #2761

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

Merged
merged 3 commits into from
Nov 8, 2024
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/app/credential-internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export class ApplicationDefaultCredential implements Credential {
private readonly googleAuth: GoogleAuth;
private authClient: AnyAuthClient;
private projectId?: string;
private quotaProjectId?: string;
private accountId?: string;

constructor(httpAgent?: Agent) {
Expand All @@ -58,6 +59,7 @@ export class ApplicationDefaultCredential implements Credential {
}
await this.authClient.getAccessToken();
const credentials = this.authClient.credentials;
this.quotaProjectId = this.authClient.quotaProjectId;
return populateCredential(credentials);
}

Expand All @@ -68,6 +70,13 @@ export class ApplicationDefaultCredential implements Credential {
return Promise.resolve(this.projectId);
}

public getQuotaProjectId(): string | undefined {
if (!this.quotaProjectId) {
this.quotaProjectId = this.authClient?.quotaProjectId;
}
return this.quotaProjectId;
}

public async isComputeEngineCredential(): Promise<boolean> {
if (!this.authClient) {
this.authClient = await this.googleAuth.getClient();
Expand Down
21 changes: 17 additions & 4 deletions src/utils/api-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import url = require('url');
import { EventEmitter } from 'events';
import { Readable } from 'stream';
import * as zlibmod from 'zlib';
import { ApplicationDefaultCredential } from '../app/credential-internal';
import { getMetricsHeader } from '../utils/index';

/** Http method type definition. */
Expand Down Expand Up @@ -1077,10 +1078,13 @@ export class AuthorizedHttpClient extends HttpClient {
const authHeader = 'Authorization';
requestCopy.headers[authHeader] = `Bearer ${token}`;

// Fix issue where firebase-admin does not specify quota project that is
// necessary for use when utilizing human account with ADC (RSDF)
if (!requestCopy.headers['x-goog-user-project'] && this.app.options.projectId) {
requestCopy.headers['x-goog-user-project'] = this.app.options.projectId
let quotaProjectId: string | undefined;
if (this.app.options.credential instanceof ApplicationDefaultCredential) {
quotaProjectId = this.app.options.credential.getQuotaProjectId();
}
quotaProjectId = process.env.GOOGLE_CLOUD_QUOTA_PROJECT || quotaProjectId;
if (!requestCopy.headers['x-goog-user-project'] && validator.isNonEmptyString(quotaProjectId)) {
requestCopy.headers['x-goog-user-project'] = quotaProjectId;
}

if (!requestCopy.httpAgent && this.app.options.httpAgent) {
Expand Down Expand Up @@ -1112,6 +1116,15 @@ export class AuthorizedHttp2Client extends Http2Client {
const authHeader = 'Authorization';
requestCopy.headers[authHeader] = `Bearer ${token}`;

let quotaProjectId: string | undefined;
if (this.app.options.credential instanceof ApplicationDefaultCredential) {
quotaProjectId = this.app.options.credential.getQuotaProjectId();
}
quotaProjectId = process.env.GOOGLE_CLOUD_QUOTA_PROJECT || quotaProjectId;
if (!requestCopy.headers['x-goog-user-project'] && validator.isNonEmptyString(quotaProjectId)) {
requestCopy.headers['x-goog-user-project'] = quotaProjectId;
}

requestCopy.headers['X-Goog-Api-Client'] = getMetricsHeader()

return super.send(requestCopy);
Expand Down
1 change: 0 additions & 1 deletion test/unit/app-check/app-check-api-client-internal.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ describe('AppCheckApiClient', () => {
const EXPECTED_HEADERS = {
'Authorization': 'Bearer mock-token',
'X-Firebase-Client': `fire-admin-node/${getSdkVersion()}`,
'x-goog-user-project': 'test-project',
'X-Goog-Api-Client': getMetricsHeader(),
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ describe('DataConnectApiClient', () => {
const EXPECTED_HEADERS = {
'Authorization': 'Bearer mock-token',
'X-Firebase-Client': `fire-admin-node/${getSdkVersion()}`,
'x-goog-user-project': 'test-project',
'X-Goog-Api-Client': getMetricsHeader(),
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ describe('Extension API client', () => {
const EXPECTED_HEADERS = {
'Authorization': 'Bearer mock-token',
'X-Firebase-Client': `fire-admin-node/${getSdkVersion()}`,
'x-goog-user-project': 'test-project',
'X-Goog-Api-Client': getMetricsHeader(),
}

Expand Down
1 change: 0 additions & 1 deletion test/unit/functions/functions-api-client-internal.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ describe('FunctionsApiClient', () => {
const EXPECTED_HEADERS = {
'X-Firebase-Client': `fire-admin-node/${getSdkVersion()}`,
'Authorization': 'Bearer mock-token',
'x-goog-user-project': 'test-project',
'X-Goog-Api-Client': getMetricsHeader(),
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,6 @@ describe('MachineLearningApiClient', () => {
const EXPECTED_HEADERS = {
'Authorization': 'Bearer mock-token',
'X-Firebase-Client': `fire-admin-node/${getSdkVersion()}`,
'x-goog-user-project': 'test-project',
'X-Goog-Api-Client': getMetricsHeader(),
};
const noProjectId = 'Failed to determine project ID. Initialize the SDK with service '
Expand Down
1 change: 0 additions & 1 deletion test/unit/remote-config/remote-config-api-client.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ describe('RemoteConfigApiClient', () => {
'Authorization': 'Bearer mock-token',
'X-Firebase-Client': `fire-admin-node/${getSdkVersion()}`,
'Accept-Encoding': 'gzip',
'x-goog-user-project': 'test-project',
'X-Goog-Api-Client': getMetricsHeader(),
};

Expand Down
1 change: 0 additions & 1 deletion test/unit/security-rules/security-rules-api-client.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ describe('SecurityRulesApiClient', () => {
const EXPECTED_HEADERS = {
'Authorization': 'Bearer mock-token',
'X-Firebase-Client': `fire-admin-node/${getSdkVersion()}`,
'x-goog-user-project': 'test-project',
'X-Goog-Api-Client': getMetricsHeader(),
};
const noProjectId = 'Failed to determine project ID. Initialize the SDK with service '
Expand Down
41 changes: 41 additions & 0 deletions test/unit/utils/api-request.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

'use strict';

import * as _ from 'lodash';
import * as chai from 'chai';
import * as nock from 'nock';
import * as sinon from 'sinon';
Expand All @@ -35,6 +36,7 @@ import {
import { deepCopy } from '../../../src/utils/deep-copy';
import { Agent } from 'http';
import * as zlib from 'zlib';
import { getMetricsHeader } from '../../../src/utils';

chai.should();
chai.use(sinonChai);
Expand Down Expand Up @@ -2648,6 +2650,45 @@ describe('AuthorizedHttpClient', () => {
});
});

describe('Quota Project', () => {
let stubs: sinon.SinonStub[] = [];

afterEach(() => {
_.forEach(stubs, (stub) => stub.restore());
stubs = [];
if (process.env.GOOGLE_CLOUD_QUOTA_PROJECT) {
delete process.env.GOOGLE_CLOUD_QUOTA_PROJECT;
}
});

it('should include quota project id in headers when GOOGLE_CLOUD_QUOTA_PROJECT is set', () => {
const reqData = { request: 'data' };
const stub = sinon
.stub(HttpClient.prototype, 'send')
.resolves(utils.responseFrom({}, 200));
stubs.push(stub);
process.env.GOOGLE_CLOUD_QUOTA_PROJECT = 'test-project-id';
const client = new AuthorizedHttpClient(mockApp);
return client.send({
method: 'POST',
url: mockUrl,
data: reqData,
})
.then(() => {
expect(stub).to.have.been.calledOnce.and.calledWith({
method: 'POST',
url: mockUrl,
headers: {
...requestHeaders.reqheaders,
'x-goog-user-project': 'test-project-id',
'X-Goog-Api-Client': getMetricsHeader(),
},
data: reqData
});
});
});
});

it('should not mutate the arguments', () => {
const reqData = { request: 'data' };
const options = {
Expand Down