az2aws 1.6.2 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.gitattributes +1 -0
- package/CHANGELOG.md +37 -0
- package/CONTRIBUTING.md +20 -0
- package/README.md +86 -11
- package/lib/awsConfig.js +135 -10
- package/lib/configureProfileAsync.js +10 -8
- package/lib/index.js +16 -3
- package/lib/login.js +170 -104
- package/lib/loginStates.js +64 -81
- package/lib/sensitiveOutput.js +85 -0
- package/lib/sessionDuration.js +27 -0
- package/lib/updateNotifier.js +25 -11
- package/lib/validateCliOptions.js +12 -0
- package/package.json +5 -2
- package/pnpm-workspace.yaml +5 -0
package/.gitattributes
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
* text=auto eol=lf
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.8.0](https://github.com/kuma0128/az2aws/compare/v1.7.0...v1.8.0) (2026-04-27)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* show saved credential usage details ([#206](https://github.com/kuma0128/az2aws/issues/206)) ([04855cf](https://github.com/kuma0128/az2aws/commit/04855cf18e42f84ec2d639c825b0b6233730c3a0))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **login:** retry managed profile reset on Windows lock errors ([#199](https://github.com/kuma0128/az2aws/issues/199)) ([a4ad1c7](https://github.com/kuma0128/az2aws/commit/a4ad1c76d0f193cee16f17319e47449c6b5cba44))
|
|
14
|
+
* **login:** skip non-az2aws profiles in --all-profiles ([#196](https://github.com/kuma0128/az2aws/issues/196)) ([0dc6820](https://github.com/kuma0128/az2aws/commit/0dc6820775ebf03637dda78a899e0e2c16c25561))
|
|
15
|
+
* outdated references in README and docker-launch.sh ([#192](https://github.com/kuma0128/az2aws/issues/192)) ([79421ae](https://github.com/kuma0128/az2aws/commit/79421ae4c6c734cb81b37fde6a457e2b9c0752e8))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Performance Improvements
|
|
19
|
+
|
|
20
|
+
* switch CI to Corepack and drop manual pnpm store cache ([a27b64c](https://github.com/kuma0128/az2aws/commit/a27b64c4183382a10618a6dfcae0ccf803261894))
|
|
21
|
+
|
|
22
|
+
## [1.7.0](https://github.com/kuma0128/az2aws/compare/v1.6.2...v1.7.0) (2026-04-13)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
### Features
|
|
26
|
+
|
|
27
|
+
* add credential_process output mode ([#95](https://github.com/kuma0128/az2aws/issues/95)) ([2a3e66f](https://github.com/kuma0128/az2aws/commit/2a3e66fcd19fd461dd2f84fc32caa383fb56990c))
|
|
28
|
+
* add macOS CI coverage and separate E2E job ([#183](https://github.com/kuma0128/az2aws/issues/183)) ([cf617f8](https://github.com/kuma0128/az2aws/commit/cf617f899b5d2ba88650f7ffd26c3195dc32c695))
|
|
29
|
+
* Use Windows app-data conventions for update notifier cache ([#180](https://github.com/kuma0128/az2aws/issues/180)) ([8383ae4](https://github.com/kuma0128/az2aws/commit/8383ae46a468630ce531cc9df0dd7d64f115e49c))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
### Bug Fixes
|
|
33
|
+
|
|
34
|
+
* --no-prompt on account selection screen ([#173](https://github.com/kuma0128/az2aws/issues/173)) ([07c833f](https://github.com/kuma0128/az2aws/commit/07c833ff8a4553b8c32ed7841b3544776c41f907))
|
|
35
|
+
* clear password input before typing to prevent appending ([#171](https://github.com/kuma0128/az2aws/issues/171)) ([dfb0d65](https://github.com/kuma0128/az2aws/commit/dfb0d65c85fdb3a38bbc2d6ccdbd492f18e97c25))
|
|
36
|
+
* enforce whole number validation for session duration hours ([#167](https://github.com/kuma0128/az2aws/issues/167)) ([0bc7fb4](https://github.com/kuma0128/az2aws/commit/0bc7fb4f187dad422f08a42ece57a4b04138c51d))
|
|
37
|
+
* PATH handling when installing Chrome in CI workflow ([#184](https://github.com/kuma0128/az2aws/issues/184)) ([f6fd008](https://github.com/kuma0128/az2aws/commit/f6fd008a65f7c722b78ae65121e1e20f421dc232))
|
|
38
|
+
* support custom AWS config parent directories ([#172](https://github.com/kuma0128/az2aws/issues/172)) ([19753dd](https://github.com/kuma0128/az2aws/commit/19753dd769f31a72b4e0bdb84acf9638c1cd67f7))
|
|
39
|
+
|
|
3
40
|
## [1.6.2](https://github.com/kuma0128/az2aws/compare/v1.6.1...v1.6.2) (2026-04-10)
|
|
4
41
|
|
|
5
42
|
|
package/CONTRIBUTING.md
CHANGED
|
@@ -90,11 +90,31 @@ pnpm build && node ./lib/index.js
|
|
|
90
90
|
| `pnpm build` | Build for production |
|
|
91
91
|
| `pnpm test` | Run unit tests |
|
|
92
92
|
| `pnpm test:coverage` | Run unit tests with coverage |
|
|
93
|
+
| `pnpm test:e2e` | Run the live Azure→AWS browser smoke test |
|
|
93
94
|
| `pnpm lint` | Run ESLint and formatting checks |
|
|
94
95
|
| `pnpm eslint` | Run ESLint |
|
|
95
96
|
| `pnpm prettier:check` | Check code formatting |
|
|
96
97
|
| `pnpm prettier:write` | Auto-fix code formatting |
|
|
97
98
|
|
|
99
|
+
### E2E Smoke Test
|
|
100
|
+
|
|
101
|
+
`pnpm test:e2e` launches a real Puppeteer/Chrome session, signs in through
|
|
102
|
+
Azure AD, and verifies that az2aws can retrieve AWS credentials via
|
|
103
|
+
`credential_process` without persisting them to the shared credentials file.
|
|
104
|
+
|
|
105
|
+
Copy `.env.example` to `.env` and fill in the values. See
|
|
106
|
+
[`vitest.e2e.config.ts`](vitest.e2e.config.ts) for the Vitest settings used by
|
|
107
|
+
this suite.
|
|
108
|
+
|
|
109
|
+
To troubleshoot a failing run, rerun with `AZ2AWS_E2E_MODE=debug` (visible
|
|
110
|
+
browser, auto-fill) or `AZ2AWS_E2E_MODE=gui` (fully manual).
|
|
111
|
+
|
|
112
|
+
> **Note:** Passkey-first Microsoft accounts are not supported — use a dedicated
|
|
113
|
+
> account with standard username/password/MFA.
|
|
114
|
+
|
|
115
|
+
CI only runs this test on `push` to `main` and `workflow_dispatch`; PR
|
|
116
|
+
validation does not depend on repository secrets.
|
|
117
|
+
|
|
98
118
|
## Development Workflow
|
|
99
119
|
|
|
100
120
|
1. Create a new branch from `main`:
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
# az2aws
|
|
7
7
|
|
|
8
|
-
Log in to AWS CLI using [
|
|
8
|
+
Log in to AWS CLI using [Microsoft Entra ID](https://learn.microsoft.com/en-us/entra/identity/) SSO. Supports MFA and places temporary credentials in the proper location for AWS CLI and SDKs.
|
|
9
9
|
|
|
10
10
|
> **💡 Tip:** Let's be honest — typing `az2aws` correctly on the first try is harder than the AWS certification exam. Save your sanity:
|
|
11
11
|
>
|
|
@@ -18,6 +18,25 @@ Log in to AWS CLI using [Azure Active Directory](https://azure.microsoft.com) SS
|
|
|
18
18
|
>
|
|
19
19
|
> Your fingers will thank you. Your keyboard will thank you. Your coworkers will stop hearing you swear.
|
|
20
20
|
|
|
21
|
+
## Contents
|
|
22
|
+
|
|
23
|
+
- [Installation](#installation)
|
|
24
|
+
- [mise (Recommended)](#mise-recommended)
|
|
25
|
+
- [npm](#npm)
|
|
26
|
+
- [Docker](#docker)
|
|
27
|
+
- [Snap](#snap)
|
|
28
|
+
- [Command Options](#command-options)
|
|
29
|
+
- [Usage](#usage)
|
|
30
|
+
- [Configuration](#configuration)
|
|
31
|
+
- [Logging In](#logging-in)
|
|
32
|
+
- [Automation](#automation)
|
|
33
|
+
- [Which profiles `--all-profiles` refreshes](#which-profiles---all-profiles-refreshes)
|
|
34
|
+
- [Getting Your Tenant ID and App ID URI](#getting-your-tenant-id-and-app-id-uri)
|
|
35
|
+
- [How It Works](#how-it-works)
|
|
36
|
+
- [Troubleshooting](#troubleshooting)
|
|
37
|
+
- [Support for Other Authentication Providers](#support-for-other-authentication-providers)
|
|
38
|
+
- [Acknowledgements](#acknowledgements)
|
|
39
|
+
|
|
21
40
|
## Installation
|
|
22
41
|
|
|
23
42
|
### mise (Recommended)
|
|
@@ -50,11 +69,11 @@ Install [Node.js](https://nodejs.org/) v24 or higher, then install az2aws:
|
|
|
50
69
|
|
|
51
70
|
#### Linux Notes
|
|
52
71
|
|
|
53
|
-
You must install [puppeteer dependencies](https://github.com/
|
|
72
|
+
You must install [puppeteer dependencies](https://github.com/puppeteer/puppeteer/blob/main/docs/troubleshooting.md) first.
|
|
54
73
|
|
|
55
74
|
**Install for all users:**
|
|
56
75
|
|
|
57
|
-
sudo npm install -g az2aws
|
|
76
|
+
sudo npm install -g az2aws
|
|
58
77
|
sudo chmod -R go+rx $(npm root -g)
|
|
59
78
|
|
|
60
79
|
**Install for current user only:**
|
|
@@ -67,15 +86,19 @@ You must install [puppeteer dependencies](https://github.com/GoogleChrome/puppet
|
|
|
67
86
|
|
|
68
87
|
#### Windows Notes
|
|
69
88
|
|
|
70
|
-
If you get a missing Chrome/Chromium error,
|
|
89
|
+
If you get a missing Chrome/Chromium error, reinstall the Puppeteer browser from the installed `az2aws` package directory:
|
|
90
|
+
|
|
91
|
+
node <npm_global_node_modules>\az2aws\node_modules\puppeteer\install.mjs
|
|
71
92
|
|
|
72
|
-
|
|
93
|
+
For an npm global install, replace `<npm_global_node_modules>` with the output of `npm root -g`.
|
|
94
|
+
If you installed az2aws with pnpm or another package manager, locate `puppeteer/install.mjs`
|
|
95
|
+
under the installed `az2aws` package directory and run it with `node`.
|
|
73
96
|
|
|
74
97
|
### Docker
|
|
75
98
|
|
|
76
99
|
Run az2aws with a volume mounted to your AWS configuration directory:
|
|
77
100
|
|
|
78
|
-
docker run --rm -it -v ~/.aws:/root/.aws
|
|
101
|
+
docker run --rm -it -v ~/.aws:/root/.aws taiseiito1000/az2aws
|
|
79
102
|
|
|
80
103
|
You can also install the docker-launch.sh script to your PATH:
|
|
81
104
|
|
|
@@ -108,10 +131,11 @@ https://snapcraft.io/az2aws
|
|
|
108
131
|
| `--no-prompt` | Skip prompts, use defaults |
|
|
109
132
|
| `--enable-chrome-network-service` | Enable Network Service (for 3XX redirects) |
|
|
110
133
|
| `--no-verify-ssl` | Disable AWS SSL verification |
|
|
111
|
-
| `--enable-chrome-seamless-sso` | Enable
|
|
134
|
+
| `--enable-chrome-seamless-sso` | Enable Microsoft Entra Seamless SSO |
|
|
112
135
|
| `--no-disable-extensions` | Keep browser extensions enabled |
|
|
113
136
|
| `--disable-gpu` | Disable GPU acceleration |
|
|
114
137
|
| `--incognito` | Open the login flow in an incognito browser context |
|
|
138
|
+
| `--credential-process` | Output credentials for AWS CLI credential_process |
|
|
115
139
|
| `--version (-v)` | Show version number |
|
|
116
140
|
|
|
117
141
|
## Usage
|
|
@@ -148,11 +172,34 @@ Enable "Stay logged in" during configuration to use `--no-prompt` without storin
|
|
|
148
172
|
helps avoid reusing an existing browser session, and it overrides any saved
|
|
149
173
|
"Stay logged in" browser state for that run.
|
|
150
174
|
|
|
175
|
+
#### AWS CLI credential_process
|
|
176
|
+
|
|
177
|
+
Configure the profile first so it has the defaults needed for non-interactive
|
|
178
|
+
login, then point AWS CLI at `az2aws`:
|
|
179
|
+
|
|
180
|
+
[profile myprofile]
|
|
181
|
+
credential_process = az2aws --profile myprofile --credential-process
|
|
182
|
+
|
|
183
|
+
`--credential-process` uses the same non-interactive defaults as `--no-prompt`,
|
|
184
|
+
so make sure the profile already has the role and other required values set.
|
|
185
|
+
Standard output is reserved for the AWS CLI JSON payload, while human-readable
|
|
186
|
+
status messages are written to stderr.
|
|
187
|
+
|
|
188
|
+
Example stdout payload:
|
|
189
|
+
|
|
190
|
+
{
|
|
191
|
+
"Version": 1,
|
|
192
|
+
"AccessKeyId": "...",
|
|
193
|
+
"SecretAccessKey": "...",
|
|
194
|
+
"SessionToken": "...",
|
|
195
|
+
"Expiration": "2026-01-01T00:00:00.000Z"
|
|
196
|
+
}
|
|
197
|
+
|
|
151
198
|
#### Environment Variables
|
|
152
199
|
|
|
153
200
|
You can set defaults via environment variables (use with `--no-prompt`):
|
|
154
201
|
|
|
155
|
-
- `AZURE_TENANT_ID` / `AZURE_APP_ID_URI` -
|
|
202
|
+
- `AZURE_TENANT_ID` / `AZURE_APP_ID_URI` - Microsoft Entra ID settings
|
|
156
203
|
- `AZURE_DEFAULT_USERNAME` / `AZURE_DEFAULT_PASSWORD` - Credentials
|
|
157
204
|
- `AZURE_DEFAULT_ROLE_ARN` / `AZURE_DEFAULT_DURATION_HOURS` - AWS role settings
|
|
158
205
|
|
|
@@ -192,10 +239,15 @@ Example:
|
|
|
192
239
|
az2aws # Default profile
|
|
193
240
|
az2aws --profile foo # Named profile
|
|
194
241
|
az2aws --mode gui # Use browser UI (more reliable)
|
|
242
|
+
az2aws --mode debug # Show the browser while az2aws still drives the flow
|
|
195
243
|
az2aws --mode gui --incognito # Open a fresh incognito login window
|
|
196
244
|
|
|
197
245
|
You'll be prompted for username, password, and MFA if required. After login, use AWS CLI/SDKs as usual.
|
|
198
246
|
|
|
247
|
+
`--mode gui` is fully manual and waits for you to complete the browser flow
|
|
248
|
+
yourself. If you want the browser to stay visible while az2aws still auto-fills
|
|
249
|
+
the login steps, use `--mode debug`.
|
|
250
|
+
|
|
199
251
|
**Tips:**
|
|
200
252
|
- Set `AWS_PROFILE` env var instead of using `--profile`
|
|
201
253
|
- Use `--mode gui --disable-gpu` on VMs or if rendering fails
|
|
@@ -217,6 +269,13 @@ If you see device compliance errors (e.g., "Device UnSecured Or Non-Compliant"),
|
|
|
217
269
|
Try:
|
|
218
270
|
`--mode gui` and use your system Chrome via `BROWSER_CHROME_BIN`.
|
|
219
271
|
|
|
272
|
+
If your Microsoft account requires a saved passkey prompt before the username
|
|
273
|
+
or password page appears, that flow is unsupported in `az2aws --mode cli`.
|
|
274
|
+
The prompt is rendered by the browser/OS passkey UI instead of the page DOM,
|
|
275
|
+
so az2aws cannot dismiss it automatically. Use `--mode gui` and handle it
|
|
276
|
+
manually, or use an account that can continue with the standard page-based
|
|
277
|
+
username/password/MFA flow.
|
|
278
|
+
|
|
220
279
|
If you see "Unable to recognize page state!", Azure's login pages may have
|
|
221
280
|
changed. Try:
|
|
222
281
|
|
|
@@ -232,9 +291,25 @@ Renew all profiles at once:
|
|
|
232
291
|
|
|
233
292
|
Credentials are only refreshed if expiring within 11 minutes - safe to run as a cron job.
|
|
234
293
|
|
|
294
|
+
### Which profiles `--all-profiles` refreshes
|
|
295
|
+
|
|
296
|
+
`--all-profiles` iterates every `[default]` / `[profile <name>]` section in
|
|
297
|
+
`~/.aws/config` that has **at least one `azure_*` key** (e.g.
|
|
298
|
+
`azure_tenant_id`, `azure_app_id_uri`, `azure_default_role_arn`). Sections
|
|
299
|
+
without any `azure_*` key — plain AWS profiles, `[sso-session ...]`,
|
|
300
|
+
`[services ...]` — are skipped.
|
|
301
|
+
|
|
302
|
+
Profiles that intentionally keep `azure_tenant_id` / `azure_app_id_uri` in
|
|
303
|
+
environment variables (`AZURE_TENANT_ID`, `AZURE_APP_ID_URI`) instead of
|
|
304
|
+
the config file are still refreshed, as long as they have some other
|
|
305
|
+
`azure_*` key on disk. If required values are missing even after the
|
|
306
|
+
env-var merge, az2aws fails loudly with
|
|
307
|
+
`Profile '<name>' is not configured properly.` rather than skipping
|
|
308
|
+
silently.
|
|
309
|
+
|
|
235
310
|
## Getting Your Tenant ID and App ID URI
|
|
236
311
|
|
|
237
|
-
Ask your
|
|
312
|
+
Ask your Microsoft Entra ID admin for these values, or extract them from myapps.microsoft.com:
|
|
238
313
|
|
|
239
314
|
1. Load the myapps.microsoft.com page.
|
|
240
315
|
2. Click the app tile for the login you want.
|
|
@@ -248,7 +323,7 @@ Ask your Azure AD admin for these values, or extract them from myapps.microsoft.
|
|
|
248
323
|
|
|
249
324
|
## How It Works
|
|
250
325
|
|
|
251
|
-
az2aws uses [Puppeteer](https://github.com/
|
|
326
|
+
az2aws uses [Puppeteer](https://github.com/puppeteer/puppeteer) to automate a Chromium browser for Microsoft Entra ID login. It parses the SAML response and calls [AWS STS AssumeRoleWithSAML](https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithSAML.html) to get temporary credentials.
|
|
252
327
|
|
|
253
328
|
## Troubleshooting
|
|
254
329
|
|
|
@@ -260,7 +335,7 @@ If login fails, try these in order:
|
|
|
260
335
|
|
|
261
336
|
## Support for Other Authentication Providers
|
|
262
337
|
|
|
263
|
-
This tool only supports
|
|
338
|
+
This tool only supports Microsoft Entra ID. Contributions for other SAML providers are welcome - open an issue on GitHub to discuss.
|
|
264
339
|
|
|
265
340
|
## Acknowledgements
|
|
266
341
|
|
package/lib/awsConfig.js
CHANGED
|
@@ -9,9 +9,79 @@ const debug_1 = __importDefault(require("debug"));
|
|
|
9
9
|
const paths_1 = require("./paths");
|
|
10
10
|
const promises_1 = require("node:fs/promises");
|
|
11
11
|
const fs_1 = __importDefault(require("fs"));
|
|
12
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
13
|
+
const path_1 = __importDefault(require("path"));
|
|
12
14
|
const util_1 = __importDefault(require("util"));
|
|
13
15
|
const debug = (0, debug_1.default)("az2aws");
|
|
14
16
|
const writeFile = util_1.default.promisify(fs_1.default.writeFile);
|
|
17
|
+
const awsDirMode = 0o700;
|
|
18
|
+
const awsFileMode = 0o600;
|
|
19
|
+
const ignoredChmodErrorCodes = new Set([
|
|
20
|
+
"EACCES",
|
|
21
|
+
"EINVAL",
|
|
22
|
+
"ENOSYS",
|
|
23
|
+
"ENOTSUP",
|
|
24
|
+
"EPERM",
|
|
25
|
+
"EROFS",
|
|
26
|
+
]);
|
|
27
|
+
async function hardenPathPermissions(fsPath, mode) {
|
|
28
|
+
if (process.platform === "win32") {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
await (0, promises_1.chmod)(fsPath, mode);
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
const code = error.code;
|
|
36
|
+
if (typeof code === "string" && ignoredChmodErrorCodes.has(code)) {
|
|
37
|
+
debug(`Skipping permission hardening due to ${code}`);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async function hardenCreatedDirectories(createdDir, targetDir) {
|
|
44
|
+
const resolvedCreatedDir = path_1.default.resolve(createdDir);
|
|
45
|
+
const resolvedTargetDir = path_1.default.resolve(targetDir);
|
|
46
|
+
const relativeTargetDir = path_1.default.relative(resolvedCreatedDir, resolvedTargetDir);
|
|
47
|
+
if (relativeTargetDir === ".." ||
|
|
48
|
+
relativeTargetDir.startsWith(`..${path_1.default.sep}`) ||
|
|
49
|
+
path_1.default.isAbsolute(relativeTargetDir)) {
|
|
50
|
+
debug("Skipping permission hardening because the target directory is not within the created directory.");
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
let currentDir = resolvedCreatedDir;
|
|
54
|
+
await hardenPathPermissions(currentDir, awsDirMode);
|
|
55
|
+
if (!relativeTargetDir) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
for (const segment of relativeTargetDir.split(path_1.default.sep)) {
|
|
59
|
+
if (!segment || segment === ".") {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
currentDir = path_1.default.join(currentDir, segment);
|
|
63
|
+
await hardenPathPermissions(currentDir, awsDirMode);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async function atomicWriteTextFile(targetPath, text) {
|
|
67
|
+
const targetDir = path_1.default.dirname(targetPath);
|
|
68
|
+
const tempPath = targetDir === "."
|
|
69
|
+
? `.${path_1.default.basename(targetPath)}.${node_crypto_1.default.randomUUID()}.tmp`
|
|
70
|
+
: path_1.default.join(targetDir, `.${path_1.default.basename(targetPath)}.${node_crypto_1.default.randomUUID()}.tmp`);
|
|
71
|
+
let shouldCleanupTempPath = false;
|
|
72
|
+
try {
|
|
73
|
+
await writeFile(tempPath, text);
|
|
74
|
+
shouldCleanupTempPath = true;
|
|
75
|
+
await hardenPathPermissions(tempPath, awsFileMode);
|
|
76
|
+
await (0, promises_1.rename)(tempPath, targetPath);
|
|
77
|
+
shouldCleanupTempPath = false;
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
if (shouldCleanupTempPath) {
|
|
81
|
+
await (0, promises_1.rm)(tempPath, { force: true }).catch(() => undefined);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
15
85
|
// Autorefresh credential time limit in milliseconds
|
|
16
86
|
const refreshLimitInMs = 11 * 60 * 1000;
|
|
17
87
|
exports.awsConfig = {
|
|
@@ -70,12 +140,45 @@ exports.awsConfig = {
|
|
|
70
140
|
debug(`Received profiles: ${profiles.toString()}`);
|
|
71
141
|
return profiles;
|
|
72
142
|
},
|
|
143
|
+
async getAz2awsProfileNames() {
|
|
144
|
+
debug(`Getting az2aws-configured profiles from config.`);
|
|
145
|
+
const config = (await this._loadAsync("config")) || {};
|
|
146
|
+
const profiles = [];
|
|
147
|
+
for (const [sectionName, sectionConfig] of Object.entries(config)) {
|
|
148
|
+
let profileName;
|
|
149
|
+
if (sectionName === "default") {
|
|
150
|
+
profileName = "default";
|
|
151
|
+
}
|
|
152
|
+
else if (sectionName.startsWith("profile ")) {
|
|
153
|
+
profileName = sectionName.substring("profile ".length);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
debug(`Skipping non-profile section '${sectionName}'`);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
// Treat a profile as az2aws-managed if it has at least one azure_* key.
|
|
160
|
+
// Required values (azure_tenant_id / azure_app_id_uri) may still be
|
|
161
|
+
// supplied by environment variables at runtime, so don't hard-require
|
|
162
|
+
// them in the config file.
|
|
163
|
+
const hasAzureKey = sectionConfig &&
|
|
164
|
+
typeof sectionConfig === "object" &&
|
|
165
|
+
Object.keys(sectionConfig).some((key) => key.startsWith("azure_"));
|
|
166
|
+
if (!hasAzureKey) {
|
|
167
|
+
debug(`Skipping profile '${profileName}' because it has no az2aws (azure_*) keys`);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
profiles.push(profileName);
|
|
171
|
+
}
|
|
172
|
+
debug(`Received az2aws profiles: ${profiles.toString()}`);
|
|
173
|
+
return profiles;
|
|
174
|
+
},
|
|
73
175
|
async _loadAsync(type) {
|
|
74
|
-
|
|
176
|
+
const targetPath = paths_1.paths[type];
|
|
177
|
+
if (!targetPath)
|
|
75
178
|
throw new Error(`Unknown config type: '${type}'`);
|
|
76
179
|
return new Promise((resolve, reject) => {
|
|
77
|
-
debug(`Loading '${type}' file
|
|
78
|
-
fs_1.default.readFile(
|
|
180
|
+
debug(`Loading '${type}' file`);
|
|
181
|
+
fs_1.default.readFile(targetPath, "utf8", (err, data) => {
|
|
79
182
|
if (err) {
|
|
80
183
|
if (err.code === "ENOENT") {
|
|
81
184
|
debug(`File not found. Returning undefined.`);
|
|
@@ -86,23 +189,45 @@ exports.awsConfig = {
|
|
|
86
189
|
}
|
|
87
190
|
}
|
|
88
191
|
debug("Parsing data");
|
|
89
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
90
192
|
const parsedIni = ini_1.default.parse(data);
|
|
91
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
92
193
|
return resolve(parsedIni);
|
|
93
194
|
});
|
|
94
195
|
});
|
|
95
196
|
},
|
|
96
197
|
async _saveAsync(type, data) {
|
|
97
|
-
|
|
198
|
+
const targetPath = paths_1.paths[type];
|
|
199
|
+
if (!targetPath)
|
|
98
200
|
throw new Error(`Unknown config type: '${type}'`);
|
|
99
201
|
if (!data)
|
|
100
202
|
throw new Error(`You must provide data for saving.`);
|
|
101
203
|
debug(`Stringifying ${type} INI data`);
|
|
102
204
|
const text = ini_1.default.stringify(data);
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
205
|
+
const targetDir = path_1.default.dirname(targetPath);
|
|
206
|
+
const isDefaultAwsDir = path_1.default.resolve(targetDir) === path_1.default.resolve(paths_1.paths.awsDir);
|
|
207
|
+
if (targetDir !== ".") {
|
|
208
|
+
debug(`Creating target directory for '${type}' if it does not exist.`);
|
|
209
|
+
const createdDir = await (0, promises_1.mkdir)(targetDir, {
|
|
210
|
+
recursive: true,
|
|
211
|
+
mode: awsDirMode,
|
|
212
|
+
});
|
|
213
|
+
if (isDefaultAwsDir) {
|
|
214
|
+
await hardenPathPermissions(targetDir, awsDirMode);
|
|
215
|
+
}
|
|
216
|
+
else if (createdDir) {
|
|
217
|
+
await hardenCreatedDirectories(createdDir, targetDir);
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
debug("Skipping directory permission hardening for existing custom directory.");
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
debug(`Skipping target directory creation for '${type}' because it uses the current working directory.`);
|
|
225
|
+
}
|
|
226
|
+
debug(`Writing '${type}' INI to file atomically`);
|
|
227
|
+
await atomicWriteTextFile(targetPath, text);
|
|
228
|
+
// Defensive: atomicWriteTextFile already sets permissions on the temp file
|
|
229
|
+
// before rename, but we re-apply here in case rename semantics differ across
|
|
230
|
+
// platforms or file-systems.
|
|
231
|
+
await hardenPathPermissions(targetPath, awsFileMode);
|
|
107
232
|
},
|
|
108
233
|
};
|
|
@@ -5,8 +5,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.configureProfileAsync = configureProfileAsync;
|
|
7
7
|
const inquirer_1 = __importDefault(require("inquirer"));
|
|
8
|
+
const CLIError_1 = require("./CLIError");
|
|
8
9
|
const awsConfig_1 = require("./awsConfig");
|
|
10
|
+
const sessionDuration_1 = require("./sessionDuration");
|
|
9
11
|
async function configureProfileAsync(profileName) {
|
|
12
|
+
var _a;
|
|
10
13
|
console.log(`Configuring profile '${profileName}'`);
|
|
11
14
|
const profile = await awsConfig_1.awsConfig.getProfileConfigAsync(profileName);
|
|
12
15
|
const questions = [
|
|
@@ -54,13 +57,8 @@ async function configureProfileAsync(profileName) {
|
|
|
54
57
|
type: "input",
|
|
55
58
|
name: "defaultDurationHours",
|
|
56
59
|
message: "Default Session Duration Hours (up to 12):",
|
|
57
|
-
default: (profile
|
|
58
|
-
validate:
|
|
59
|
-
const num = Number(input);
|
|
60
|
-
if (num > 0 && num <= 12)
|
|
61
|
-
return true;
|
|
62
|
-
return "Duration hours must be between 1 and 12";
|
|
63
|
-
},
|
|
60
|
+
default: (_a = (0, sessionDuration_1.parseSessionDurationHours)(profile === null || profile === void 0 ? void 0 : profile.azure_default_duration_hours)) !== null && _a !== void 0 ? _a : 1,
|
|
61
|
+
validate: sessionDuration_1.validateSessionDurationHours,
|
|
64
62
|
},
|
|
65
63
|
{
|
|
66
64
|
type: "input",
|
|
@@ -70,12 +68,16 @@ async function configureProfileAsync(profileName) {
|
|
|
70
68
|
},
|
|
71
69
|
];
|
|
72
70
|
const answers = await inquirer_1.default.prompt(questions);
|
|
71
|
+
const defaultDurationHours = (0, sessionDuration_1.parseSessionDurationHours)(answers.defaultDurationHours);
|
|
72
|
+
if (defaultDurationHours === null) {
|
|
73
|
+
throw new CLIError_1.CLIError(sessionDuration_1.sessionDurationHoursValidationMessage);
|
|
74
|
+
}
|
|
73
75
|
await awsConfig_1.awsConfig.setProfileConfigValuesAsync(profileName, {
|
|
74
76
|
azure_tenant_id: answers.tenantId,
|
|
75
77
|
azure_app_id_uri: answers.appIdUri,
|
|
76
78
|
azure_default_username: answers.username,
|
|
77
79
|
azure_default_role_arn: answers.defaultRoleArn,
|
|
78
|
-
azure_default_duration_hours:
|
|
80
|
+
azure_default_duration_hours: String(defaultDurationHours),
|
|
79
81
|
azure_default_remember_me: answers.rememberMe === "true",
|
|
80
82
|
region: answers.region,
|
|
81
83
|
});
|
package/lib/index.js
CHANGED
|
@@ -6,7 +6,9 @@ process.on("SIGTERM", () => process.exit(1));
|
|
|
6
6
|
const commander_1 = require("commander");
|
|
7
7
|
const configureProfileAsync_1 = require("./configureProfileAsync");
|
|
8
8
|
const login_1 = require("./login");
|
|
9
|
+
const sensitiveOutput_1 = require("./sensitiveOutput");
|
|
9
10
|
const updateNotifier_1 = require("./updateNotifier");
|
|
11
|
+
const validateCliOptions_1 = require("./validateCliOptions");
|
|
10
12
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
11
13
|
const { version } = require("../package.json");
|
|
12
14
|
const program = new commander_1.Command();
|
|
@@ -24,6 +26,7 @@ program
|
|
|
24
26
|
.option("--enable-chrome-seamless-sso", "Enable Chromium's pass-through authentication with Azure Active Directory Seamless Single Sign-On")
|
|
25
27
|
.option("--no-disable-extensions", "Tell Puppeteer not to pass the --disable-extensions flag to Chromium")
|
|
26
28
|
.option("--disable-gpu", "Tell Puppeteer to pass the --disable-gpu flag to Chromium")
|
|
29
|
+
.option("--credential-process", "Output credentials in JSON format for AWS CLI credential_process")
|
|
27
30
|
.option("--incognito", "Launch Chromium in incognito mode")
|
|
28
31
|
.parse(process.argv);
|
|
29
32
|
const options = program.opts();
|
|
@@ -39,6 +42,7 @@ const enableChromeSeamlessSso = !!options.enableChromeSeamlessSso;
|
|
|
39
42
|
const forceRefresh = !!options.forceRefresh;
|
|
40
43
|
const noDisableExtensions = !options.disableExtensions;
|
|
41
44
|
const disableGpu = !!options.disableGpu;
|
|
45
|
+
const credentialProcess = !!options.credentialProcess;
|
|
42
46
|
const incognito = !!options.incognito;
|
|
43
47
|
// Start the update lookup immediately, but only print after the main flow ends.
|
|
44
48
|
const updateCheckPromise = (0, updateNotifier_1.checkForUpdate)(version, {
|
|
@@ -47,6 +51,11 @@ const updateCheckPromise = (0, updateNotifier_1.checkForUpdate)(version, {
|
|
|
47
51
|
async function runAsync() {
|
|
48
52
|
let exitCode = 0;
|
|
49
53
|
try {
|
|
54
|
+
(0, validateCliOptions_1.validateCliOptions)({
|
|
55
|
+
allProfiles: !!options.allProfiles,
|
|
56
|
+
configure: !!options.configure,
|
|
57
|
+
credentialProcess,
|
|
58
|
+
});
|
|
50
59
|
if (options.allProfiles) {
|
|
51
60
|
await login_1.login.loginAll(mode, disableSandbox, noPrompt, enableChromeNetworkService, awsNoVerifySsl, enableChromeSeamlessSso, forceRefresh, noDisableExtensions, disableGpu, incognito);
|
|
52
61
|
}
|
|
@@ -54,16 +63,20 @@ async function runAsync() {
|
|
|
54
63
|
await (0, configureProfileAsync_1.configureProfileAsync)(profileName);
|
|
55
64
|
}
|
|
56
65
|
else {
|
|
57
|
-
await login_1.login.loginAsync(profileName, mode, disableSandbox, noPrompt, enableChromeNetworkService, awsNoVerifySsl, enableChromeSeamlessSso, noDisableExtensions, disableGpu, incognito);
|
|
66
|
+
await login_1.login.loginAsync(profileName, mode, disableSandbox, noPrompt, enableChromeNetworkService, awsNoVerifySsl, enableChromeSeamlessSso, noDisableExtensions, disableGpu, incognito, credentialProcess);
|
|
58
67
|
}
|
|
59
68
|
}
|
|
60
69
|
catch (err) {
|
|
61
70
|
if (err instanceof Error && err.name === "CLIError") {
|
|
62
|
-
console.error(err.message);
|
|
71
|
+
console.error((0, sensitiveOutput_1.formatCliErrorMessage)(err.message));
|
|
63
72
|
exitCode = 2;
|
|
64
73
|
}
|
|
74
|
+
else if (err instanceof Error) {
|
|
75
|
+
console.error((0, sensitiveOutput_1.formatUnexpectedErrorMessage)(err));
|
|
76
|
+
exitCode = 1;
|
|
77
|
+
}
|
|
65
78
|
else {
|
|
66
|
-
console.error(err);
|
|
79
|
+
console.error((0, sensitiveOutput_1.formatUnexpectedErrorMessage)(err));
|
|
67
80
|
exitCode = 1;
|
|
68
81
|
}
|
|
69
82
|
}
|