Skip to content

Commit 7c4e01f

Browse files
authored
Make auth universe-aware (#352)
This adds support for making the action "universe" aware, so it will be usable for TPC and GDCH.
1 parent 097d292 commit 7c4e01f

13 files changed

+298
-230
lines changed

README.md

+12
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,18 @@ regardless of the authentication mechanism.
273273
identities to use for impersonation in the chain. By default there are no
274274
delegates.
275275
276+
- `universe`: (Optional) The Google Cloud universe to use for constructing API
277+
endpoints. The default universe is "googleapis.com", which corresponds to
278+
https://cloud.google.com. Trusted Partner Cloud and Google Distributed
279+
Hosted Cloud should set this to their universe address.
280+
281+
You can also override individual API endpoints by setting the environment variable `GHA_ENDPOINT_OVERRIDE_<endpoint>` where endpoint is the API endpoint to override. This only applies to the `auth` action and does not persist to other steps. For example:
282+
283+
```yaml
284+
env:
285+
GHA_ENDPOINT_OVERRIDE_oauth2: 'https://oauth2.myapi.endpoint/v1'
286+
```
287+
276288
- `cleanup_credentials`: (Optional) If true, the action will remove any
277289
created credentials from the filesystem upon completion. This only applies
278290
if "create_credentials_file" is true. The default is true.

action.yml

+8
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,14 @@ inputs:
9494
impersonation in the chain.
9595
default: ''
9696
required: false
97+
universe:
98+
description: |-
99+
The Google Cloud universe to use for constructing API endpoints. The
100+
default universe is "googleapis.com", which corresponds to
101+
https://cloud.google.com. Trusted Partner Cloud and Google Distributed
102+
Hosted Cloud should set this to their universe address.
103+
required: false
104+
default: 'googleapis.com'
97105
cleanup_credentials:
98106
description: |-
99107
If true, the action will remove any created credentials from the

dist/main/index.js

+3-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/client/auth_client.ts

-36
This file was deleted.

src/client/client.ts

+106
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// Copyright 2023 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import { HttpClient } from '@actions/http-client';
16+
17+
import { Logger } from '../logger';
18+
import { userAgent } from '../utils';
19+
20+
/**
21+
* AuthClient is the default HTTP client for interacting with the IAM credentials
22+
* API.
23+
*/
24+
export interface AuthClient {
25+
/**
26+
* getToken() gets or generates the best token for the auth client.
27+
*/
28+
getToken(): Promise<string>;
29+
30+
/**
31+
* createCredentialsFile creates a credential file (for use with gcloud and
32+
* other Google Cloud tools) that instructs the tool how to perform identity
33+
* federation.
34+
*/
35+
createCredentialsFile(outputPath: string): Promise<string>;
36+
37+
/**
38+
* signJWT signs a JWT using the auth provider.
39+
*/
40+
signJWT(claims: any): Promise<string>;
41+
}
42+
43+
export interface ClientParameters {
44+
logger: Logger;
45+
universe: string;
46+
child: string;
47+
}
48+
49+
export class Client {
50+
protected readonly _logger: Logger;
51+
protected readonly _httpClient: HttpClient;
52+
53+
protected readonly _endpoints = {
54+
iam: 'https://iam.{universe}/v1',
55+
iamcredentials: 'https://iamcredentials.{universe}/v1',
56+
oauth2: 'https://oauth2.{universe}',
57+
sts: 'https://sts.{universe}/v1',
58+
www: 'https://www.{universe}',
59+
};
60+
protected readonly _universe;
61+
62+
constructor(opts: ClientParameters) {
63+
this._logger = opts.logger.withNamespace(opts.child);
64+
65+
// Create the http client with our user agent.
66+
this._httpClient = new HttpClient(userAgent, undefined, {
67+
allowRedirects: true,
68+
allowRetries: true,
69+
keepAlive: true,
70+
maxRedirects: 5,
71+
maxRetries: 3,
72+
});
73+
74+
// Expand universe to support TPC and custom endpoints.
75+
this._universe = opts.universe;
76+
for (const key of Object.keys(this._endpoints) as Array<keyof typeof this._endpoints>) {
77+
this._endpoints[key] = this.expandEndpoint(key);
78+
}
79+
this._logger.debug(`Computed endpoints for universe ${this._universe}`, this._endpoints);
80+
}
81+
82+
expandEndpoint(key: keyof typeof this._endpoints): string {
83+
const envOverrideKey = `GHA_ENDPOINT_OVERRIDE_${key}`;
84+
const envOverrideValue = process.env[envOverrideKey];
85+
if (envOverrideValue && envOverrideValue !== '') {
86+
this._logger.debug(
87+
`Overriding API endpoint for ${key} because ${envOverrideKey} is set`,
88+
envOverrideValue,
89+
);
90+
return envOverrideValue.replace(/\/+$/, '');
91+
}
92+
93+
return (this._endpoints[key] || '').replace(/{universe}/g, this._universe).replace(/\/+$/, '');
94+
}
95+
}
96+
export { IAMCredentialsClient, IAMCredentialsClientParameters } from './iamcredentials';
97+
98+
export {
99+
ServiceAccountKeyClient,
100+
ServiceAccountKeyClientParameters,
101+
} from './service_account_key_json';
102+
103+
export {
104+
WorkloadIdentityFederationClient,
105+
WorkloadIdentityFederationClientParameters,
106+
} from './workload_identity_federation';

src/base.ts renamed to src/client/iamcredentials.ts

+34-43
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@
1414

1515
import { URLSearchParams } from 'url';
1616

17-
import { HttpClient } from '@actions/http-client';
17+
import { errorMessage } from '@google-github-actions/actions-utils';
1818

19-
import { Logger } from './logger';
20-
import { expandEndpoint, userAgent } from './utils';
19+
import { Client } from './client';
20+
import { Logger } from '../logger';
2121

2222
/**
2323
* GenerateAccessTokenParameters are the inputs to the generateAccessToken call.
@@ -43,35 +43,27 @@ export interface GenerateIDTokenParameters {
4343
* IAMCredentialsClientParameters are the inputs to the IAM client.
4444
*/
4545
export interface IAMCredentialsClientParameters {
46+
readonly logger: Logger;
47+
readonly universe: string;
48+
4649
readonly authToken: string;
4750
}
4851

4952
/**
5053
* IAMCredentialsClient is a thin HTTP client around the Google Cloud IAM
5154
* Credentials API.
5255
*/
53-
export class IAMCredentialsClient {
54-
readonly #logger: Logger;
55-
readonly #httpClient: HttpClient;
56+
export class IAMCredentialsClient extends Client {
5657
readonly #authToken: string;
5758

58-
readonly #universe: string = 'googleapis.com';
59-
readonly #endpoints = {
60-
iamcredentials: 'https://iamcredentials.{universe}/v1',
61-
oauth2: 'https://oauth2.{universe}',
62-
};
63-
64-
constructor(logger: Logger, opts: IAMCredentialsClientParameters) {
65-
this.#logger = logger.withNamespace(this.constructor.name);
66-
this.#httpClient = new HttpClient(userAgent);
59+
constructor(opts: IAMCredentialsClientParameters) {
60+
super({
61+
logger: opts.logger,
62+
universe: opts.universe,
63+
child: `IAMCredentialsClient`,
64+
});
6765

6866
this.#authToken = opts.authToken;
69-
70-
const endpoints = this.#endpoints;
71-
for (const key of Object.keys(this.#endpoints) as Array<keyof typeof endpoints>) {
72-
this.#endpoints[key] = expandEndpoint(this.#endpoints[key], this.#universe);
73-
}
74-
this.#logger.debug(`Computed endpoints`, this.#endpoints);
7567
}
7668

7769
/**
@@ -84,7 +76,9 @@ export class IAMCredentialsClient {
8476
scopes,
8577
lifetime,
8678
}: GenerateAccessTokenParameters): Promise<string> {
87-
const pth = `${this.#endpoints.iamcredentials}/projects/-/serviceAccounts/${serviceAccount}:generateAccessToken`;
79+
const logger = this._logger.withNamespace('generateAccessToken');
80+
81+
const pth = `${this._endpoints.iamcredentials}/projects/-/serviceAccounts/${serviceAccount}:generateAccessToken`;
8882

8983
const headers = { Authorization: `Bearer ${this.#authToken}` };
9084

@@ -100,15 +94,15 @@ export class IAMCredentialsClient {
10094
body.lifetime = `${lifetime}s`;
10195
}
10296

103-
this.#logger.withNamespace('generateAccessToken').debug({
97+
logger.debug(`Built request`, {
10498
method: `POST`,
10599
path: pth,
106100
headers: headers,
107101
body: body,
108102
});
109103

110104
try {
111-
const resp = await this.#httpClient.postJson<{ accessToken: string }>(pth, body, headers);
105+
const resp = await this._httpClient.postJson<{ accessToken: string }>(pth, body, headers);
112106
const statusCode = resp.statusCode || 500;
113107
if (statusCode < 200 || statusCode > 299) {
114108
throw new Error(`Failed to call ${pth}: HTTP ${statusCode}: ${resp.result || '[no body]'}`);
@@ -120,14 +114,17 @@ export class IAMCredentialsClient {
120114
}
121115
return result.accessToken;
122116
} catch (err) {
117+
const msg = errorMessage(err);
123118
throw new Error(
124-
`Failed to generate Google Cloud OAuth 2.0 Access Token for ${serviceAccount}: ${err}`,
119+
`Failed to generate Google Cloud OAuth 2.0 Access Token for ${serviceAccount}: ${msg}`,
125120
);
126121
}
127122
}
128123

129124
async generateDomainWideDelegationAccessToken(assertion: string): Promise<string> {
130-
const pth = `${this.#endpoints.oauth2}/token`;
125+
const logger = this._logger.withNamespace('generateDomainWideDelegationAccessToken');
126+
127+
const pth = `${this._endpoints.oauth2}/token`;
131128

132129
const headers = {
133130
'Accept': 'application/json',
@@ -138,15 +135,15 @@ export class IAMCredentialsClient {
138135
body.append('grant_type', 'urn:ietf:params:oauth:grant-type:jwt-bearer');
139136
body.append('assertion', assertion);
140137

141-
this.#logger.withNamespace('generateDomainWideDelegationAccessToken').debug({
138+
logger.debug(`Built request`, {
142139
method: `POST`,
143140
path: pth,
144141
headers: headers,
145142
body: body,
146143
});
147144

148145
try {
149-
const resp = await this.#httpClient.post(pth, body.toString(), headers);
146+
const resp = await this._httpClient.post(pth, body.toString(), headers);
150147
const respBody = await resp.readBody();
151148
const statusCode = resp.message.statusCode || 500;
152149
if (statusCode < 200 || statusCode > 299) {
@@ -155,8 +152,9 @@ export class IAMCredentialsClient {
155152
const parsed = JSON.parse(respBody) as { accessToken: string };
156153
return parsed.accessToken;
157154
} catch (err) {
155+
const msg = errorMessage(err);
158156
throw new Error(
159-
`Failed to generate Google Cloud Domain Wide Delegation OAuth 2.0 Access Token: ${err}`,
157+
`Failed to generate Google Cloud Domain Wide Delegation OAuth 2.0 Access Token: ${msg}`,
160158
);
161159
}
162160
}
@@ -171,7 +169,9 @@ export class IAMCredentialsClient {
171169
delegates,
172170
includeEmail,
173171
}: GenerateIDTokenParameters): Promise<string> {
174-
const pth = `${this.#endpoints.iamcredentials}/projects/-/serviceAccounts/${serviceAccount}:generateIdToken`;
172+
const logger = this._logger.withNamespace('generateIDToken');
173+
174+
const pth = `${this._endpoints.iamcredentials}/projects/-/serviceAccounts/${serviceAccount}:generateIdToken`;
175175

176176
const headers = { Authorization: `Bearer ${this.#authToken}` };
177177

@@ -183,15 +183,15 @@ export class IAMCredentialsClient {
183183
body.delegates = delegates;
184184
}
185185

186-
this.#logger.withNamespace('generateIDToken').debug({
186+
logger.debug(`Built request`, {
187187
method: `POST`,
188188
path: pth,
189189
headers: headers,
190190
body: body,
191191
});
192192

193193
try {
194-
const resp = await this.#httpClient.postJson<{ token: string }>(pth, body, headers);
194+
const resp = await this._httpClient.postJson<{ token: string }>(pth, body, headers);
195195
const statusCode = resp.statusCode || 500;
196196
if (statusCode < 200 || statusCode > 299) {
197197
throw new Error(`Failed to call ${pth}: HTTP ${statusCode}: ${resp.result || '[no body]'}`);
@@ -203,19 +203,10 @@ export class IAMCredentialsClient {
203203
}
204204
return result.token;
205205
} catch (err) {
206+
const msg = errorMessage(err);
206207
throw new Error(
207-
`Failed to generate Google Cloud OpenID Connect ID token for ${serviceAccount}: ${err}`,
208+
`Failed to generate Google Cloud OpenID Connect ID token for ${serviceAccount}: ${msg}`,
208209
);
209210
}
210211
}
211212
}
212-
213-
export { AuthClient } from './client/auth_client';
214-
export {
215-
ServiceAccountKeyClientParameters,
216-
ServiceAccountKeyClient,
217-
} from './client/credentials_json_client';
218-
export {
219-
WorkloadIdentityFederationClientParameters,
220-
WorkloadIdentityFederationClient,
221-
} from './client/workload_identity_client';

0 commit comments

Comments
 (0)