mirror of
https://github.com/aws-actions/configure-aws-credentials.git
synced 2026-09-03 06:05:04 +09:00
aa6526434b
* fix: enforce allowed-account-ids when the list contains empty entries An empty first element previously short-circuited the allowed account check. Empty entries are now filtered out and validation applies whenever any non-empty entry exists. * fix: enforce allowed-account-ids on the use-existing-credentials path The early return for valid pre-existing credentials skipped the allowed-account-ids check, now included. * fix: reject newlines in names and values when writing profile files If the profile file writing was enabled, we emitted newlines into the file verbatim, permitting injecting arbitrary profiles into the file. Writing now fails instead. * fix: honor configured STS endpoint for "ambient" credentials Ambient credential resolution built a bare STS client, so a web-identity token found by the SDK default chain (e.g. AWS_WEB_IDENTITY_TOKEN_FILE on a self-hosted runner) was exchanged with public STS instead of any operator-configured sts-endpoint. Resolution now passes the configured region, endpoint, and proxy handler to the default provider chain. * fix: mask proxy URL credentials in job logs Basic-auth userinfo in the http-proxy input or HTTP(S)_PROXY environment variables was never registered as a secret, so error messages carrying the proxy URL printed the credentials unmasked in the job log. * fix: omit account IDs from the allowed-account-ids failure message The mismatch error is thrown before exportAccountId registers the account-id mask, so setFailed wrote the raw account ID (and the configured allow-list) into a public annotation. (C4) * chore: remove outdated examples All of the examples were out of date and we do not have a mechanism for keeping them up to date. Removed the examples.
214 lines
6.8 KiB
TypeScript
214 lines
6.8 KiB
TypeScript
import * as os from 'node:os';
|
|
import * as path from 'node:path';
|
|
import * as core from '@actions/core';
|
|
import type { Credentials } from '@aws-sdk/client-sts';
|
|
import { mkdir, readFileUtf8, writeFileUtf8 } from './helpers';
|
|
|
|
/**
|
|
* Parse an INI-format string into a nested object.
|
|
* Preserves literal section names (e.g. "profile dev" stays as-is).
|
|
*/
|
|
export function parseIni(iniData: string): Record<string, Record<string, string>> {
|
|
const result: Record<string, Record<string, string>> = {};
|
|
let currentSection: string | undefined;
|
|
|
|
for (const line of iniData.split(/\r?\n/)) {
|
|
const trimmed = line.trim();
|
|
if (!trimmed || trimmed.startsWith(';') || trimmed.startsWith('#')) {
|
|
continue;
|
|
}
|
|
|
|
const sectionMatch = trimmed.match(/^\[([^\]]*)\]$/);
|
|
if (sectionMatch) {
|
|
currentSection = sectionMatch[1] as string;
|
|
if (currentSection === '__proto__') {
|
|
currentSection = undefined;
|
|
continue;
|
|
}
|
|
result[currentSection] = result[currentSection] || {};
|
|
continue;
|
|
}
|
|
|
|
if (currentSection) {
|
|
const eqIndex = trimmed.indexOf('=');
|
|
if (eqIndex > 0) {
|
|
const key = trimmed.substring(0, eqIndex).trim();
|
|
const value = trimmed.substring(eqIndex + 1).trim();
|
|
if (key !== '__proto__') {
|
|
const section = result[currentSection];
|
|
if (section) {
|
|
section[key] = value;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Serialize a nested object into INI-format string.
|
|
*/
|
|
export function stringifyIni(data: Record<string, Record<string, string>>): string {
|
|
const sections: string[] = [];
|
|
for (const [sectionName, sectionData] of Object.entries(data)) {
|
|
if (/[\r\n]/.test(sectionName)) {
|
|
throw new Error('INI section names must not contain newline characters');
|
|
}
|
|
const lines: string[] = [`[${sectionName}]`];
|
|
for (const [key, value] of Object.entries(sectionData)) {
|
|
// A newline in a key or value would inject arbitrary INI lines (e.g. credential_process).
|
|
if (/[\r\n]/.test(key) || /[\r\n]/.test(value)) {
|
|
throw new Error('INI keys and values must not contain newline characters');
|
|
}
|
|
lines.push(`${key} = ${value}`);
|
|
}
|
|
sections.push(lines.join('\n'));
|
|
}
|
|
return `${sections.join('\n\n')}\n`;
|
|
}
|
|
|
|
interface ProfileFilePaths {
|
|
credentials: string;
|
|
config: string;
|
|
}
|
|
|
|
/**
|
|
* Get the file paths for AWS credentials and config files
|
|
* Respects AWS_SHARED_CREDENTIALS_FILE and AWS_CONFIG_FILE environment variables
|
|
*/
|
|
export function getProfileFilePaths(): ProfileFilePaths {
|
|
const credentialsPath = process.env.AWS_SHARED_CREDENTIALS_FILE || path.join(os.homedir(), '.aws', 'credentials');
|
|
const configPath = process.env.AWS_CONFIG_FILE || path.join(os.homedir(), '.aws', 'config');
|
|
|
|
return {
|
|
credentials: credentialsPath,
|
|
config: configPath,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Ensure the AWS directory exists with secure permissions
|
|
* Creates the directory with 700 permissions (rwx for owner only)
|
|
*/
|
|
export function ensureAwsDirectoryExists(filePath: string): void {
|
|
const dir = path.dirname(filePath);
|
|
core.debug(`Ensuring directory exists: ${dir}`);
|
|
mkdir(dir, 0o700);
|
|
}
|
|
|
|
/**
|
|
* Validate profile name format
|
|
* Profile names must be non-empty, contain no whitespace, brackets, or path separators
|
|
*/
|
|
export function validateProfileName(profileName: string): void {
|
|
if (!profileName || profileName.trim() === '') {
|
|
throw new Error('aws-profile must not be empty');
|
|
}
|
|
|
|
if (/\s/.test(profileName)) {
|
|
throw new Error('aws-profile must not contain whitespace');
|
|
}
|
|
|
|
// INI section names can't contain brackets
|
|
if (/[[\]]/.test(profileName)) {
|
|
throw new Error('aws-profile must not contain brackets');
|
|
}
|
|
|
|
// Prevent path traversal
|
|
if (profileName.includes('/') || profileName.includes('\\')) {
|
|
throw new Error('aws-profile must not contain path separators');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Merge a profile section into an INI file
|
|
* Reads existing file, updates the specified section, and writes back
|
|
*/
|
|
export function mergeProfileSection(
|
|
filePath: string,
|
|
sectionName: string,
|
|
data: Record<string, string>,
|
|
overwriteAwsProfile: boolean,
|
|
): void {
|
|
const fileContent = readFileUtf8(filePath);
|
|
const existingContent: Record<string, Record<string, string>> = fileContent === null ? {} : parseIni(fileContent);
|
|
|
|
if (existingContent[sectionName] && !overwriteAwsProfile) {
|
|
throw new Error(
|
|
`Profile with name "${sectionName}" already exists. Please use the overwrite-aws-profile input if you want to overwrite existing profiles.`,
|
|
);
|
|
}
|
|
// Merge: update existing profile or add new one
|
|
existingContent[sectionName] = data;
|
|
|
|
const content = stringifyIni(existingContent);
|
|
|
|
core.debug(`Writing profile to ${filePath}`);
|
|
writeFileUtf8(filePath, content, 0o600);
|
|
}
|
|
|
|
/**
|
|
* Write AWS profile files with credentials and configuration
|
|
* This is the main entry point for profile file operations
|
|
*
|
|
* @param profileName - Name of the AWS profile to configure
|
|
* @param credentials - AWS credentials (access key, secret key, session token)
|
|
* @param region - AWS region
|
|
*/
|
|
export function writeProfileFiles(
|
|
profileName: string,
|
|
credentials: Partial<Credentials>,
|
|
region: string,
|
|
overwriteAwsProfile: boolean,
|
|
): void {
|
|
try {
|
|
// Validate profile name
|
|
validateProfileName(profileName);
|
|
|
|
const paths = getProfileFilePaths();
|
|
|
|
// Ensure .aws directory exists
|
|
ensureAwsDirectoryExists(paths.credentials);
|
|
ensureAwsDirectoryExists(paths.config);
|
|
|
|
// Prepare credentials data
|
|
const credentialsData: Record<string, string> = {};
|
|
if (credentials.AccessKeyId) {
|
|
credentialsData.aws_access_key_id = credentials.AccessKeyId;
|
|
}
|
|
if (credentials.SecretAccessKey) {
|
|
credentialsData.aws_secret_access_key = credentials.SecretAccessKey;
|
|
}
|
|
if (credentials.SessionToken) {
|
|
credentialsData.aws_session_token = credentials.SessionToken;
|
|
}
|
|
|
|
// Credentials file uses [profileName] syntax
|
|
const credsSectionName = profileName;
|
|
|
|
// Config file uses [profile profileName] syntax, except for 'default'
|
|
const configSectionName = profileName === 'default' ? 'default' : `profile ${profileName}`;
|
|
|
|
// Prepare config data
|
|
const configData: Record<string, string> = {
|
|
region: region,
|
|
};
|
|
|
|
// Write to credentials file
|
|
core.info(`Writing credentials to profile: ${profileName}`);
|
|
mergeProfileSection(paths.credentials, credsSectionName, credentialsData, overwriteAwsProfile);
|
|
|
|
// Write to config file
|
|
core.info(`Writing config to profile: ${profileName}`);
|
|
mergeProfileSection(paths.config, configSectionName, configData, overwriteAwsProfile);
|
|
|
|
core.info(`✓ Successfully configured AWS profile: ${profileName}`);
|
|
} catch (error) {
|
|
throw new Error(
|
|
`Failed to write AWS profile '${profileName}': ${error instanceof Error ? error.message : String(error)}`,
|
|
);
|
|
}
|
|
}
|