Skip to content
Open
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
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,19 @@ Identity Federation.
Federation](#direct-wif). If this input is provided, the GitHub Action will use
[Workload Identity Federation through a Service Account](#indirect-wif).

- `service_account_impersonation_lifetime`: (Optional) Desired lifetime duration
of service account access tokens requested using the generated WIF credentials
file, in seconds. Specify the number of seconds with a trailing "s"
(e.g. 900s). This only applies when `workload_identity_provider` and
`service_account` are specified and `create_credentials_file` is true.
If omitted, authentication libraries use their default lifetime of 1 hour
(3600s).

The minimum value is 600s and the maximum is 43200s. Values above 3600s require
the `constraints/iam.allowServiceAccountCredentialLifetimeExtension`
organization policy. For the Action's `access_token` output, use
`access_token_lifetime` instead.

- `audience`: (Optional) The value for the audience (`aud`) parameter in the
generated GitHub Actions OIDC token. This value defaults to the value of
`workload_identity_provider`, which is also the default value Google Cloud
Expand Down
9 changes: 9 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@ inputs:
which to generate credentials. This is required if
"workload_identity_provider" is specified.
required: false
service_account_impersonation_lifetime:
description: |-
Desired lifetime duration of service account access tokens requested using
the generated WIF credentials file, in seconds. Specify the number of
seconds with a trailing "s" (e.g. 900s), between 600s and 43200s. This
only applies when "workload_identity_provider" and "service_account" are
specified and "create_credentials_file" is true. This does not affect the
"access_token" output.
required: false
audience:
description: |-
The value for the audience (aud) parameter in GitHub's generated OIDC
Expand Down
2 changes: 1 addition & 1 deletion dist/main/index.js

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions src/client/workload_identity_federation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ export interface WorkloadIdentityFederationClientParameters extends ClientParame
readonly workloadIdentityProviderName: string;
readonly audience?: string;
readonly serviceAccount?: string;
readonly serviceAccountImpersonationLifetime?: number;
}

/**
Expand All @@ -121,6 +122,7 @@ export class WorkloadIdentityFederationClient extends Client implements AuthClie
readonly #githubOIDCTokenAudience: string;
readonly #workloadIdentityProviderName: string;
readonly #serviceAccount?: string;
readonly #serviceAccountImpersonationLifetime?: number;
readonly #audience: string;

#cachedToken?: string;
Expand All @@ -135,6 +137,7 @@ export class WorkloadIdentityFederationClient extends Client implements AuthClie
this.#githubOIDCTokenAudience = opts.githubOIDCTokenAudience;
this.#workloadIdentityProviderName = opts.workloadIdentityProviderName;
this.#serviceAccount = opts.serviceAccount;
this.#serviceAccountImpersonationLifetime = opts.serviceAccountImpersonationLifetime;

const iamHost = new URL(this._endpoints.iam).host;
this.#audience = `//${iamHost}/${this.#workloadIdentityProviderName}`;
Expand Down Expand Up @@ -301,6 +304,14 @@ export class WorkloadIdentityFederationClient extends Client implements AuthClie
const impersonationURL = `${this._endpoints.iamcredentials}/projects/-/serviceAccounts/${this.#serviceAccount}:generateAccessToken`;
logger.debug(`Enabling service account impersonation via ${impersonationURL}`);
data.service_account_impersonation_url = impersonationURL;
if (
this.#serviceAccountImpersonationLifetime &&
this.#serviceAccountImpersonationLifetime > 0
) {
data.service_account_impersonation = {
token_lifetime_seconds: this.#serviceAccountImpersonationLifetime,
};
}
}

logger.debug(`Creating credentials`, {
Expand Down
14 changes: 14 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ export async function run(logger: Logger) {
const delegates = parseMultilineCSV(getInput(`delegates`));
const universe = getInput(`universe`);
const requestReason = getInput(`request_reason`);
const serviceAccountImpersonationLifetimeInput = getInput(
`service_account_impersonation_lifetime`,
);

// Ensure exactly one of workload_identity_provider and credentials_json was
// provided.
Expand All @@ -97,6 +100,16 @@ export async function run(logger: Logger) {
);
}

let serviceAccountImpersonationLifetime: number | undefined;
if (
workloadIdentityProvider &&
serviceAccount &&
createCredentialsFile &&
serviceAccountImpersonationLifetimeInput
) {
serviceAccountImpersonationLifetime = parseDuration(serviceAccountImpersonationLifetimeInput);
}

// Instantiate the correct client based on the provided input parameters.
let client: AuthClient;
if (workloadIdentityProvider) {
Expand Down Expand Up @@ -128,6 +141,7 @@ export async function run(logger: Logger) {
githubOIDCTokenAudience: oidcTokenAudience,
workloadIdentityProviderName: workloadIdentityProvider,
serviceAccount: serviceAccount,
serviceAccountImpersonationLifetime: serviceAccountImpersonationLifetime,
});
} else {
logger.debug(`Using credentials JSON`);
Expand Down
42 changes: 42 additions & 0 deletions tests/client/workload_identity_client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,4 +306,46 @@ test('#createCredentialsFile', { concurrency: true }, async (suite) => {

assert.deepStrictEqual(got, exp);
});

await suite.test('writes the file with impersonation lifetime', async () => {
const outputFile = pathjoin(tmpdir(), randomFilename());
const client = new WorkloadIdentityFederationClient({
logger: new NullLogger(),
universe: 'googleapis.com',

githubOIDCToken: 'my-token',
githubOIDCTokenRequestURL: 'https://example.com/',
githubOIDCTokenRequestToken: 'token',
githubOIDCTokenAudience: 'my-aud',
workloadIdentityProviderName: 'my-provider',
serviceAccount: 'my-service@my-project.iam.gserviceaccount.com',
serviceAccountImpersonationLifetime: 900,
});

const exp = {
audience: '//iam.googleapis.com/my-provider',
credential_source: {
format: {
subject_token_field_name: 'value',
type: 'json',
},
headers: {
Authorization: 'Bearer token',
},
url: 'https://example.com/?audience=my-aud',
},
service_account_impersonation: { token_lifetime_seconds: 900 },
service_account_impersonation_url:
'https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/my-service@my-project.iam.gserviceaccount.com:generateAccessToken',
subject_token_type: 'urn:ietf:params:oauth:token-type:jwt',
token_url: 'https://sts.googleapis.com/v1/token',
type: 'external_account',
};

const pth = await client.createCredentialsFile(outputFile);
const data = readFileSync(pth);
const got = JSON.parse(data.toString('utf8'));

assert.deepStrictEqual(got, exp);
});
});
Loading