Skip to content

Instantly share code, notes, and snippets.

@jeiea
Last active September 1, 2023 05:24
Show Gist options
  • Save jeiea/b3a8b03aea3ecd0f9b5f17b595606a0a to your computer and use it in GitHub Desktop.
Save jeiea/b3a8b03aea3ecd0f9b5f17b595606a0a to your computer and use it in GitHub Desktop.
Why should I write this
import { parse } from "https://deno.land/x/ini@v2.1.0/mod.ts";
import { parseFlags } from "https://deno.land/x/cliffy@v1.0.0-rc.3/flags/mod.ts";
type Config = { profile?: string; serial?: string };
let fileConfig: undefined | Config;
if (import.meta.main) {
await main();
}
async function main() {
const { flags, literal } = parseFlags(Deno.args, {
flags: [
{ name: "profile", aliases: ["p"], type: "string" },
{ name: "serial", aliases: ["s"], type: "string" },
],
});
const shell = getShell();
const profile = (flags.profile as string | undefined) ??
(await getProfileFromConfigFile()) ??
"default";
const serial = (flags.serial as string | undefined) ??
await getSerialFromConfigFile();
const otp = literal?.[0] ?? promptOtp();
if (!serial) {
console.log(
`Please set mfa serial string in ~/.aws.config like the following.
[easy-mfa]
profile = default
serial = arn:aws:iam::123456789012:mfa/username`,
);
Deno.exit(1);
}
await spawnShellWithEnvironment(profile, serial, otp, shell);
}
async function getProfileFromConfigFile() {
try {
const { profile } = await getFileConfig();
return profile;
} catch (error) {
console.error(`Cannot read serial in ~/.aws/config: ${error}`);
Deno.exit(2);
}
}
function promptOtp() {
for (let trial = 0; trial < 3; trial++) {
const otp = prompt("Input OTP:")?.trim() ?? "";
if (otp.length !== 6) {
console.log("OTP length should be 6, but it was not. Please retype.");
continue;
}
return otp;
}
console.error("OTP input failure. exit.");
Deno.exit(3);
}
async function spawnShellWithEnvironment(
profile: string,
serial: string,
otp: string,
shell: string,
) {
const args = [
...["sts", "get-session-token"],
...["--profile", profile],
...["--serial-number", serial],
...["--token-code", otp],
...["--duration", "129600"],
];
const command = new Deno.Command("aws", {
args,
});
const result = await command.output();
const decoder = new TextDecoder();
const maybeJson = decoder.decode(result.stdout);
if (!result.success) {
console.log(`> aws ${args.join(" ")}`);
console.log(maybeJson);
console.error(decoder.decode(result.stderr));
Deno.exit(4);
}
const { Credentials: { AccessKeyId, SecretAccessKey, SessionToken } } = JSON
.parse(maybeJson) as {
Credentials: {
AccessKeyId: string;
SecretAccessKey: string;
SessionToken: string;
Expiration: string;
};
};
Deno.env.set("AWS_ACCESS_KEY_ID", AccessKeyId);
Deno.env.set("AWS_SECRET_ACCESS_KEY", SecretAccessKey);
Deno.env.set("AWS_SESSION_TOKEN", SessionToken);
const subShell = new Deno.Command(shell);
subShell.spawn();
}
function getShell() {
const shell = Deno.env.get("SHELL");
if (!shell) {
console.warn("Cannot read $SHELL, it'll use /bin/bash");
return "/bin/bash";
}
return shell;
}
async function getSerialFromConfigFile(): Promise<string | undefined> {
try {
const { serial } = await getFileConfig();
return serial;
} catch (error) {
console.error(`Cannot read serial in ~/.aws/config: ${error}`);
Deno.exit(2);
}
}
async function getFileConfig(): Promise<Config> {
if (fileConfig) {
return fileConfig;
}
try {
return (fileConfig = await readConfigFile());
} catch {
return (fileConfig = {});
}
}
async function readConfigFile() {
const home = Deno.env.get("HOME");
const ini = await Deno.readTextFile(`${home}/.aws/config`);
return parse(ini)?.["easy-mfa"] ?? {};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment