* chore(deps-dev): bump vitest from 3.2.4 to 4.1.5 (#1748) * chore(deps-dev): bump vitest from 3.2.4 to 4.1.5 Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 3.2.4 to 4.1.5. - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.5/packages/vitest) --- updated-dependencies: - dependency-name: vitest dependency-version: 4.1.5 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> * chore(deps-dev): update @vitest/coverage-v8 Bump @vitest/coverage-v8 to ^4.1.5 to match peer dependency, and move vi.mock('node:fs') calls to the top level of test files to reflect actual hoisting semantics required by vitest 4.x. --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Tom Keller <kellertk@amazon.com> (cherry picked from commit78f374f6d1) * chore(deps): bump @actions/core from 2.0.3 to 3.0.1 (#1746) * chore(deps): bump @actions/core from 2.0.3 to 3.0.1 Bumps [@actions/core](https://github.com/actions/toolkit/tree/HEAD/packages/core) from 2.0.3 to 3.0.1. - [Changelog](https://github.com/actions/toolkit/blob/main/packages/core/RELEASES.md) - [Commits](https://github.com/actions/toolkit/commits/HEAD/packages/core) --- updated-dependencies: - dependency-name: "@actions/core" dependency-version: 3.0.1 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> * chore: update test mocks for @actions/core ESM @actions/core v3 ships as an ESM module with non-configurable exports, breaking vi.spyOn(). Switch to vi.mock('@actions/core') which intercepts at the module loader level. --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Tom Keller <kellertk@amazon.com> (cherry picked from commit64d8e82527) * chore(deps): bump @aws-sdk/client-sts from 3.1043.0 to 3.1044.0 (#1754) Bumps [@aws-sdk/client-sts](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-sts) from 3.1043.0 to 3.1044.0. - [Release notes](https://github.com/aws/aws-sdk-js-v3/releases) - [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-sts/CHANGELOG.md) - [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1044.0/clients/client-sts) --- updated-dependencies: - dependency-name: "@aws-sdk/client-sts" dependency-version: 3.1042.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> (cherry picked from commit4cfda40a13) * chore: bump unit test node version (#1758) (cherry picked from commit39d1702721) * chore: automatic major version tagging (#1565) * Update release-please.yml to auto-update version tag * chore: configure release-please auto floating tag --------- Co-authored-by: Tom Keller <kellertk@amazon.com> (cherry picked from commitc36525a567) * feat: Allow custom session tags to be passed when assuming a role (#1759) * Add possibility to input custom session tags * Use json for input to custom-tags, add documentation for custom-tags * Add more examples * Simplify example to avoid parse error * Add input validation for custom tags * Fix unit tests for custom-tags * Add debugging message * Skip failing test for now * Build package * Remove some unused validation for custom tags * feat: add validation for custom session tags Harden the custom-tags feature against misuse and misconfiguration: - Validate input is a JSON object (reject arrays, primitives, null) - Enforce STS tag constraints: key length (128), value length (256), allowed characters - Reject nested object/array values that would silently stringify to '[object Object]' - Block overriding default session tags (GitHub, Repository, Workflow, etc.) - Enforce 50-tag session limit - Warn when custom-tags used with OIDC or web identity - Fix missing await on helpers test assertion - Remove unused CUSTOM_TAGS_JSON_INPUTS fixture - Normalize test mocking to vi.mocked() pattern --------- Co-authored-by: Sylvain Verly <sylvain.verly@gmail.com> (cherry picked from commit61f50f630f) * chore: configure codeql to ignore generated code (#1760) (cherry picked from commitdc2353e57a) * feat: support custom STS endpoints (#1762) Closes #1067. This is a advanced option and is not needed for most deployments. (cherry picked from commit8d52d05d7a) * chore: automate README version bumping (#1763) Closes #1420. (cherry picked from commit07ada0fe07) * feat: add more retry logic and better logging (#1764) Wraps exportAccountId and validateCredentials calls in retryAndBackoff. Closes #1681. Adds a label parameter to retryAndBackoff for better info-level log messages. (cherry picked from commit540d0c13ae) * feat: add regex validation to role-session-name (#1765) Previously invalid role session names would get errors from the STS API instead of this action rejecting them, causing unnecessary retries. Now we check them and fail early. Closes #1656. That FR recommended that we sanitize the name before sending to STS, but instead we error to not silently change the user's selected session name (avoiding the potential security sharp edge) (cherry picked from commite35449909c) * chore: update documentation for environment workflows (#1766) Closes #1238. (cherry picked from commit3f7e1b63d7) * chore(deps): bump @aws-sdk/client-sts from 3.1044.0 to 3.1045.0 (#1767) Bumps [@aws-sdk/client-sts](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-sts) from 3.1044.0 to 3.1045.0. - [Release notes](https://github.com/aws/aws-sdk-js-v3/releases) - [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-sts/CHANGELOG.md) - [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1045.0/clients/client-sts) --- updated-dependencies: - dependency-name: "@aws-sdk/client-sts" dependency-version: 3.1045.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> (cherry picked from commita388f23f7d) * chore(deps-dev): bump @vitest/coverage-v8 from 4.1.5 to 4.1.6 (#1768) Bumps [@vitest/coverage-v8](https://github.com/vitest-dev/vitest/tree/HEAD/packages/coverage-v8) from 4.1.5 to 4.1.6. - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.6/packages/coverage-v8) --- updated-dependencies: - dependency-name: "@vitest/coverage-v8" dependency-version: 4.1.6 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> (cherry picked from commit1fb495c4b2) * chore(deps-dev): bump @smithy/property-provider from 4.2.14 to 4.3.1 (#1771) Bumps [@smithy/property-provider](https://github.com/smithy-lang/smithy-typescript/tree/HEAD/packages/property-provider) from 4.2.14 to 4.3.1. - [Release notes](https://github.com/smithy-lang/smithy-typescript/releases) - [Changelog](https://github.com/smithy-lang/smithy-typescript/blob/main/packages/property-provider/CHANGELOG.md) - [Commits](https://github.com/smithy-lang/smithy-typescript/commits/@smithy/property-provider@4.3.1/packages/property-provider) --- updated-dependencies: - dependency-name: "@smithy/property-provider" dependency-version: 4.3.1 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> (cherry picked from commit1ab31502aa) * chore(deps): bump @smithy/node-http-handler from 4.6.1 to 4.7.1 (#1770) Bumps [@smithy/node-http-handler](https://github.com/smithy-lang/smithy-typescript/tree/HEAD/packages/node-http-handler) from 4.6.1 to 4.7.1. - [Release notes](https://github.com/smithy-lang/smithy-typescript/releases) - [Changelog](https://github.com/smithy-lang/smithy-typescript/blob/main/packages/node-http-handler/CHANGELOG.md) - [Commits](https://github.com/smithy-lang/smithy-typescript/commits/@smithy/node-http-handler@4.7.1/packages/node-http-handler) --- updated-dependencies: - dependency-name: "@smithy/node-http-handler" dependency-version: 4.7.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> (cherry picked from commitdbd503f368) * chore(deps-dev): bump @biomejs/biome from 2.4.14 to 2.4.15 (#1772) Bumps [@biomejs/biome](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) from 2.4.14 to 2.4.15. - [Release notes](https://github.com/biomejs/biome/releases) - [Changelog](https://github.com/biomejs/biome/blob/main/packages/@biomejs/biome/CHANGELOG.md) - [Commits](https://github.com/biomejs/biome/commits/@biomejs/biome@2.4.15/packages/@biomejs/biome) --- updated-dependencies: - dependency-name: "@biomejs/biome" dependency-version: 2.4.15 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> (cherry picked from commit7521c55910) * chore(deps-dev): bump @types/node from 25.6.0 to 25.7.0 (#1773) Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 25.6.0 to 25.7.0. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) --- updated-dependencies: - dependency-name: "@types/node" dependency-version: 25.7.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> (cherry picked from commitef734cca81) * feat: expose run id in STS client user-agent (#1774) * feat: expose run id in STS client user-agent Closes #483. This commit modifies the user-agent string so that it includes the GITHUB_RUN_ID and the GITHUB_RUN_ATTEMPT, in the format typically used by the SDK. User agent strings are logged to CloudTrail, allowing users to correlate CloudTrail events with GHA runs. We took this approach instead of logging the ACCESS_KEY_ID as suggested in the issue to avoid logging sensitive information. * feat: add github_action to ua string (cherry picked from commit29d1be3027) * feat: add additional session tags by default (#1775) Closes #390. Note that 50 session tags are the AWS default, and this commit changes our default set from 7 tags to 15 tags. This commit includes logic to split the tags into "required" vs "overridable". Required tags are this action's previous defaults and could never be overridden. Overridable tags are the new set and can be overridden by custom-tags. The action will not add tags if the addition plus the required plus the user's custom tags exceed the AWS limit of 50 total tags. This ensures backwards compat for the tag additions. (cherry picked from commite0ba768507) * chore: document forgejo compatibility (#1776) * chore: document forgejo compatibility * chore: linting fixes (cherry picked from commitf35a7d7d7e) * fix: skip credential check on output-env-credentials: false (#1778) Closes #1554. (cherry picked from commit58e7c47adf) * chore: update README for additional claim support (#1779) * chore: update README for additional claim support * chore: lint fix (whitespace) (cherry picked from commit713aaabfec) * chore(deps): bump @smithy/node-http-handler from 4.7.1 to 4.7.3 (#1781) Bumps [@smithy/node-http-handler](https://github.com/smithy-lang/smithy-typescript/tree/HEAD/packages/node-http-handler) from 4.7.1 to 4.7.3. - [Release notes](https://github.com/smithy-lang/smithy-typescript/releases) - [Changelog](https://github.com/smithy-lang/smithy-typescript/blob/main/packages/node-http-handler/CHANGELOG.md) - [Commits](https://github.com/smithy-lang/smithy-typescript/commits/@smithy/node-http-handler@4.7.3/packages/node-http-handler) --- updated-dependencies: - dependency-name: "@smithy/node-http-handler" dependency-version: 4.7.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> (cherry picked from commita7c33ae483) * chore(deps-dev): bump @types/node from 25.7.0 to 25.9.0 (#1785) Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 25.7.0 to 25.9.0. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) --- updated-dependencies: - dependency-name: "@types/node" dependency-version: 25.9.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> (cherry picked from commitffde832a1d) * chore(deps-dev): bump @aws-sdk/credential-provider-env (#1784) Bumps [@aws-sdk/credential-provider-env](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages-internal/credential-provider-env) from 3.972.34 to 3.972.38. - [Release notes](https://github.com/aws/aws-sdk-js-v3/releases) - [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/packages-internal/credential-provider-env/CHANGELOG.md) - [Commits](https://github.com/aws/aws-sdk-js-v3/commits/HEAD/packages-internal/credential-provider-env) --- updated-dependencies: - dependency-name: "@aws-sdk/credential-provider-env" dependency-version: 3.972.38 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> (cherry picked from commitbc1093db1d) * chore(deps-dev): bump @smithy/property-provider from 4.3.1 to 4.3.3 (#1783) Bumps [@smithy/property-provider](https://github.com/smithy-lang/smithy-typescript/tree/HEAD/packages/property-provider) from 4.3.1 to 4.3.3. - [Release notes](https://github.com/smithy-lang/smithy-typescript/releases) - [Changelog](https://github.com/smithy-lang/smithy-typescript/blob/main/packages/property-provider/CHANGELOG.md) - [Commits](https://github.com/smithy-lang/smithy-typescript/commits/@smithy/property-provider@4.3.3/packages/property-provider) --- updated-dependencies: - dependency-name: "@smithy/property-provider" dependency-version: 4.3.3 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> (cherry picked from commitfe6ad3af19) * chore(deps): bump @aws-sdk/client-sts from 3.1045.0 to 3.1049.0 (#1782) Bumps [@aws-sdk/client-sts](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-sts) from 3.1045.0 to 3.1049.0. - [Release notes](https://github.com/aws/aws-sdk-js-v3/releases) - [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-sts/CHANGELOG.md) - [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1049.0/clients/client-sts) --- updated-dependencies: - dependency-name: "@aws-sdk/client-sts" dependency-version: 3.1049.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> (cherry picked from commit4684f47f89) * chore: reconcile lockfile and test formatting * chore: Update dist --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Michael Lehmann <lehmanmj@amazon.com> Co-authored-by: Sylvain Verly <sylvain.verly@gmail.com>
804 lines
28 KiB
TypeScript
804 lines
28 KiB
TypeScript
import * as core from '@actions/core';
|
|
import { fs, vol } from 'memfs';
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import {
|
|
ensureAwsDirectoryExists,
|
|
getProfileFilePaths,
|
|
mergeProfileSection,
|
|
parseIni,
|
|
stringifyIni,
|
|
validateProfileName,
|
|
writeProfileFiles,
|
|
} from '../src/profileManager';
|
|
|
|
vi.mock('node:fs');
|
|
vi.mock('@actions/core');
|
|
|
|
describe('Profile Manager', {}, () => {
|
|
beforeEach(() => {
|
|
vi.restoreAllMocks();
|
|
vi.clearAllMocks();
|
|
vol.reset();
|
|
});
|
|
|
|
describe('parseIni', {}, () => {
|
|
it('parses a single section', {}, () => {
|
|
const result = parseIni('[dev]\naws_access_key_id=AKIA\naws_secret_access_key=secret\n');
|
|
expect(result.dev).toEqual({ aws_access_key_id: 'AKIA', aws_secret_access_key: 'secret' });
|
|
});
|
|
|
|
it('parses multiple sections', {}, () => {
|
|
const result = parseIni('[dev]\nkey=dev_val\n\n[prod]\nkey=prod_val\n');
|
|
expect(result.dev.key).toBe('dev_val');
|
|
expect(result.prod.key).toBe('prod_val');
|
|
});
|
|
|
|
it('skips comments and empty lines', {}, () => {
|
|
const result = parseIni('# comment\n; another comment\n\n[dev]\nkey=val\n');
|
|
expect(result.dev).toEqual({ key: 'val' });
|
|
});
|
|
|
|
it('trims whitespace around keys and values', {}, () => {
|
|
const result = parseIni('[dev]\n key = val \n');
|
|
expect(result.dev.key).toBe('val');
|
|
});
|
|
|
|
it('preserves section names with spaces (e.g. profile prefix)', {}, () => {
|
|
const result = parseIni('[profile dev]\nregion=us-east-1\n');
|
|
expect(result['profile dev']).toEqual({ region: 'us-east-1' });
|
|
});
|
|
|
|
it('guards against __proto__ section pollution', {}, () => {
|
|
const result = parseIni('[__proto__]\npolluted=true\n[safe]\nkey=val\n');
|
|
expect(result.__proto__).not.toHaveProperty('polluted');
|
|
expect(result.safe).toEqual({ key: 'val' });
|
|
});
|
|
|
|
it('guards against __proto__ key pollution', {}, () => {
|
|
const result = parseIni('[dev]\n__proto__=evil\naws_access_key_id=AKIA\n');
|
|
expect(result.dev).toEqual({ aws_access_key_id: 'AKIA' });
|
|
expect(result.dev).not.toHaveProperty('__proto__', 'evil');
|
|
});
|
|
|
|
it('handles values containing equals signs', {}, () => {
|
|
const result = parseIni('[dev]\naws_session_token=FwoGZXIvYXdzEBYa/base64==\n');
|
|
expect(result.dev.aws_session_token).toBe('FwoGZXIvYXdzEBYa/base64==');
|
|
});
|
|
|
|
it('handles empty values', {}, () => {
|
|
const result = parseIni('[dev]\ncli_pager=\n');
|
|
expect(result.dev.cli_pager).toBe('');
|
|
});
|
|
|
|
it('returns empty object for empty input', {}, () => {
|
|
expect(parseIni('')).toEqual({});
|
|
});
|
|
|
|
it('returns empty object for whitespace-only input', {}, () => {
|
|
expect(parseIni(' \n\n \n')).toEqual({});
|
|
});
|
|
|
|
it('handles Windows line endings (CRLF)', {}, () => {
|
|
const result = parseIni('[dev]\r\naws_access_key_id=AKIA\r\naws_secret_access_key=secret\r\n');
|
|
expect(result.dev).toEqual({ aws_access_key_id: 'AKIA', aws_secret_access_key: 'secret' });
|
|
});
|
|
});
|
|
|
|
describe('stringifyIni', {}, () => {
|
|
it('serializes a single section', {}, () => {
|
|
const result = stringifyIni({ dev: { key: 'val' } });
|
|
expect(result).toBe('[dev]\nkey = val\n');
|
|
});
|
|
|
|
it('serializes multiple sections with blank line separator', {}, () => {
|
|
const result = stringifyIni({ dev: { a: '1' }, prod: { b: '2' } });
|
|
expect(result).toBe('[dev]\na = 1\n\n[prod]\nb = 2\n');
|
|
});
|
|
|
|
it('round-trips through parseIni', {}, () => {
|
|
const data = {
|
|
dev: { aws_access_key_id: 'AKIA', aws_secret_access_key: 'secret' },
|
|
'profile prod': { region: 'us-west-2' },
|
|
};
|
|
const roundTripped = parseIni(stringifyIni(data));
|
|
expect(roundTripped).toEqual(data);
|
|
});
|
|
|
|
it('handles empty data object', {}, () => {
|
|
const result = stringifyIni({});
|
|
expect(result).toBe('\n');
|
|
expect(parseIni(result)).toEqual({});
|
|
});
|
|
|
|
it('handles section with no keys', {}, () => {
|
|
const result = stringifyIni({ dev: {} });
|
|
expect(result).toBe('[dev]\n');
|
|
});
|
|
});
|
|
|
|
describe('validateProfileName', {}, () => {
|
|
it('accepts valid profile names', {}, () => {
|
|
expect(() => validateProfileName('dev')).not.toThrow();
|
|
expect(() => validateProfileName('production')).not.toThrow();
|
|
expect(() => validateProfileName('my-profile-123')).not.toThrow();
|
|
expect(() => validateProfileName('default')).not.toThrow();
|
|
});
|
|
|
|
it('rejects empty profile names', {}, () => {
|
|
expect(() => validateProfileName('')).toThrow('aws-profile must not be empty');
|
|
expect(() => validateProfileName(' ')).toThrow('aws-profile must not be empty');
|
|
});
|
|
|
|
it('rejects profile names with whitespace', {}, () => {
|
|
expect(() => validateProfileName('my profile')).toThrow('aws-profile must not contain whitespace');
|
|
expect(() => validateProfileName('dev\ntest')).toThrow('aws-profile must not contain whitespace');
|
|
expect(() => validateProfileName('prod\tenv')).toThrow('aws-profile must not contain whitespace');
|
|
});
|
|
|
|
it('rejects profile names with brackets', {}, () => {
|
|
expect(() => validateProfileName('dev[test]')).toThrow('aws-profile must not contain brackets');
|
|
expect(() => validateProfileName('[profile]')).toThrow('aws-profile must not contain brackets');
|
|
});
|
|
|
|
it('rejects profile names with path separators', {}, () => {
|
|
expect(() => validateProfileName('dev/test')).toThrow('aws-profile must not contain path separators');
|
|
expect(() => validateProfileName('dev\\test')).toThrow('aws-profile must not contain path separators');
|
|
expect(() => validateProfileName('../etc/passwd')).toThrow('aws-profile must not contain path separators');
|
|
});
|
|
});
|
|
|
|
describe('getProfileFilePaths', {}, () => {
|
|
it('returns default paths when env vars not set', {}, () => {
|
|
delete process.env.AWS_SHARED_CREDENTIALS_FILE;
|
|
delete process.env.AWS_CONFIG_FILE;
|
|
|
|
const paths = getProfileFilePaths();
|
|
|
|
expect(paths.credentials).toMatch(/\.aws[/\\]credentials$/);
|
|
expect(paths.config).toMatch(/\.aws[/\\]config$/);
|
|
});
|
|
|
|
it('respects AWS_SHARED_CREDENTIALS_FILE env var', {}, () => {
|
|
process.env.AWS_SHARED_CREDENTIALS_FILE = '/custom/path/credentials';
|
|
process.env.AWS_CONFIG_FILE = '/custom/path/config';
|
|
|
|
const paths = getProfileFilePaths();
|
|
|
|
expect(paths.credentials).toBe('/custom/path/credentials');
|
|
expect(paths.config).toBe('/custom/path/config');
|
|
});
|
|
});
|
|
|
|
describe('ensureAwsDirectoryExists', {}, () => {
|
|
it('creates directory if it does not exist', {}, () => {
|
|
const filePath = '/home/runner/.aws/credentials';
|
|
|
|
ensureAwsDirectoryExists(filePath);
|
|
|
|
expect(fs.existsSync('/home/runner/.aws')).toBe(true);
|
|
});
|
|
|
|
it('does not error if directory already exists', {}, () => {
|
|
const filePath = '/home/runner/.aws/credentials';
|
|
|
|
fs.mkdirSync('/home/runner/.aws', { recursive: true });
|
|
|
|
expect(() => ensureAwsDirectoryExists(filePath)).not.toThrow();
|
|
});
|
|
|
|
it('creates nested directories', {}, () => {
|
|
const filePath = '/home/runner/custom/path/.aws/credentials';
|
|
|
|
ensureAwsDirectoryExists(filePath);
|
|
|
|
expect(fs.existsSync('/home/runner/custom/path/.aws')).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('mergeProfileSection', {}, () => {
|
|
it('creates new file with profile section', {}, () => {
|
|
const filePath = '/home/runner/.aws/credentials';
|
|
fs.mkdirSync('/home/runner/.aws', { recursive: true });
|
|
|
|
mergeProfileSection(
|
|
filePath,
|
|
'dev',
|
|
{
|
|
aws_access_key_id: 'AKIAIOSFODNN7EXAMPLE',
|
|
aws_secret_access_key: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
|
|
},
|
|
false,
|
|
);
|
|
|
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
const parsed = parseIni(content);
|
|
|
|
expect(parsed.dev).toBeDefined();
|
|
expect(parsed.dev.aws_access_key_id).toBe('AKIAIOSFODNN7EXAMPLE');
|
|
expect(parsed.dev.aws_secret_access_key).toBe('wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY');
|
|
});
|
|
|
|
it('merges with existing profiles', {}, () => {
|
|
const filePath = '/home/runner/.aws/credentials';
|
|
fs.mkdirSync('/home/runner/.aws', { recursive: true });
|
|
|
|
// Create initial profile
|
|
mergeProfileSection(
|
|
filePath,
|
|
'dev',
|
|
{
|
|
aws_access_key_id: 'AKIAIOSFODNN7EXAMPLE',
|
|
aws_secret_access_key: 'devSecretKey',
|
|
},
|
|
false,
|
|
);
|
|
|
|
// Add second profile
|
|
mergeProfileSection(
|
|
filePath,
|
|
'prod',
|
|
{
|
|
aws_access_key_id: 'AKIAPRODEXAMPLE',
|
|
aws_secret_access_key: 'prodSecretKey',
|
|
},
|
|
false,
|
|
);
|
|
|
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
const parsed = parseIni(content);
|
|
|
|
expect(parsed.dev).toBeDefined();
|
|
expect(parsed.dev.aws_access_key_id).toBe('AKIAIOSFODNN7EXAMPLE');
|
|
expect(parsed.prod).toBeDefined();
|
|
expect(parsed.prod.aws_access_key_id).toBe('AKIAPRODEXAMPLE');
|
|
});
|
|
|
|
it('overwrites existing profile with same name', {}, () => {
|
|
const filePath = '/home/runner/.aws/credentials';
|
|
fs.mkdirSync('/home/runner/.aws', { recursive: true });
|
|
|
|
// Create initial profile
|
|
mergeProfileSection(
|
|
filePath,
|
|
'dev',
|
|
{
|
|
aws_access_key_id: 'OLD_KEY',
|
|
aws_secret_access_key: 'oldSecretKey',
|
|
aws_session_token: 'oldSessionToken',
|
|
},
|
|
false,
|
|
);
|
|
|
|
// Overwrite with new credentials
|
|
mergeProfileSection(
|
|
filePath,
|
|
'dev',
|
|
{
|
|
aws_access_key_id: 'NEW_KEY',
|
|
aws_secret_access_key: 'newSecretKey',
|
|
aws_session_token: 'newSessionToken',
|
|
},
|
|
true,
|
|
);
|
|
|
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
const parsed = parseIni(content);
|
|
|
|
expect(parsed.dev.aws_access_key_id).toBe('NEW_KEY');
|
|
expect(parsed.dev.aws_secret_access_key).toBe('newSecretKey');
|
|
expect(parsed.dev.aws_session_token).toBe('newSessionToken');
|
|
});
|
|
|
|
it('overwriting a profile removes stale keys', {}, () => {
|
|
const filePath = '/home/runner/.aws/credentials';
|
|
fs.mkdirSync('/home/runner/.aws', { recursive: true });
|
|
|
|
// Create profile with session token
|
|
mergeProfileSection(
|
|
filePath,
|
|
'dev',
|
|
{
|
|
aws_access_key_id: 'AKIA',
|
|
aws_secret_access_key: 'secret',
|
|
aws_session_token: 'old-token',
|
|
},
|
|
false,
|
|
);
|
|
|
|
// Overwrite without session token
|
|
mergeProfileSection(
|
|
filePath,
|
|
'dev',
|
|
{
|
|
aws_access_key_id: 'AKIA2',
|
|
aws_secret_access_key: 'secret2',
|
|
},
|
|
true,
|
|
);
|
|
|
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
const parsed = parseIni(content);
|
|
|
|
expect(parsed.dev.aws_access_key_id).toBe('AKIA2');
|
|
expect(parsed.dev.aws_secret_access_key).toBe('secret2');
|
|
expect(parsed.dev.aws_session_token).toBeUndefined();
|
|
});
|
|
|
|
it('handles empty existing file', {}, () => {
|
|
const filePath = '/home/runner/.aws/credentials';
|
|
fs.mkdirSync('/home/runner/.aws', { recursive: true });
|
|
fs.writeFileSync(filePath, '', { mode: 0o600 });
|
|
|
|
mergeProfileSection(
|
|
filePath,
|
|
'dev',
|
|
{
|
|
aws_access_key_id: 'AKIA',
|
|
aws_secret_access_key: 'secret',
|
|
},
|
|
false,
|
|
);
|
|
|
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
const parsed = parseIni(content);
|
|
|
|
expect(parsed.dev.aws_access_key_id).toBe('AKIA');
|
|
expect(parsed.dev.aws_secret_access_key).toBe('secret');
|
|
});
|
|
|
|
it('errors if profile name already exists but overwrite flag is false', {}, () => {
|
|
const filePath = '/home/runner/.aws/credentials';
|
|
fs.mkdirSync('/home/runner/.aws', { recursive: true });
|
|
|
|
// Create initial profile
|
|
mergeProfileSection(
|
|
filePath,
|
|
'dev',
|
|
{
|
|
aws_access_key_id: 'OLD_KEY',
|
|
aws_secret_access_key: 'oldSecretKey',
|
|
},
|
|
false,
|
|
);
|
|
|
|
// Overwrite with new credentials
|
|
expect(() =>
|
|
mergeProfileSection(
|
|
filePath,
|
|
'dev',
|
|
{
|
|
aws_access_key_id: 'NEW_KEY',
|
|
aws_secret_access_key: 'newSecretKey',
|
|
aws_session_token: 'sessionToken',
|
|
},
|
|
false,
|
|
),
|
|
).toThrow(
|
|
`Profile with name "dev" already exists. Please use the overwrite-aws-profile input if you want to overwrite existing profiles.`,
|
|
);
|
|
|
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
const parsed = parseIni(content);
|
|
|
|
expect(parsed.dev.aws_access_key_id).toBe('OLD_KEY');
|
|
expect(parsed.dev.aws_secret_access_key).toBe('oldSecretKey');
|
|
expect(parsed.dev.aws_session_token).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('writeProfileFiles', {}, () => {
|
|
beforeEach(() => {
|
|
delete process.env.AWS_SHARED_CREDENTIALS_FILE;
|
|
delete process.env.AWS_CONFIG_FILE;
|
|
});
|
|
|
|
it('writes credentials and config for new profile', {}, () => {
|
|
writeProfileFiles(
|
|
'dev',
|
|
{
|
|
AccessKeyId: 'AKIAIOSFODNN7EXAMPLE',
|
|
SecretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
|
|
SessionToken: 'FwoGZXIvYXdzEBYaDEXAMPLE',
|
|
},
|
|
'us-east-1',
|
|
false,
|
|
);
|
|
|
|
// Check credentials file
|
|
const credsPath = getProfileFilePaths().credentials;
|
|
const credContent = fs.readFileSync(credsPath, 'utf-8');
|
|
const credParsed = parseIni(credContent);
|
|
|
|
expect(credParsed.dev).toBeDefined();
|
|
expect(credParsed.dev.aws_access_key_id).toBe('AKIAIOSFODNN7EXAMPLE');
|
|
expect(credParsed.dev.aws_secret_access_key).toBe('wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY');
|
|
expect(credParsed.dev.aws_session_token).toBe('FwoGZXIvYXdzEBYaDEXAMPLE');
|
|
|
|
// Check config file
|
|
const configPath = getProfileFilePaths().config;
|
|
const configContent = fs.readFileSync(configPath, 'utf-8');
|
|
const configParsed = parseIni(configContent);
|
|
|
|
expect(configParsed['profile dev']).toBeDefined();
|
|
expect(configParsed['profile dev'].region).toBe('us-east-1');
|
|
});
|
|
|
|
it('uses correct section naming for default profile', {}, () => {
|
|
writeProfileFiles(
|
|
'default',
|
|
{
|
|
AccessKeyId: 'AKIAIOSFODNN7EXAMPLE',
|
|
SecretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
|
|
},
|
|
'us-west-2',
|
|
false,
|
|
);
|
|
|
|
// Check credentials file uses [default]
|
|
const credsPath = getProfileFilePaths().credentials;
|
|
const credContent = fs.readFileSync(credsPath, 'utf-8');
|
|
const credParsed = parseIni(credContent);
|
|
|
|
expect(credParsed.default).toBeDefined();
|
|
expect(credParsed['profile default']).toBeUndefined();
|
|
|
|
// Check config file uses [default] (not [profile default])
|
|
const configPath = getProfileFilePaths().config;
|
|
const configContent = fs.readFileSync(configPath, 'utf-8');
|
|
const configParsed = parseIni(configContent);
|
|
|
|
expect(configParsed.default).toBeDefined();
|
|
expect(configParsed['profile default']).toBeUndefined();
|
|
});
|
|
|
|
it('supports multiple profiles', {}, () => {
|
|
// Write first profile
|
|
writeProfileFiles(
|
|
'dev',
|
|
{
|
|
AccessKeyId: 'AKIADEV',
|
|
SecretAccessKey: 'devSecret',
|
|
},
|
|
'us-east-1',
|
|
false,
|
|
);
|
|
|
|
// Write second profile
|
|
writeProfileFiles(
|
|
'prod',
|
|
{
|
|
AccessKeyId: 'AKIAPROD',
|
|
SecretAccessKey: 'prodSecret',
|
|
SessionToken: 'prodToken',
|
|
},
|
|
'us-west-2',
|
|
false,
|
|
);
|
|
|
|
// Verify both profiles exist
|
|
const credsPath = getProfileFilePaths().credentials;
|
|
const credContent = fs.readFileSync(credsPath, 'utf-8');
|
|
const credParsed = parseIni(credContent);
|
|
|
|
expect(credParsed.dev).toBeDefined();
|
|
expect(credParsed.prod).toBeDefined();
|
|
expect(credParsed.dev.aws_access_key_id).toBe('AKIADEV');
|
|
expect(credParsed.prod.aws_access_key_id).toBe('AKIAPROD');
|
|
});
|
|
|
|
it('handles credentials without session token', {}, () => {
|
|
writeProfileFiles(
|
|
'dev',
|
|
{
|
|
AccessKeyId: 'AKIAIOSFODNN7EXAMPLE',
|
|
SecretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
|
|
},
|
|
'us-east-1',
|
|
false,
|
|
);
|
|
|
|
const credsPath = getProfileFilePaths().credentials;
|
|
const credContent = fs.readFileSync(credsPath, 'utf-8');
|
|
const credParsed = parseIni(credContent);
|
|
|
|
expect(credParsed.dev.aws_access_key_id).toBe('AKIAIOSFODNN7EXAMPLE');
|
|
expect(credParsed.dev.aws_secret_access_key).toBe('wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY');
|
|
expect(credParsed.dev.aws_session_token).toBeUndefined();
|
|
});
|
|
|
|
it('throws error for invalid profile name', {}, () => {
|
|
expect(() =>
|
|
writeProfileFiles(
|
|
'invalid profile',
|
|
{
|
|
AccessKeyId: 'AKIA',
|
|
SecretAccessKey: 'secret',
|
|
},
|
|
'us-east-1',
|
|
false,
|
|
),
|
|
).toThrow('Failed to write AWS profile');
|
|
expect(() =>
|
|
writeProfileFiles(
|
|
'invalid profile',
|
|
{
|
|
AccessKeyId: 'AKIA',
|
|
SecretAccessKey: 'secret',
|
|
},
|
|
'us-east-1',
|
|
false,
|
|
),
|
|
).toThrow('whitespace');
|
|
});
|
|
|
|
it('respects custom file paths from env vars', {}, () => {
|
|
process.env.AWS_SHARED_CREDENTIALS_FILE = '/custom/credentials';
|
|
process.env.AWS_CONFIG_FILE = '/custom/config';
|
|
|
|
fs.mkdirSync('/custom', { recursive: true });
|
|
|
|
writeProfileFiles(
|
|
'dev',
|
|
{
|
|
AccessKeyId: 'AKIA',
|
|
SecretAccessKey: 'secret',
|
|
},
|
|
'us-east-1',
|
|
false,
|
|
);
|
|
|
|
expect(fs.existsSync('/custom/credentials')).toBe(true);
|
|
expect(fs.existsSync('/custom/config')).toBe(true);
|
|
});
|
|
|
|
it('logs info messages', {}, () => {
|
|
writeProfileFiles(
|
|
'dev',
|
|
{
|
|
AccessKeyId: 'AKIA',
|
|
SecretAccessKey: 'secret',
|
|
},
|
|
'us-east-1',
|
|
false,
|
|
);
|
|
|
|
expect(core.info).toHaveBeenCalledWith('Writing credentials to profile: dev');
|
|
expect(core.info).toHaveBeenCalledWith('Writing config to profile: dev');
|
|
expect(core.info).toHaveBeenCalledWith('✓ Successfully configured AWS profile: dev');
|
|
});
|
|
|
|
it('preserves pre-existing unrelated profiles in credentials file', {}, () => {
|
|
const credsPath = getProfileFilePaths().credentials;
|
|
fs.mkdirSync(require('node:path').dirname(credsPath), { recursive: true });
|
|
fs.writeFileSync(
|
|
credsPath,
|
|
'[personal]\naws_access_key_id=AKIAPERSONAL\naws_secret_access_key=personalSecret\naws_session_token=personalToken\n',
|
|
);
|
|
|
|
writeProfileFiles('dev', { AccessKeyId: 'AKIADEV', SecretAccessKey: 'devSecret' }, 'us-east-1', false);
|
|
|
|
const content = fs.readFileSync(credsPath, 'utf-8');
|
|
const parsed = parseIni(content);
|
|
|
|
// Pre-existing profile must be fully intact
|
|
expect(parsed.personal).toEqual({
|
|
aws_access_key_id: 'AKIAPERSONAL',
|
|
aws_secret_access_key: 'personalSecret',
|
|
aws_session_token: 'personalToken',
|
|
});
|
|
// New profile also present
|
|
expect(parsed.dev.aws_access_key_id).toBe('AKIADEV');
|
|
});
|
|
|
|
it('preserves pre-existing config with extra keys', {}, () => {
|
|
const configPath = getProfileFilePaths().config;
|
|
fs.mkdirSync(require('node:path').dirname(configPath), { recursive: true });
|
|
fs.writeFileSync(configPath, '[profile personal]\nregion=eu-west-1\noutput=json\ncli_pager=\n');
|
|
|
|
writeProfileFiles('dev', { AccessKeyId: 'AKIA', SecretAccessKey: 'secret' }, 'us-east-1', false);
|
|
|
|
const content = fs.readFileSync(configPath, 'utf-8');
|
|
const parsed = parseIni(content);
|
|
|
|
expect(parsed['profile personal']).toEqual({
|
|
region: 'eu-west-1',
|
|
output: 'json',
|
|
cli_pager: '',
|
|
});
|
|
expect(parsed['profile dev'].region).toBe('us-east-1');
|
|
});
|
|
|
|
it('preserves pre-existing default profile when writing a named profile', {}, () => {
|
|
const credsPath = getProfileFilePaths().credentials;
|
|
fs.mkdirSync(require('node:path').dirname(credsPath), { recursive: true });
|
|
fs.writeFileSync(credsPath, '[default]\naws_access_key_id=AKIADEFAULT\naws_secret_access_key=defaultSecret\n');
|
|
|
|
writeProfileFiles('dev', { AccessKeyId: 'AKIADEV', SecretAccessKey: 'devSecret' }, 'us-west-2', false);
|
|
|
|
const content = fs.readFileSync(credsPath, 'utf-8');
|
|
const parsed = parseIni(content);
|
|
|
|
expect(parsed.default).toEqual({
|
|
aws_access_key_id: 'AKIADEFAULT',
|
|
aws_secret_access_key: 'defaultSecret',
|
|
});
|
|
expect(parsed.dev.aws_access_key_id).toBe('AKIADEV');
|
|
});
|
|
|
|
it('comments in pre-existing files are stripped on round-trip', {}, () => {
|
|
const credsPath = getProfileFilePaths().credentials;
|
|
fs.mkdirSync(require('node:path').dirname(credsPath), { recursive: true });
|
|
fs.writeFileSync(
|
|
credsPath,
|
|
'# My important comment\n[personal]\naws_access_key_id=AKIA\naws_secret_access_key=secret\n',
|
|
);
|
|
|
|
writeProfileFiles('dev', { AccessKeyId: 'AKIADEV', SecretAccessKey: 'devSecret' }, 'us-east-1', false);
|
|
|
|
const content = fs.readFileSync(credsPath, 'utf-8') as string;
|
|
|
|
// Comment is lost (known trade-off), but profile data is preserved
|
|
expect(content).not.toContain('# My important comment');
|
|
const parsed = parseIni(content);
|
|
expect(parsed.personal.aws_access_key_id).toBe('AKIA');
|
|
expect(parsed.dev.aws_access_key_id).toBe('AKIADEV');
|
|
});
|
|
|
|
it('writes empty section when credentials object has no keys', {}, () => {
|
|
writeProfileFiles('dev', {}, 'us-east-1', false);
|
|
|
|
const credsPath = getProfileFilePaths().credentials;
|
|
const credContent = fs.readFileSync(credsPath, 'utf-8');
|
|
const credParsed = parseIni(credContent);
|
|
|
|
// Section exists but has no credential keys
|
|
expect(credParsed.dev).toEqual({});
|
|
|
|
// Config still gets region
|
|
const configPath = getProfileFilePaths().config;
|
|
const configContent = fs.readFileSync(configPath, 'utf-8');
|
|
const configParsed = parseIni(configContent);
|
|
expect(configParsed['profile dev'].region).toBe('us-east-1');
|
|
});
|
|
|
|
it('resolves credentials and config paths independently from env vars', {}, () => {
|
|
process.env.AWS_SHARED_CREDENTIALS_FILE = '/custom-creds/credentials';
|
|
// AWS_CONFIG_FILE is NOT set — should use default path
|
|
|
|
fs.mkdirSync('/custom-creds', { recursive: true });
|
|
|
|
writeProfileFiles('dev', { AccessKeyId: 'AKIA', SecretAccessKey: 'secret' }, 'us-east-1', false);
|
|
|
|
expect(fs.existsSync('/custom-creds/credentials')).toBe(true);
|
|
// Config file should be at the default path (under homedir)
|
|
const defaultConfigPath = require('node:path').join(require('node:os').homedir(), '.aws', 'config');
|
|
expect(fs.existsSync(defaultConfigPath)).toBe(true);
|
|
});
|
|
|
|
it('produces AWS CLI-compatible INI output (golden file)', {}, () => {
|
|
writeProfileFiles(
|
|
'dev',
|
|
{
|
|
AccessKeyId: 'AKIAIOSFODNN7EXAMPLE',
|
|
SecretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
|
|
SessionToken: 'FwoGZXIvYXdzEBYaDEXAMPLE',
|
|
},
|
|
'us-east-1',
|
|
false,
|
|
);
|
|
|
|
const credsPath = getProfileFilePaths().credentials;
|
|
const credContent = fs.readFileSync(credsPath, 'utf-8');
|
|
|
|
const configPath = getProfileFilePaths().config;
|
|
const configContent = fs.readFileSync(configPath, 'utf-8');
|
|
|
|
// Verify exact byte-for-byte format matching AWS CLI style:
|
|
// - [section] header on its own line
|
|
// - key = value with spaces around =
|
|
// - LF line endings, trailing newline
|
|
expect(credContent).toBe(
|
|
'[dev]\n' +
|
|
'aws_access_key_id = AKIAIOSFODNN7EXAMPLE\n' +
|
|
'aws_secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY\n' +
|
|
'aws_session_token = FwoGZXIvYXdzEBYaDEXAMPLE\n',
|
|
);
|
|
expect(configContent).toBe('[profile dev]\n' + 'region = us-east-1\n');
|
|
});
|
|
|
|
it('golden file for multi-profile output', {}, () => {
|
|
writeProfileFiles('dev', { AccessKeyId: 'AKIADEV', SecretAccessKey: 'devSecret' }, 'us-east-1', false);
|
|
writeProfileFiles(
|
|
'prod',
|
|
{ AccessKeyId: 'AKIAPROD', SecretAccessKey: 'prodSecret', SessionToken: 'prodToken' },
|
|
'us-west-2',
|
|
false,
|
|
);
|
|
|
|
const credsPath = getProfileFilePaths().credentials;
|
|
const credContent = fs.readFileSync(credsPath, 'utf-8');
|
|
|
|
const configPath = getProfileFilePaths().config;
|
|
const configContent = fs.readFileSync(configPath, 'utf-8');
|
|
|
|
expect(credContent).toBe(
|
|
'[dev]\n' +
|
|
'aws_access_key_id = AKIADEV\n' +
|
|
'aws_secret_access_key = devSecret\n' +
|
|
'\n' +
|
|
'[prod]\n' +
|
|
'aws_access_key_id = AKIAPROD\n' +
|
|
'aws_secret_access_key = prodSecret\n' +
|
|
'aws_session_token = prodToken\n',
|
|
);
|
|
expect(configContent).toBe(
|
|
'[profile dev]\n' + 'region = us-east-1\n' + '\n' + '[profile prod]\n' + 'region = us-west-2\n',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('symlink hardening', {}, () => {
|
|
const credsPath = '/home/user/.aws/credentials';
|
|
const configPath = '/home/user/.aws/config';
|
|
|
|
beforeEach(() => {
|
|
process.env.AWS_SHARED_CREDENTIALS_FILE = credsPath;
|
|
process.env.AWS_CONFIG_FILE = configPath;
|
|
});
|
|
|
|
it('mergeProfileSection refuses when the credentials path is a symlink and leaves the target unchanged', {}, () => {
|
|
fs.mkdirSync('/home/user/.aws', { recursive: true });
|
|
fs.mkdirSync('/etc', { recursive: true });
|
|
fs.writeFileSync('/etc/passwd', 'root:x:0:0::/root:/bin/sh');
|
|
fs.symlinkSync('/etc/passwd', credsPath);
|
|
|
|
expect(() => mergeProfileSection(credsPath, 'dev', { aws_access_key_id: 'AKIA' }, true)).toThrow(
|
|
/Refusing .* \(.* symbolic link\)/,
|
|
);
|
|
expect(fs.readFileSync('/etc/passwd', 'utf-8')).toBe('root:x:0:0::/root:/bin/sh');
|
|
});
|
|
|
|
it('mergeProfileSection refuses when the config path is a symlink', {}, () => {
|
|
fs.mkdirSync('/home/user/.aws', { recursive: true });
|
|
fs.mkdirSync('/etc', { recursive: true });
|
|
fs.writeFileSync('/etc/sensitive', 'do not overwrite');
|
|
fs.symlinkSync('/etc/sensitive', configPath);
|
|
|
|
expect(() => mergeProfileSection(configPath, 'profile dev', { region: 'us-east-1' }, true)).toThrow(
|
|
/Refusing .* \(.* symbolic link\)/,
|
|
);
|
|
expect(fs.readFileSync('/etc/sensitive', 'utf-8')).toBe('do not overwrite');
|
|
});
|
|
|
|
it('ensureAwsDirectoryExists refuses when ~/.aws is a symlink', {}, () => {
|
|
fs.mkdirSync('/real-target', { recursive: true });
|
|
fs.mkdirSync('/home/user', { recursive: true });
|
|
fs.symlinkSync('/real-target', '/home/user/.aws');
|
|
|
|
expect(() => ensureAwsDirectoryExists(credsPath)).toThrow(/Refusing .* \(.* symbolic link\)/);
|
|
});
|
|
|
|
it('writeProfileFiles refuses to overwrite a pre-existing symlink at the credentials path', {}, () => {
|
|
fs.mkdirSync('/home/user/.aws', { recursive: true });
|
|
fs.mkdirSync('/etc', { recursive: true });
|
|
fs.writeFileSync('/etc/passwd', 'root:x:0:0::/root:/bin/sh');
|
|
fs.symlinkSync('/etc/passwd', credsPath);
|
|
|
|
expect(() =>
|
|
writeProfileFiles('dev', { AccessKeyId: 'AKIA', SecretAccessKey: 'secret' }, 'us-east-1', true),
|
|
).toThrow(/Refusing .* \(.* symbolic link\)/);
|
|
|
|
expect(fs.lstatSync(credsPath).isSymbolicLink()).toBe(true);
|
|
expect(fs.readFileSync('/etc/passwd', 'utf-8')).toBe('root:x:0:0::/root:/bin/sh');
|
|
});
|
|
|
|
it('happy path still writes both files with mode 0o600 when no symlinks are present', {}, () => {
|
|
writeProfileFiles('dev', { AccessKeyId: 'AKIA', SecretAccessKey: 'secret' }, 'us-east-1', false);
|
|
|
|
expect(fs.statSync(credsPath).mode & 0o777).toBe(0o600);
|
|
expect(fs.statSync(configPath).mode & 0o777).toBe(0o600);
|
|
expect(fs.lstatSync(credsPath).isSymbolicLink()).toBe(false);
|
|
expect(fs.lstatSync(configPath).isSymbolicLink()).toBe(false);
|
|
});
|
|
});
|
|
});
|