Skip to content

Commit 7e8a932

Browse files
authored
feat: add quotaProject support to existing credentials (#724)
1 parent 7efc541 commit 7e8a932

File tree

7 files changed

+133
-10
lines changed

7 files changed

+133
-10
lines changed

googleapis_auth/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 2.2.0-wip
2+
3+
- Added `quotaProject` support to existing credentials classes
4+
(`ServiceAccountCredentials`, `ClientViaServiceAccount`, `ClientFromFlow`).
5+
16
## 2.1.0
27

38
- `AuthClientSigningExtension`: Added `sign()` which accepts an optional

googleapis_auth/lib/src/auth_functions.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,14 @@ Client clientViaApiKey(String apiKey, {Client? baseClient}) {
5252
/// [baseClient].
5353
///
5454
/// {@macro googleapis_auth_not_close_the_baseClient}
55+
///
56+
/// If [quotaProject] is provided, it will be added to the `X-Goog-User-Project`
57+
/// header for all requests.
5558
AuthClient authenticatedClient(
5659
Client baseClient,
5760
AccessCredentials credentials, {
5861
bool closeUnderlyingClient = false,
62+
String? quotaProject,
5963
}) {
6064
if (credentials.accessToken.type != 'Bearer') {
6165
throw ArgumentError('Only Bearer access tokens are accepted.');
@@ -64,6 +68,7 @@ AuthClient authenticatedClient(
6468
baseClient,
6569
credentials,
6670
closeUnderlyingClient: closeUnderlyingClient,
71+
quotaProject: quotaProject,
6772
);
6873
}
6974

googleapis_auth/lib/src/oauth2_flows/base_flow.dart

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,12 @@ abstract class BaseFlow {
1414
Future<AccessCredentials> run();
1515
}
1616

17+
/// If [quotaProject] is provided, it will be added to the `X-Goog-User-Project`
18+
/// header for all requests.
1719
Future<AutoRefreshingAuthClient> clientFromFlow(
1820
BaseFlow Function(Client client) flowFactory, {
1921
Client? baseClient,
22+
String? quotaProject,
2023
}) async {
2124
if (baseClient == null) {
2225
baseClient = Client();
@@ -28,7 +31,7 @@ Future<AutoRefreshingAuthClient> clientFromFlow(
2831

2932
try {
3033
final credentials = await flow.run();
31-
return _FlowClient(baseClient, credentials, flow);
34+
return _FlowClient(baseClient, credentials, flow, quotaProject);
3235
} catch (e) {
3336
baseClient.close();
3437
rethrow;
@@ -38,21 +41,29 @@ Future<AutoRefreshingAuthClient> clientFromFlow(
3841
// Will close the underlying `http.Client`.
3942
class _FlowClient extends AutoRefreshDelegatingClient {
4043
final BaseFlow _flow;
41-
@override
42-
AccessCredentials credentials;
43-
Client _authClient;
44+
final String? _quotaProject;
45+
46+
AccessCredentials _credentials;
47+
late Client _authClient;
48+
49+
_FlowClient(super.client, this._credentials, this._flow, this._quotaProject) {
50+
_authClient = _recreateClient(_credentials);
51+
}
4452

45-
_FlowClient(super.client, this.credentials, this._flow)
46-
: _authClient = authenticatedClient(client, credentials);
53+
@override
54+
AccessCredentials get credentials => _credentials;
4755

4856
@override
4957
Future<StreamedResponse> send(BaseRequest request) async {
50-
if (credentials.accessToken.hasExpired) {
58+
if (_credentials.accessToken.hasExpired) {
5159
final newCredentials = await _flow.run();
5260
notifyAboutNewCredentials(newCredentials);
53-
credentials = newCredentials;
54-
_authClient = authenticatedClient(baseClient, credentials);
61+
_credentials = newCredentials;
62+
_authClient = _recreateClient(newCredentials);
5563
}
5664
return _authClient.send(request);
5765
}
66+
67+
Client _recreateClient(AccessCredentials credentials) =>
68+
authenticatedClient(baseClient, credentials, quotaProject: _quotaProject);
5869
}

googleapis_auth/lib/src/service_account_client.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,17 @@ Future<AccessCredentials> obtainAccessCredentialsViaServiceAccount(
3939
///
4040
/// {@macro googleapis_auth_close_the_client}
4141
/// {@macro googleapis_auth_not_close_the_baseClient}
42+
///
43+
/// If [quotaProject] is provided, it will be added to the `X-Goog-User-Project`
44+
/// header for all requests.
45+
///
46+
/// Otherwise, the [ServiceAccountCredentials.quotaProject] property on
47+
/// [clientCredentials] will be used.
4248
Future<AutoRefreshingAuthClient> clientViaServiceAccount(
4349
ServiceAccountCredentials clientCredentials,
4450
List<String> scopes, {
4551
Client? baseClient,
52+
String? quotaProject,
4653
}) async => await clientFromFlow(
4754
(c) => JwtFlow(
4855
clientCredentials.email,
@@ -52,4 +59,5 @@ Future<AutoRefreshingAuthClient> clientViaServiceAccount(
5259
c,
5360
),
5461
baseClient: baseClient,
62+
quotaProject: quotaProject ?? clientCredentials.quotaProject,
5563
);

googleapis_auth/lib/src/service_account_credentials.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ class ServiceAccountCredentials {
4343
/// (e.g., Government Cloud or other isolated environments).
4444
final String universeDomain;
4545

46+
/// The quota project to use for requests.
47+
final String? quotaProject;
48+
4649
/// Private key as an [RSAPrivateKey].
4750
final RSAPrivateKey privateRSAKey;
4851

@@ -75,6 +78,7 @@ class ServiceAccountCredentials {
7578
projectId: map['project_id'] as String?,
7679
universeDomain:
7780
map['universe_domain'] as String? ?? defaultUniverseDomain,
81+
quotaProject: map['quota_project_id'] as String?,
7882
),
7983
final Map map when map['type'] != 'service_account' =>
8084
throw ArgumentError(
@@ -109,13 +113,17 @@ class ServiceAccountCredentials {
109113
/// The optional named argument [universeDomain] specifies the universe
110114
/// domain.
111115
/// Defaults to [defaultUniverseDomain] if not provided.
116+
///
117+
/// The optional named argument [quotaProject] specifies the quota project
118+
/// to use for requests.
112119
ServiceAccountCredentials(
113120
this.email,
114121
this.clientId,
115122
this.privateKey, {
116123
this.impersonatedUser,
117124
this.projectId,
118125
this.universeDomain = defaultUniverseDomain,
126+
this.quotaProject,
119127
}) : privateRSAKey = keyFromString(privateKey);
120128

121129
/// Signs the given [data] using RSA-SHA256 with this service account's

googleapis_auth/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: googleapis_auth
2-
version: 2.1.0
2+
version: 2.2.0-wip
33
description: Obtain Access credentials for Google services using OAuth 2.0
44
repository: https://github.com/google/googleapis.dart/tree/master/googleapis_auth
55

googleapis_auth/test/oauth2_test.dart

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ void main() {
7474
'type': 'service_account',
7575
'project_id': 'test-project',
7676
'universe_domain': 'example.com',
77+
'quota_project_id': 'test-quota',
7778
};
7879

7980
test('from valid individual params', () {
@@ -82,13 +83,15 @@ void main() {
8283
clientId,
8384
testPrivateKeyString,
8485
projectId: 'test-project',
86+
quotaProject: 'test-quota',
8587
);
8688
expect(credentials.email, 'email');
8789
expect(credentials.clientId, clientId);
8890
expect(credentials.privateKey, testPrivateKeyString);
8991
expect(credentials.impersonatedUser, isNull);
9092
expect(credentials.projectId, 'test-project');
9193
expect(credentials.universeDomain, defaultUniverseDomain);
94+
expect(credentials.quotaProject, 'test-quota');
9295
});
9396

9497
test('from valid individual params with user', () {
@@ -98,13 +101,15 @@ void main() {
98101
testPrivateKeyString,
99102
impersonatedUser: 'x@y.com',
100103
projectId: 'test-project',
104+
quotaProject: 'test-quota',
101105
);
102106
expect(credentials.email, 'email');
103107
expect(credentials.clientId, clientId);
104108
expect(credentials.privateKey, testPrivateKeyString);
105109
expect(credentials.impersonatedUser, 'x@y.com');
106110
expect(credentials.projectId, 'test-project');
107111
expect(credentials.universeDomain, defaultUniverseDomain);
112+
expect(credentials.quotaProject, 'test-quota');
108113
});
109114

110115
test('from JSON string', () {
@@ -118,6 +123,7 @@ void main() {
118123
expect(credentialsFromJson.impersonatedUser, isNull);
119124
expect(credentialsFromJson.projectId, 'test-project');
120125
expect(credentialsFromJson.universeDomain, 'example.com');
126+
expect(credentialsFromJson.quotaProject, 'test-quota');
121127
});
122128

123129
test('from JSON string with user', () {
@@ -132,6 +138,7 @@ void main() {
132138
expect(credentialsFromJson.impersonatedUser, 'x@y.com');
133139
expect(credentialsFromJson.projectId, 'test-project');
134140
expect(credentialsFromJson.universeDomain, 'example.com');
141+
expect(credentialsFromJson.quotaProject, 'test-quota');
135142
});
136143

137144
test('from JSON map', () {
@@ -145,6 +152,7 @@ void main() {
145152
expect(credentialsFromJson.impersonatedUser, isNull);
146153
expect(credentialsFromJson.projectId, 'test-project');
147154
expect(credentialsFromJson.universeDomain, 'example.com');
155+
expect(credentialsFromJson.quotaProject, 'test-quota');
148156
});
149157

150158
test('from JSON map with user', () {
@@ -159,6 +167,7 @@ void main() {
159167
expect(credentialsFromJson.impersonatedUser, 'x@y.com');
160168
expect(credentialsFromJson.projectId, 'test-project');
161169
expect(credentialsFromJson.universeDomain, 'example.com');
170+
expect(credentialsFromJson.quotaProject, 'test-quota');
162171
});
163172

164173
test('sign data', () {
@@ -336,6 +345,35 @@ void main() {
336345
expect(response.statusCode, 204);
337346
});
338347

348+
test('successful request with quotaProject', () async {
349+
final client = authenticatedClient(
350+
mockClient(
351+
expectAsync1((request) async {
352+
expect(request.method, 'POST');
353+
expect(request.url, url);
354+
expect(request.headers, hasLength(2));
355+
expect(
356+
request.headers,
357+
containsPair('Authorization', 'Bearer bar'),
358+
);
359+
expect(
360+
request.headers,
361+
containsPair('x-goog-user-project', 'test-quota-project'),
362+
);
363+
364+
return Response('', 204);
365+
}),
366+
expectClose: false,
367+
),
368+
credentials,
369+
quotaProject: 'test-quota-project',
370+
);
371+
expect(client.credentials, credentials);
372+
373+
final response = await client.send(RequestImpl('POST', url));
374+
expect(response.statusCode, 204);
375+
});
376+
339377
test('throws on access denied', () {
340378
final client = authenticatedClient(
341379
mockClient(
@@ -555,6 +593,54 @@ void main() {
555593
client.close();
556594
});
557595

596+
test('clientViaServiceAccount sends quotaProject header', () async {
597+
final credentials = ServiceAccountCredentials.fromJson({
598+
'private_key_id': '301029',
599+
'private_key': testPrivateKeyString,
600+
'client_email': 'test@test.iam.gserviceaccount.com',
601+
'client_id': 'myid',
602+
'type': 'service_account',
603+
'quota_project_id': 'test-quota-project',
604+
});
605+
606+
var callCount = 0;
607+
final client = await clientViaServiceAccount(
608+
credentials,
609+
['https://www.googleapis.com/auth/cloud-platform'],
610+
baseClient: mockClient(
611+
expectAsync1((request) async {
612+
if (callCount == 0) {
613+
expect(request.method, 'POST');
614+
expect(request.url, googleOauth2TokenEndpoint);
615+
callCount++;
616+
return Response(
617+
jsonEncode({
618+
'access_token': 'test_token',
619+
'token_type': 'Bearer',
620+
'expires_in': 3600,
621+
}),
622+
200,
623+
headers: jsonContentType,
624+
);
625+
} else {
626+
expect(
627+
request.headers,
628+
containsPair('x-goog-user-project', 'test-quota-project'),
629+
);
630+
callCount++;
631+
return Response('', 200);
632+
}
633+
}, count: 2),
634+
expectClose: false,
635+
),
636+
);
637+
638+
final response = await client.get(Uri.parse('http://example.com'));
639+
expect(response.statusCode, 200);
640+
641+
client.close();
642+
});
643+
558644
test('clientViaMetadataServer returns null credentials', () async {
559645
final client = await clientViaMetadataServer(
560646
baseClient: mockClient(

0 commit comments

Comments
 (0)