diff --git a/.release-please-manifest.json b/.release-please-manifest.json index d50077a..5adccf1 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,5 +1,5 @@ { ".release-please-manifest.json": "4.0.2", "package.json": "6.0.0", - ".": "6.2.0" + ".": "6.1.2" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a7c538..c3b6647 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,8 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. -## [6.2.0](https://github.com/aws-actions/configure-aws-credentials/compare/v6.1.3...v6.2.0) (2026-06-01) +## [6.1.2](https://github.com/aws-actions/configure-aws-credentials/compare/v6.1.1...v6.1.2) (2026-05-26) + ### Features @@ -13,22 +14,11 @@ All notable changes to this project will be documented in this file. See [standa * expose run id in STS client user-agent ([#1774](https://github.com/aws-actions/configure-aws-credentials/issues/1774)) ([29d1be3](https://github.com/aws-actions/configure-aws-credentials/commit/29d1be30273e7ef371d59fccf6ec54572c64ec89)) * support custom STS endpoints ([#1762](https://github.com/aws-actions/configure-aws-credentials/issues/1762)) ([8d52d05](https://github.com/aws-actions/configure-aws-credentials/commit/8d52d05d7a4521fa52b39de50cb6114b12e5c332)) -### Bug Fixes - -* skip credential check on output-env-credentials: false ([#1778](https://github.com/aws-actions/configure-aws-credentials/issues/1778)) ([58e7c47](https://github.com/aws-actions/configure-aws-credentials/commit/58e7c47adf77846879008deadfeeef8a6969fe6c)) -* assumeRole failing from session tag size too large ([#1808](https://github.com/aws-actions/configure-aws-credentials/issues/1808)) ([d6f5dc3](https://github.com/aws-actions/configure-aws-credentials/commit/d6f5dc331b44474b19a52caaf85fa4d637b13c8e)) - -## [6.1.3](https://github.com/aws-actions/configure-aws-credentials/compare/v6.1.2...v6.1.3) (2026-05-28) - -### Bug Fixes - -* fix: allow kubelet token symlink in [#1805](https://github.com/aws-actions/configure-aws-credentials/issues/1805) - -## [6.1.2](https://github.com/aws-actions/configure-aws-credentials/compare/v6.1.1...v6.1.2) (2026-05-26) ### Bug Fixes * additional filesystem checks ([#1799](https://github.com/aws-actions/configure-aws-credentials/issues/1799)) ([c39f282](https://github.com/aws-actions/configure-aws-credentials/commit/c39f282697aca8a78c522ecf1f7da9899a31432c)) +* skip credential check on output-env-credentials: false ([#1778](https://github.com/aws-actions/configure-aws-credentials/issues/1778)) ([58e7c47](https://github.com/aws-actions/configure-aws-credentials/commit/58e7c47adf77846879008deadfeeef8a6969fe6c)) ## [6.1.1](https://github.com/aws-actions/configure-aws-credentials/compare/v6.1.0...v6.1.1) (2026-05-05) diff --git a/README.md b/README.md index 09f4b69..df38357 100644 --- a/README.md +++ b/README.md @@ -168,7 +168,6 @@ detail. | role-session-name | Defaults to "GitHubActions", but may be changed if required. | No | | role-skip-session-tagging | Skips session tagging if set. | No | | transitive-tag-keys | Define a list of transitive tag keys to pass when assuming a role. | No | -| custom-tags | Additional tags to apply to the assumed role session. Must be a JSON object provided as a string. Custom tags are not usable with OIDC or web identity token authentication. | No | | inline-session-policy | You may further restrict the assumed role policy by defining an inline policy here. | No | | managed-session-policies | You may further restrict the assumed role policy by specifying a managed policy here. | No | | output-credentials | When set, outputs fetched credentials as action step output. (Outputs aws-access-key-id, aws-secret-access-key, aws-session-token, aws-account-id, authenticated-arn, and aws-expiration). Defaults to false. | No | @@ -181,8 +180,6 @@ detail. | allowed-account-ids | A comma-delimited list of expected AWS account IDs. The action will fail if we receive credentials for the wrong account. | No | | force-skip-oidc | When set, the action will skip using GitHub OIDC provider even if the id-token permission is set. | No | | action-timeout-s | Global timeout for the action in seconds. If set to a value greater than 0, the action will fail if it takes longer than this time to complete. | No | -| no-proxy | Hosts to skip for the proxy configuration. | No | -| sts-endpoint | Custom STS endpoint URL. Use this to point to an STS-compatible API (e.g. MinIO, LocalStack) instead of the default AWS STS endpoint for the region. | No | @@ -353,7 +350,8 @@ documentation for `GITHUB_` environment variable definitions][gh-env-vars]) [gh-env-vars]: https://docs.github.com/en/actions/reference/workflows-and-actions/variables#default-environment-variables -**Default tags** are always emitted when session tags are used. +**Protected tags** are always emitted when session tags are used, and cannot be +overridden via `custom-tags`: | Key | Value | | ---------- | ----------------- | @@ -365,24 +363,21 @@ documentation for `GITHUB_` environment variable definitions][gh-env-vars]) | Commit | GITHUB_SHA | | Branch | GITHUB_REF | -**Droppable tags** are automatically added to the set of default session tags. -If the session tags exceed the [packed size limit][packed-size-limit], these -tags will be dropped, and the AssumeRole call will be retried. If it still -fails, the action will error out. (It is difficult to predict the packed size -before making the call, as session tags and session policies are compressed into -a binary format as part of the call.) +**Overrideable tags** are automatically added to the set of default session tags +but may be overridden via `custom-tags`. AWS has a maximum limit of 50 session +tags; tags from this list are dropped in reverse priority order if your +`custom-tags` set plus the protected set exceeds this limit. -[packed-size-limit]: - https://docs.aws.amazon.com/IAM/latest/UserGuide/id_session-tags.html#id_session-tags_know - -| Key | Value | -| --------------- | ----------------------- | -| EventName | GITHUB_EVENT_NAME | -| BaseRef | GITHUB_BASE_REF | -| HeadRef | GITHUB_HEAD_REF | -| RunId | GITHUB_RUN_ID | -| Job | GITHUB_JOB | -| TriggeringActor | GITHUB_TRIGGERING_ACTOR | +| Key | Value | Priority | +| --------------- | ----------------------- | -------- | +| EventName | GITHUB_EVENT_NAME | 1 | +| BaseRef | GITHUB_BASE_REF | 2 | +| HeadRef | GITHUB_HEAD_REF | 3 | +| RefName | GITHUB_REF_NAME | 4 | +| RunId | GITHUB_RUN_ID | 5 | +| RefType | GITHUB_REF_TYPE | 6 | +| Job | GITHUB_JOB | 7 | +| TriggeringActor | GITHUB_TRIGGERING_ACTOR | 8 | Tags whose source environment variable is unset are omitted (e.g., `BaseRef` and `HeadRef` are only set on `pull_request` events). @@ -390,21 +385,21 @@ Tags whose source environment variable is unset are omitted (e.g., `BaseRef` and _Note: all tag values must conform to [the tag requirements][sts-tag-requirements]. Values longer than 256 characters will be truncated, and characters outside the -allowed set will be replaced with an underscore (`_`)._ +allowed set will be replaced with an underscore (`_`).\_ [sts-tag-requirements]: https://docs.aws.amazon.com/STS/latest/APIReference/API_Tag.html -The action will use session tagging by default unless you are using OIDC or a -Web Identify Token File. +The action will use session tagging by default unless you are using OIDC. To [forward session tags to subsequent sessions in a role -chain][session-tag-chaining], you can use the `transitive-tag-keys` input to -specify the keys of the tags to be passed. +chain][session-tag-chaining], you can use [session-tag-chaining]: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_session-tags.html#id_session-tags_role-chaining +the `transitive-tag-keys` input to specify the keys of the tags to be passed. + _Note that all subsequent roles in the chain must have `role-skip-session-tagging` set to `true`_ @@ -421,10 +416,9 @@ with: ### Custom session tags You can add custom session tags using the `custom-tags` input, which accepts a -JSON object. Custom tags cannot override existing tags. Note that AWS allows a -maximum of 50 tags (so you can supply a maximum of 43 custom tags), although it -is likely that you will exceed the [packed size limit][packed-size-limit] -before you exceed the maximum number of tags. +JSON object. Custom tags cannot override protected tags, but they can override +overrideable tags (in which case the overrideable tag's slot is freed for the +next overrideable tag in the priority list, if any). ```yaml uses: aws-actions/configure-aws-credentials@v6 @@ -590,7 +584,7 @@ claims ([1][gh-blog-oidc], [2][sub-claim-custom]). > **Warning:** Avoid `ForAllValues:` in `Allow` statements. These operators > return true when the claim is absent or misspelled, which can lead to -> unintended access. Instead, use `StringEquals` or `StringLike` operators to +> uninended access. Instead, use `StringEquals` or `StringLike` operators to > check for specific claim values. [least-privilege]: @@ -623,35 +617,6 @@ For further information on OIDC and GitHub Actions, please see: - [GitHub docs: Configuring OpenID Connect in Amazon Web Services](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services) - [GitHub changelog: GitHub Actions: Secure cloud deployments with OpenID Connect](https://github.blog/changelog/2021-10-27-github-actions-secure-cloud-deployments-with-openid-connect/) -## Getting Credentials in AWS Self-Hosted Runners - -If you are running GitHub Actions in a self-hosted runner using an AWS Service -(such as Codebuild or EKS) and you have properly configured the service, -credentials should be available by default; the AWS CLI will fetch credentials -using the AWS_CONTAINER_CREDENTIALS_FULL_URI or -AWS_CONTAINER_CREDENTIALS_RELATIVE_URI environment variables. However, you may -still want to use this action if you need to export those credentials for use -with other tools in your workflow. You may also want to use this action in -scenarios where you need to use that 'default' role to assume another role. - -To export credentials, simply run the action with `role-to-assume` set to the -default role of the container. - -To assume another role from the container's default role, use the -`role-chaining: true` flag, so that the action fetches the default credentials -from the environment before assuming the other role. - -If you are using EKS Pod Identities and encountering an error related to the -packed size of session tags, you must either run the action with -`role-skip-session-tagging: true` to disable the tags set by the action, or -[disable EKS session tagging][eks-disable-session-tagging] in the EKS settings -to disable the tags that are automatically set by the EKS Pod Identity Service. -Check the values of the action's session tags and the session tags that are -added by EKS so you can keep the set of tags which is more useful to you. - -[eks-disable-session-tagging]: - https://docs.aws.amazon.com/eks/latest/userguide/pod-id-abac.html#pod-id-abac-tags - ## Compatibility with non-GitHub Actions environments This action has been sucessfully tested with diff --git a/dist/index.js b/dist/index.js index 051134f..174f632 100644 --- a/dist/index.js +++ b/dist/index.js @@ -44986,7 +44986,7 @@ var require_errors2 = __commonJS({ } }; exports2.MalformedPolicyDocumentException = MalformedPolicyDocumentException2; - var PackedPolicyTooLargeException3 = class _PackedPolicyTooLargeException extends STSServiceException_1.STSServiceException { + var PackedPolicyTooLargeException2 = class _PackedPolicyTooLargeException extends STSServiceException_1.STSServiceException { name = "PackedPolicyTooLargeException"; $fault = "client"; constructor(opts) { @@ -44998,7 +44998,7 @@ var require_errors2 = __commonJS({ Object.setPrototypeOf(this, _PackedPolicyTooLargeException.prototype); } }; - exports2.PackedPolicyTooLargeException = PackedPolicyTooLargeException3; + exports2.PackedPolicyTooLargeException = PackedPolicyTooLargeException2; var RegionDisabledException2 = class _RegionDisabledException extends STSServiceException_1.STSServiceException { name = "RegionDisabledException"; $fault = "client"; @@ -74060,7 +74060,6 @@ async function assumeRoleWithWebIdentityTokenFile(params, client, webIdentityTok info("Assuming role with web identity token file"); try { delete params.Tags; - delete params.TransitiveTagKeys; const creds = await client.send( new import_client_sts2.AssumeRoleWithWebIdentityCommand({ ...params, @@ -74078,13 +74077,6 @@ async function assumeRoleWithCredentials(params, client) { const creds = await client.send(new import_client_sts2.AssumeRoleCommand({ ...params })); return creds; } catch (error3) { - if (error3 instanceof import_client_sts2.PackedPolicyTooLargeException) { - info("Session tag size is too large; dropping droppable tags and retrying."); - const droppableKeys = new Set(DROPPABLE_TAG_SOURCES.map((s) => s.key)); - params.Tags = params.Tags?.filter((tag2) => !droppableKeys.has(tag2.Key ?? "")); - const creds = await client.send(new import_client_sts2.AssumeRoleCommand({ ...params })); - return creds; - } throw new Error(`Could not assume role with user credentials: ${errorMessage(error3)}`); } } @@ -74093,7 +74085,7 @@ var TAG_VALUE_REGEX = /^[\p{L}\p{Z}\p{N}_.:/=+\-@]*$/u; var MAX_TAG_KEY_LENGTH = 128; var MAX_TAG_VALUE_LENGTH2 = 256; var MAX_SESSION_TAGS = 50; -var NON_DROPPABLE_TAG_SOURCES = [ +var PROTECTED_TAG_SOURCES = [ { key: "Repository", envVar: "GITHUB_REPOSITORY" }, { key: "Workflow", envVar: "GITHUB_WORKFLOW" }, { key: "Action", envVar: "GITHUB_ACTION" }, @@ -74101,19 +74093,17 @@ var NON_DROPPABLE_TAG_SOURCES = [ { key: "Commit", envVar: "GITHUB_SHA" }, { key: "Branch", envVar: "GITHUB_REF" } ]; -var DROPPABLE_TAG_SOURCES = [ +var OVERRIDEABLE_TAG_SOURCES_BY_PRIORITY = [ { key: "EventName", envVar: "GITHUB_EVENT_NAME" }, { key: "BaseRef", envVar: "GITHUB_BASE_REF" }, { key: "HeadRef", envVar: "GITHUB_HEAD_REF" }, + { key: "RefName", envVar: "GITHUB_REF_NAME" }, { key: "RunId", envVar: "GITHUB_RUN_ID" }, + { key: "RefType", envVar: "GITHUB_REF_TYPE" }, { key: "Job", envVar: "GITHUB_JOB" }, { key: "TriggeringActor", envVar: "GITHUB_TRIGGERING_ACTOR" } ]; -var PROTECTED_TAG_KEYS = /* @__PURE__ */ new Set([ - "GitHub", - ...NON_DROPPABLE_TAG_SOURCES.map((s) => s.key), - ...DROPPABLE_TAG_SOURCES.map((s) => s.key) -]); +var PROTECTED_TAG_KEYS = /* @__PURE__ */ new Set(["GitHub", ...PROTECTED_TAG_SOURCES.map((s) => s.key)]); function parseAndValidateCustomTags(customTags, existingTags) { let parsed; try { @@ -74185,26 +74175,30 @@ async function assumeRole(params) { throw new Error("Missing required environment variables. Are you running in GitHub Actions?"); } const protectedTags = [{ Key: "GitHub", Value: "Actions" }]; - for (const { key, envVar } of NON_DROPPABLE_TAG_SOURCES) { - const value = process.env[envVar]; - if (value) { - protectedTags.push({ Key: key, Value: sanitizeGitHubVariables(value) }); - } - } - for (const { key, envVar } of DROPPABLE_TAG_SOURCES) { + for (const { key, envVar } of PROTECTED_TAG_SOURCES) { const value = process.env[envVar]; if (value) { protectedTags.push({ Key: key, Value: sanitizeGitHubVariables(value) }); } } const parsedCustomTags = customTags ? parseAndValidateCustomTags(customTags, protectedTags) : []; - const tagArray = [...protectedTags, ...parsedCustomTags]; + const customTagKeys = new Set(parsedCustomTags.map((t) => t.Key)); + const availableOverrideableSlots = MAX_SESSION_TAGS - protectedTags.length - parsedCustomTags.length; + const overrideableTags = []; + for (const { key, envVar } of OVERRIDEABLE_TAG_SOURCES_BY_PRIORITY) { + if (overrideableTags.length >= availableOverrideableSlots) break; + if (customTagKeys.has(key)) continue; + const value = process.env[envVar]; + if (value) { + overrideableTags.push({ Key: key, Value: sanitizeGitHubVariables(value) }); + } + } + const tagArray = [...protectedTags, ...overrideableTags, ...parsedCustomTags]; const tags = roleSkipSessionTagging ? void 0 : tagArray; if (!tags) { debug("Role session tagging has been skipped."); } else { debug(`${tags.length} role session tags are being used:`); - debug(JSON.stringify(tagArray)); } const transitiveTagKeysArray = roleSkipSessionTagging ? void 0 : transitiveTagKeys?.filter((key) => tags?.some((tag2) => tag2.Key === key)); let roleArn = roleToAssume; diff --git a/package-lock.json b/package-lock.json index 521b8a1..82069bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "configure-aws-credentials", - "version": "6.2.0", + "version": "6.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "configure-aws-credentials", - "version": "6.2.0", + "version": "6.1.1", "license": "MIT", "dependencies": { "@actions/core": "^3.0.1", diff --git a/package.json b/package.json index 06eb6c6..78bc9ac 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "configure-aws-credentials", "description": "A GitHub Action to configure AWS credentials", - "version": "6.2.0", + "version": "6.1.1", "scripts": { "build": "tsc", "lint": "biome check --error-on-warnings ./src ./test && markdownlint -i node_modules -i CHANGELOG.md '**/*.md'", diff --git a/src/assumeRole.ts b/src/assumeRole.ts index 95f6bdc..ebf27a6 100644 --- a/src/assumeRole.ts +++ b/src/assumeRole.ts @@ -2,11 +2,7 @@ import assert from 'node:assert'; import path from 'node:path'; import * as core from '@actions/core'; import type { AssumeRoleCommandInput, STSClient, Tag } from '@aws-sdk/client-sts'; -import { - AssumeRoleCommand, - AssumeRoleWithWebIdentityCommand, - PackedPolicyTooLargeException, -} from '@aws-sdk/client-sts'; +import { AssumeRoleCommand, AssumeRoleWithWebIdentityCommand } from '@aws-sdk/client-sts'; import type { CredentialsClient } from './CredentialsClient'; import { errorMessage, isDefined, readFileUtf8, sanitizeGitHubVariables } from './helpers'; @@ -46,7 +42,6 @@ async function assumeRoleWithWebIdentityTokenFile( core.info('Assuming role with web identity token file'); try { delete params.Tags; - delete params.TransitiveTagKeys; const creds = await client.send( new AssumeRoleWithWebIdentityCommand({ ...params, @@ -65,13 +60,6 @@ async function assumeRoleWithCredentials(params: AssumeRoleCommandInput, client: const creds = await client.send(new AssumeRoleCommand({ ...params })); return creds; } catch (error) { - if (error instanceof PackedPolicyTooLargeException) { - core.info('Session tag size is too large; dropping droppable tags and retrying.'); - const droppableKeys = new Set(DROPPABLE_TAG_SOURCES.map((s) => s.key)); - params.Tags = params.Tags?.filter((tag) => !droppableKeys.has(tag.Key ?? '')); - const creds = await client.send(new AssumeRoleCommand({ ...params })); - return creds; - } throw new Error(`Could not assume role with user credentials: ${errorMessage(error)}`); } } @@ -98,8 +86,8 @@ const MAX_TAG_KEY_LENGTH = 128; const MAX_TAG_VALUE_LENGTH = 256; const MAX_SESSION_TAGS = 50; -// Identity/audit primitives. Always emitted and cannot be dropped. -const NON_DROPPABLE_TAG_SOURCES: ReadonlyArray<{ key: string; envVar: string }> = [ +// Identity/audit primitives. Always emitted and cannot be overridden by custom-tags. +const PROTECTED_TAG_SOURCES: ReadonlyArray<{ key: string; envVar: string }> = [ { key: 'Repository', envVar: 'GITHUB_REPOSITORY' }, { key: 'Workflow', envVar: 'GITHUB_WORKFLOW' }, { key: 'Action', envVar: 'GITHUB_ACTION' }, @@ -108,22 +96,21 @@ const NON_DROPPABLE_TAG_SOURCES: ReadonlyArray<{ key: string; envVar: string }> { key: 'Branch', envVar: 'GITHUB_REF' }, ]; -// Convenience metadata. If the AssumeRole call fails due to compressed size of -// session tags being too large, we will drop these tags and retry once. -const DROPPABLE_TAG_SOURCES: ReadonlyArray<{ key: string; envVar: string }> = [ +// Convenience metadata. Custom-tags may override (suppresses the default for that key). +// Listed in priority order; lower-priority entries are dropped first if the user's custom-tags +// would push the total above MAX_SESSION_TAGS. +const OVERRIDEABLE_TAG_SOURCES_BY_PRIORITY: ReadonlyArray<{ key: string; envVar: string }> = [ { key: 'EventName', envVar: 'GITHUB_EVENT_NAME' }, { key: 'BaseRef', envVar: 'GITHUB_BASE_REF' }, { key: 'HeadRef', envVar: 'GITHUB_HEAD_REF' }, + { key: 'RefName', envVar: 'GITHUB_REF_NAME' }, { key: 'RunId', envVar: 'GITHUB_RUN_ID' }, + { key: 'RefType', envVar: 'GITHUB_REF_TYPE' }, { key: 'Job', envVar: 'GITHUB_JOB' }, { key: 'TriggeringActor', envVar: 'GITHUB_TRIGGERING_ACTOR' }, ]; -const PROTECTED_TAG_KEYS = new Set([ - 'GitHub', - ...NON_DROPPABLE_TAG_SOURCES.map((s) => s.key), - ...DROPPABLE_TAG_SOURCES.map((s) => s.key), -]); +const PROTECTED_TAG_KEYS = new Set(['GitHub', ...PROTECTED_TAG_SOURCES.map((s) => s.key)]); export function parseAndValidateCustomTags(customTags: string, existingTags: Tag[]): Tag[] { let parsed: unknown; @@ -210,13 +197,7 @@ export async function assumeRole(params: assumeRoleParams) { // Build session tags. Values are sanitized because the AWS tag value spec is more // restrictive than permissible characters in environment variables. const protectedTags: Tag[] = [{ Key: 'GitHub', Value: 'Actions' }]; - for (const { key, envVar } of NON_DROPPABLE_TAG_SOURCES) { - const value = process.env[envVar]; - if (value) { - protectedTags.push({ Key: key, Value: sanitizeGitHubVariables(value) }); - } - } - for (const { key, envVar } of DROPPABLE_TAG_SOURCES) { + for (const { key, envVar } of PROTECTED_TAG_SOURCES) { const value = process.env[envVar]; if (value) { protectedTags.push({ Key: key, Value: sanitizeGitHubVariables(value) }); @@ -224,15 +205,26 @@ export async function assumeRole(params: assumeRoleParams) { } const parsedCustomTags: Tag[] = customTags ? parseAndValidateCustomTags(customTags, protectedTags) : []; + const customTagKeys = new Set(parsedCustomTags.map((t) => t.Key)); - const tagArray: Tag[] = [...protectedTags, ...parsedCustomTags]; + const availableOverrideableSlots = MAX_SESSION_TAGS - protectedTags.length - parsedCustomTags.length; + const overrideableTags: Tag[] = []; + for (const { key, envVar } of OVERRIDEABLE_TAG_SOURCES_BY_PRIORITY) { + if (overrideableTags.length >= availableOverrideableSlots) break; + if (customTagKeys.has(key)) continue; + const value = process.env[envVar]; + if (value) { + overrideableTags.push({ Key: key, Value: sanitizeGitHubVariables(value) }); + } + } + + const tagArray: Tag[] = [...protectedTags, ...overrideableTags, ...parsedCustomTags]; const tags = roleSkipSessionTagging ? undefined : tagArray; if (!tags) { core.debug('Role session tagging has been skipped.'); } else { core.debug(`${tags.length} role session tags are being used:`); - core.debug(JSON.stringify(tagArray)); } //only populate transitiveTagKeys array if user is actually using session tagging diff --git a/test/index.test.ts b/test/index.test.ts index d03181a..3970951 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -3,7 +3,6 @@ import { AssumeRoleCommand, AssumeRoleWithWebIdentityCommand, GetCallerIdentityCommand, - PackedPolicyTooLargeException, STSClient, } from '@aws-sdk/client-sts'; import { mockClient } from 'aws-sdk-client-mock'; @@ -203,18 +202,6 @@ describe('Configure AWS Credentials', {}, () => { expect(core.setOutput).toHaveBeenCalledTimes(2); expect(core.setFailed).not.toHaveBeenCalled(); }); - it('does not send Tags or TransitiveTagKeys to AssumeRoleWithWebIdentity', async () => { - // AssumeRoleWithWebIdentity reads session tags from JWT claims, not the request. - // Both fields must be stripped before the STS call. - vi.mocked(core.getMultilineInput).mockImplementation((name: string) => { - if (name === 'transitive-tag-keys') return ['Repository']; - return []; - }); - await run(); - const callInput = mockedSTSClient.commandCalls(AssumeRoleWithWebIdentityCommand)[0].args[0].input; - expect(callInput.Tags).toBeUndefined(); - expect(callInput.TransitiveTagKeys).toBeUndefined(); - }); }); describe('Assume existing role', {}, () => { @@ -295,9 +282,9 @@ describe('Configure AWS Credentials', {}, () => { await run(); const tags = mockedSTSClient.commandCalls(AssumeRoleCommand)[0].args[0].input.Tags ?? []; // 7 protected (GitHub + Repository, Workflow, Action, Actor, Commit, Branch) - // + 6 droppable (EventName, BaseRef, HeadRef, RunId, Job, TriggeringActor). - // No custom-tags, all env vars set in mocks.envs → all 13 should be present, nothing else. - expect(tags).toHaveLength(13); + // + 8 overrideable (EventName, BaseRef, HeadRef, RefName, RunId, RefType, Job, TriggeringActor). + // No custom-tags, all env vars set in mocks.envs → all 15 should be present, nothing else. + expect(tags).toHaveLength(15); const tagsByKey = Object.fromEntries(tags.map((t) => [t.Key, t.Value])); expect(tagsByKey).toEqual({ GitHub: 'Actions', @@ -310,12 +297,14 @@ describe('Configure AWS Credentials', {}, () => { EventName: 'pull_request', BaseRef: 'main', HeadRef: 'feature-branch', + RefName: 'feature-branch', RunId: '16412345678', + RefType: 'branch', Job: 'build', TriggeringActor: 'MY-USERNAME_bot_', }); }); - it('omits droppable tags whose env vars are unset', {}, async () => { + it('omits overrideable tags whose env vars are unset', {}, async () => { vi.mocked(core.getInput).mockImplementation(mocks.getInput(mocks.IAM_ASSUMEROLE_INPUTS)); delete process.env.GITHUB_BASE_REF; delete process.env.GITHUB_HEAD_REF; @@ -329,27 +318,6 @@ describe('Configure AWS Credentials', {}, () => { expect(tagKeys).toContain('EventName'); expect(tagKeys).toContain('RunId'); }); - it('drops droppable tags and retries on PackedPolicyTooLargeException', {}, async () => { - vi.mocked(core.getInput).mockImplementation(mocks.getInput(mocks.IAM_ASSUMEROLE_INPUTS)); - mockedSTSClient - .on(AssumeRoleCommand) - .rejectsOnce(new PackedPolicyTooLargeException({ message: 'too large', $metadata: {} })) - .resolvesOnce(mocks.outputs.STS_CREDENTIALS); - await run(); - expect(core.info).toHaveBeenCalledWith('Session tag size is too large; dropping droppable tags and retrying.'); - const retryInput = mockedSTSClient.commandCalls(AssumeRoleCommand)[1].args[0].input; - const retryTagKeys = (retryInput.Tags ?? []).map((t) => t.Key); - expect(retryTagKeys).not.toContain('EventName'); - expect(retryTagKeys).not.toContain('BaseRef'); - expect(retryTagKeys).not.toContain('HeadRef'); - expect(retryTagKeys).not.toContain('RunId'); - expect(retryTagKeys).not.toContain('Job'); - expect(retryTagKeys).not.toContain('TriggeringActor'); - // Protected tags remain - expect(retryTagKeys).toContain('GitHub'); - expect(retryTagKeys).toContain('Repository'); - expect(core.setFailed).not.toHaveBeenCalled(); - }); it('sanitizes invalid characters in env-derived tag values', {}, async () => { vi.mocked(core.getInput).mockImplementation(mocks.getInput(mocks.IAM_ASSUMEROLE_INPUTS)); process.env.GITHUB_HEAD_REF = 'feature/has spaces&bad?chars'; @@ -402,6 +370,8 @@ describe('Configure AWS Credentials', {}, () => { { Key: 'EventName', Value: 'pull_request' }, { Key: 'RunId', Value: '16412345678' }, { Key: 'Job', Value: 'build' }, + { Key: 'RefName', Value: 'feature-branch' }, + { Key: 'RefType', Value: 'branch' }, { Key: 'TriggeringActor', Value: 'MY-USERNAME_bot_' }, { Key: 'Environment', Value: 'Production' }, { Key: 'Team', Value: 'DevOps' }, @@ -450,7 +420,7 @@ describe('Configure AWS Credentials', {}, () => { await run(); expect(core.warning).toHaveBeenCalledWith(expect.stringContaining("'custom-tags' is set but will be ignored")); }); - it('rejects custom tags that conflict with droppable tag keys', {}, async () => { + it('lets custom tags override overrideable default tag keys', {}, async () => { vi.mocked(core.getInput).mockImplementation( mocks.getInput({ ...mocks.IAM_ASSUMEROLE_INPUTS, @@ -458,10 +428,13 @@ describe('Configure AWS Credentials', {}, () => { }), ); await run(); - expect(core.setFailed).toHaveBeenCalledWith( - "custom-tags: key 'EventName' conflicts with a protected session tag set by this action and cannot be overridden", - ); - expect(mockedSTSClient.commandCalls(AssumeRoleCommand)).toHaveLength(0); + const tags = mockedSTSClient.commandCalls(AssumeRoleCommand)[0].args[0].input.Tags ?? []; + const eventNameTags = tags.filter((t) => t.Key === 'EventName'); + const baseRefTags = tags.filter((t) => t.Key === 'BaseRef'); + expect(eventNameTags).toHaveLength(1); + expect(eventNameTags[0]?.Value).toBe('workflow_dispatch'); + expect(baseRefTags).toHaveLength(1); + expect(baseRefTags[0]?.Value).toBe('release/2026'); }); it('rejects custom tags that conflict with the protected Branch tag', {}, async () => { // Regression guard: Branch was a default before v6.2 and must remain unoverridable. @@ -477,10 +450,62 @@ describe('Configure AWS Credentials', {}, () => { ); expect(mockedSTSClient.commandCalls(AssumeRoleCommand)).toHaveLength(0); }); - it('rejects custom-tags that would exceed the session-tag limit', {}, async () => { - // 13 existing tags (7 non-droppable + 6 droppable) + 38 custom = 51 > 50. + it('drops lower-priority overrideable tags when custom-tags would exceed the session-tag limit', {}, async () => { + // 7 protected (GitHub + 6 from PROTECTED_TAG_SOURCES) + 40 custom = 47 used → 3 overrideable slots. + // The first 3 overrideable tags by priority are EventName, BaseRef, HeadRef (RefName, RunId, RefType, + // Job, TriggeringActor must be dropped). const customTagsObj: Record = {}; - for (let i = 0; i < 38; i++) { + for (let i = 0; i < 40; i++) { + customTagsObj[`Custom${i}`] = `value${i}`; + } + vi.mocked(core.getInput).mockImplementation( + mocks.getInput({ + ...mocks.IAM_ASSUMEROLE_INPUTS, + 'custom-tags': JSON.stringify(customTagsObj), + }), + ); + await run(); + const tags = mockedSTSClient.commandCalls(AssumeRoleCommand)[0].args[0].input.Tags ?? []; + const tagKeys = tags.map((t) => t.Key); + expect(tags).toHaveLength(50); + expect(tagKeys).toContain('Branch'); + expect(tagKeys).toContain('EventName'); + expect(tagKeys).toContain('BaseRef'); + expect(tagKeys).toContain('HeadRef'); + expect(tagKeys).not.toContain('RefName'); + expect(tagKeys).not.toContain('RunId'); + expect(tagKeys).not.toContain('RefType'); + expect(tagKeys).not.toContain('Job'); + expect(tagKeys).not.toContain('TriggeringActor'); + }); + it('overridden overrideable tags free a slot for a lower-priority overrideable tag', {}, async () => { + // Same 40-custom-tag scenario as above, but one of the customs overrides BaseRef. + // BaseRef no longer competes for the overrideable budget, so the next-priority overrideable (RefName) gets in. + const customTagsObj: Record = { BaseRef: 'release/2026' }; + for (let i = 0; i < 39; i++) { + customTagsObj[`Custom${i}`] = `value${i}`; + } + vi.mocked(core.getInput).mockImplementation( + mocks.getInput({ + ...mocks.IAM_ASSUMEROLE_INPUTS, + 'custom-tags': JSON.stringify(customTagsObj), + }), + ); + await run(); + const tags = mockedSTSClient.commandCalls(AssumeRoleCommand)[0].args[0].input.Tags ?? []; + const tagKeys = tags.map((t) => t.Key); + expect(tags).toHaveLength(50); + expect(tagKeys).toContain('Branch'); + expect(tagKeys).toContain('EventName'); + expect(tagKeys).toContain('BaseRef'); + expect(tagKeys).toContain('HeadRef'); + expect(tagKeys).toContain('RefName'); + expect(tagKeys).not.toContain('RunId'); + }); + it('rejects custom-tags that would exceed the session-tag limit on their own', {}, async () => { + // 7 protected + 44 custom = 51, which is over 50 even with zero overrideable tags. + const customTagsObj: Record = {}; + for (let i = 0; i < 44; i++) { customTagsObj[`Custom${i}`] = `value${i}`; } vi.mocked(core.getInput).mockImplementation( @@ -493,10 +518,12 @@ describe('Configure AWS Credentials', {}, () => { expect(core.setFailed).toHaveBeenCalledWith(expect.stringContaining('would exceed the AWS limit of 50')); expect(mockedSTSClient.commandCalls(AssumeRoleCommand)).toHaveLength(0); }); - it('allows custom-tags up to the session-tag limit', {}, async () => { - // 13 existing tags + 37 custom = 50, exactly at the limit. + it('drops transitive-tag-keys entries that refer to evicted overrideable tags', {}, async () => { + // Force eviction of all overrideable tags below EventName/BaseRef/HeadRef. The user transitive-tags + // RunId (which gets evicted) and Repository (which is protected and stays). The TransitiveTagKeys + // payload must include only the keys that actually appear in Tags. const customTagsObj: Record = {}; - for (let i = 0; i < 37; i++) { + for (let i = 0; i < 40; i++) { customTagsObj[`Custom${i}`] = `value${i}`; } vi.mocked(core.getInput).mockImplementation( @@ -505,10 +532,15 @@ describe('Configure AWS Credentials', {}, () => { 'custom-tags': JSON.stringify(customTagsObj), }), ); + vi.mocked(core.getMultilineInput).mockImplementation((name: string) => { + if (name === 'transitive-tag-keys') return ['Repository', 'RunId']; + return []; + }); await run(); - expect(core.setFailed).not.toHaveBeenCalled(); - const tags = mockedSTSClient.commandCalls(AssumeRoleCommand)[0].args[0].input.Tags ?? []; - expect(tags).toHaveLength(50); + const callInput = mockedSTSClient.commandCalls(AssumeRoleCommand)[0].args[0].input; + const tagKeys = (callInput.Tags ?? []).map((t) => t.Key); + expect(tagKeys).not.toContain('RunId'); + expect(callInput.TransitiveTagKeys).toEqual(['Repository']); }); }); diff --git a/test/mockinputs.test.ts b/test/mockinputs.test.ts index 68ee8a0..cfc4125 100644 --- a/test/mockinputs.test.ts +++ b/test/mockinputs.test.ts @@ -105,6 +105,8 @@ const envs = { GITHUB_EVENT_NAME: 'pull_request', GITHUB_RUN_ID: '16412345678', GITHUB_JOB: 'build', + GITHUB_REF_NAME: 'feature-branch', + GITHUB_REF_TYPE: 'branch', GITHUB_BASE_REF: 'main', GITHUB_HEAD_REF: 'feature-branch', GITHUB_TRIGGERING_ACTOR: 'MY-USERNAME[bot]',