aws-asmr 0.0.4 → 0.0.7
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.
- checksums.yaml +4 -4
- data/README.md +73 -3
- data/bin/asmr +20 -82
- data/bin/asmr-login +13 -0
- data/lib/aws/asmr/alias.rb +8 -2
- data/lib/aws/asmr/cache.rb +11 -5
- data/lib/aws/asmr/cli.rb +119 -0
- data/lib/aws/asmr/local.rb +79 -0
- data/lib/aws/asmr/options.rb +15 -21
- data/lib/aws/asmr/version.rb +1 -1
- data/lib/aws/asmr/web_login.rb +99 -0
- data/lib/aws/asmr.rb +2 -0
- metadata +7 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5f82db6a4f331d524f5c56dab14452abe7e6b727e4870990fec14410eb728cdd
|
|
4
|
+
data.tar.gz: 62c1380dbe38966db335122e19e285fb1b3c6d0b3ba960c1e9d3023312d689ba
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5ce7c9d028f0e67023005ff923e663d1fa79ea32229656ace998ce56736251264644ca87e1f61615534097c633927b729a7fbaa2444e2652caaa285651ff8276
|
|
7
|
+
data.tar.gz: 0dd4c41871053793bece7cd69f82e8776f342137f2f927a07272023a39fd9e6badb13dfee37c2f308fc035ffbc5781b8972b1b1abb8efa2b175ef16272f6fc37
|
data/README.md
CHANGED
|
@@ -33,12 +33,14 @@ Of course you can set `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` respectively
|
|
|
33
33
|
AWS_ACCESS_KEY_ID=xxxx AWS_SECRET_ACCESS_KEY=yyyy asmr --name=arn:aws:iam::0000:role/AwesomeRole aws sts get-caller-identity
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
To specify ARN (or alias name of assumed role),
|
|
37
|
-
This is due to a development circumstance. This tool is supposed to run 2 commands. One is assume_role, and the other is subsequential(this is main though) command. To safely separate options for assume_role and subsequential commands, all components of the `asmr` args must be start with `-`. Curse my programming ability!
|
|
36
|
+
To specify ARN (or alias name of assumed role), set the `name` option in any of the usual forms. Option parsing stops at the first argument that is not an option (or at a literal `--`), and everything from there on is the subsequential command, including its own options such as `--filter`.
|
|
38
37
|
|
|
39
38
|
```
|
|
40
39
|
asmr --name=arn:aws:iam::0000:role/AwesomeRole
|
|
40
|
+
asmr --name arn:aws:iam::0000:role/AwesomeRole
|
|
41
41
|
asmr -narn:aws:iam::0000:role/AwesomeRole
|
|
42
|
+
asmr -n arn:aws:iam::0000:role/AwesomeRole
|
|
43
|
+
asmr -n arn:aws:iam::0000:role/AwesomeRole -- aws sts get-caller-identity
|
|
42
44
|
```
|
|
43
45
|
|
|
44
46
|
Of course you can set options for subsequential command.
|
|
@@ -47,10 +49,13 @@ Of course you can set options for subsequential command.
|
|
|
47
49
|
asmr --name=arn:aws:iam::0000:role/AwesomeRole aws ec2 describe-instances --filter '[{"Name":"instance-state-name","Values":["stopped"]}]'
|
|
48
50
|
```
|
|
49
51
|
|
|
50
|
-
|
|
52
|
+
When the subsequential command is given as several arguments, it is executed directly (no shell in between): each argument reaches the command exactly as your shell handed it to `asmr`, so quotes, spaces and `$` in arguments need no extra escaping. Note that your shell still interprets `|`, `&&` and redirects *before* `asmr` runs, so in `asmr aws s3 ls | grep foo` only `aws s3 ls` gets the credentials (which is usually what you want).
|
|
53
|
+
|
|
54
|
+
To run a whole pipeline as the assumed role, pass it as a single argument; a single argument is run through `sh`, so pipes, redirects and variable expansion work inside it.
|
|
51
55
|
|
|
52
56
|
```
|
|
53
57
|
asmr --name=arn:aws:iam::0000:role/AwesomeRole "aws sts get-caller-identity | grep Arn"
|
|
58
|
+
asmr --name=arn:aws:iam::0000:role/AwesomeRole 'echo $AWS_ACCESS_KEY_ID'
|
|
54
59
|
```
|
|
55
60
|
|
|
56
61
|
Without subsequential command, it just prints environment variables for assume_role.
|
|
@@ -69,6 +74,7 @@ Here is the example of alias file. `arn` is the only required attribute.
|
|
|
69
74
|
[awesome-app-staging]
|
|
70
75
|
arn = arn:aws:iam::0001:role/AwesomeRole
|
|
71
76
|
profile = custodian
|
|
77
|
+
region = ap-northeast-1
|
|
72
78
|
|
|
73
79
|
[awesome-app-production]
|
|
74
80
|
arn = arn:aws:iam::0002:role/AwesomeRole
|
|
@@ -79,6 +85,8 @@ secret_access_key = yyyy
|
|
|
79
85
|
# arn = test
|
|
80
86
|
```
|
|
81
87
|
|
|
88
|
+
`region` is optional and is only used by `asmr-login` (see below) to pick the console landing region.
|
|
89
|
+
|
|
82
90
|
Then, you can choose one of the alias.
|
|
83
91
|
|
|
84
92
|
```
|
|
@@ -91,3 +99,65 @@ Or you can specify alias name.
|
|
|
91
99
|
```
|
|
92
100
|
asmr --name=awesome-app-staging aws sts get-caller-identity
|
|
93
101
|
```
|
|
102
|
+
|
|
103
|
+
## Pin a role to a directory (`asmr local`)
|
|
104
|
+
|
|
105
|
+
Following the `rbenv local` convention, you can pin an alias (or a role ARN) to a directory. `asmr local NAME` writes the name to `.asmr` in the current directory; from then on, `asmr` and `asmr-login` run there **without `--name`** assume that role instead of asking you to choose one. The nearest `.asmr` in the current directory or any of its parents wins, so pinning a project root covers its subdirectories.
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
cd ~/src/awesome-app
|
|
109
|
+
asmr local awesome-app-staging # writes ./.asmr
|
|
110
|
+
asmr aws sts get-caller-identity # assumes awesome-app-staging, no prompt
|
|
111
|
+
asmr-login # opens the console as awesome-app-staging
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`asmr local` refuses a name that is neither an alias defined in `~/.aws-asmr/alias` nor a role ARN, the same way rbenv refuses a version that is not installed. A pin whose alias was deleted later fails with a message pointing at the `.asmr` file.
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
asmr local # prints the name pinned in the current directory
|
|
118
|
+
asmr local --unset # removes ./.asmr
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
`--name` always takes precedence over the pin. Note that pinning is by directory, not by shell: entering a pinned directory silently switches the role, so be careful with production aliases (checking `asmr local` before a destructive command is cheap). Like `.ruby-version`, `.asmr` is yours to commit or ignore.
|
|
122
|
+
|
|
123
|
+
## Session duration
|
|
124
|
+
|
|
125
|
+
The lifetime of the temporary credentials (seconds, 900-43200) is set via the alias's optional `session_duration`. It is passed as `DurationSeconds` to `assume_role`, so a longer value means fewer MFA prompts: the cached credentials stay valid until they expire.
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
[my-awesome-project]
|
|
129
|
+
arn = arn:aws:iam::xxxx:role/AdminRole
|
|
130
|
+
profile = smcdk-prejp
|
|
131
|
+
session_duration = 43200
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
When omitted, the role's default (1 hour) applies. The value must not exceed the role's *Maximum session duration* in IAM, and it cannot exceed 1 hour when the credentials used to assume the role are themselves temporary (role chaining). When STS rejects it, `asmr` retries without `session_duration` and tells you so, **except** when an MFA code was just consumed: a TOTP code cannot be reused, so `asmr` fails with guidance instead of asking you for another code. A change to `session_duration` only takes effect after the currently cached credentials expire (or after `asmr --clear`).
|
|
135
|
+
|
|
136
|
+
## Web Login (AWS Management Console)
|
|
137
|
+
|
|
138
|
+
The companion command `asmr-login` opens the **AWS Management Console** in your browser as the assumed role, using the [AWS federation endpoint](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_enable-console-custom-url.html). This is handy when you want a *browser* session for a role you normally only use from the CLI.
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
asmr-login --name=awesome-app-staging
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
It assumes the role exactly like `asmr` does — sharing the same alias resolution (`--name`/`-n`), MFA prompt and credential cache — then exchanges the temporary credentials for a sign-in token at `https://signin.aws.amazon.com/federation` and opens the resulting console URL in your default browser. On a headless host where no browser opener is available, the URL is printed instead (it is valid for 15 minutes — treat it as a secret).
|
|
145
|
+
|
|
146
|
+
### Landing region
|
|
147
|
+
|
|
148
|
+
The console page you land on is derived from the `region` of the chosen alias. Add `region` to the alias:
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
[my-awesome-project]
|
|
152
|
+
arn = arn:aws:iam::xxxx:role/AdminRole
|
|
153
|
+
profile = smcdk-prejp
|
|
154
|
+
region = ap-northeast-1
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Then `asmr-login --name=my-awesome-project` opens the console home of `ap-northeast-1`. When the alias has no `region` (or you pass an ARN directly), it falls back to the global console home (`https://console.aws.amazon.com/`).
|
|
158
|
+
|
|
159
|
+
### Session duration
|
|
160
|
+
|
|
161
|
+
The console session duration is the alias's `session_duration` described above; it is sent as `SessionDuration` to the federation endpoint as well. When omitted, `asmr-login` requests 43200 (12h).
|
|
162
|
+
|
|
163
|
+
Note: the requested duration must be **less than the assumed role's maximum session duration** (1 hour by default). When the federation endpoint rejects it (e.g. the role's max is shorter, or you reached the role via role chaining), `asmr-login` automatically retries *without* it, falling back to the lifetime of the temporary credentials. To get a full 12-hour console session, raise the role's *Maximum session duration* in IAM and set `session_duration = 43200`.
|
data/bin/asmr
CHANGED
|
@@ -1,89 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
|
|
3
|
-
require "aws/asmr"
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
Aws::ASMR::Cache.destroy!
|
|
16
|
-
exit(0)
|
|
17
|
-
else
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
prompt = Aws::ASMR::Prompt.safe
|
|
21
|
-
|
|
22
|
-
name = if options[:name]
|
|
23
|
-
options[:name]
|
|
24
|
-
else
|
|
25
|
-
alias_keys = Aws::ASMR::Alias.base.keys
|
|
26
|
-
if alias_keys.empty?
|
|
27
|
-
STDERR.puts "Please specify --name=ARN to assume_role or make alias at #{Aws::ASMR::ROOT}/alias"
|
|
28
|
-
exit(1)
|
|
29
|
-
end
|
|
30
|
-
prompt.select("Choose a role you're going to assume:", alias_keys)
|
|
31
|
-
end
|
|
32
|
-
asmr_alias = Aws::ASMR::Alias.get(name)
|
|
33
|
-
assume_role_arn = if asmr_alias
|
|
34
|
-
asmr_alias.set_environment_variables!
|
|
35
|
-
asmr_alias.arn
|
|
36
|
-
else
|
|
37
|
-
name
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def run(command, shell_variables=[])
|
|
3
|
+
require "aws/asmr/cli"
|
|
4
|
+
|
|
5
|
+
# Runs command with the temporary credentials in its environment. The
|
|
6
|
+
# credentials never go through a shell command line, so they are not visible
|
|
7
|
+
# in `ps` output or shell history of the child.
|
|
8
|
+
#
|
|
9
|
+
# asmr # no command: print the variables
|
|
10
|
+
# asmr "aws sts get-caller-identity | grep Arn"
|
|
11
|
+
# # one argument: a shell command line, so pipes and redirects work
|
|
12
|
+
# asmr aws ec2 describe-instances --filter '[{"Name":"instance-state-name","Values":["stopped"]}]'
|
|
13
|
+
# # several arguments: executed directly, each argument passed verbatim
|
|
14
|
+
def run(cache, command)
|
|
41
15
|
if command.empty?
|
|
42
|
-
|
|
16
|
+
puts cache.shell_variables
|
|
43
17
|
else
|
|
44
|
-
|
|
45
|
-
# Ex: asmr "aws sts get-caller-identity | grep Arn"
|
|
46
|
-
command
|
|
47
|
-
else
|
|
48
|
-
# Ex: asmr aws ec2 describe-instances --filter '[{"Name":"instance-state-name","Values":["stopped"]}]'
|
|
49
|
-
command.map{|plain|
|
|
50
|
-
if plain.match?(/[\"\'\ ]/)
|
|
51
|
-
quoted = plain.gsub("'"){"\\'"}
|
|
52
|
-
"'#{quoted}'"
|
|
53
|
-
else
|
|
54
|
-
plain
|
|
55
|
-
end
|
|
56
|
-
}
|
|
57
|
-
end
|
|
58
|
-
exec([*shell_variables, *command].join(' '))
|
|
18
|
+
exec(cache.environment, *command)
|
|
59
19
|
end
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
run(command_args, cache.shell_variables)
|
|
20
|
+
rescue Errno::ENOENT
|
|
21
|
+
STDERR.puts "asmr: command not found: #{command.first}"
|
|
22
|
+
exit(127)
|
|
64
23
|
end
|
|
65
24
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
assume_role_args = asmr_alias ? asmr_alias.assume_role_args : {}
|
|
70
|
-
assume_role_args = if serial_number
|
|
71
|
-
token_code = prompt.ask("Type MFA token code:")
|
|
72
|
-
assume_role_args.merge({serial_number: serial_number, token_code: token_code})
|
|
73
|
-
else
|
|
74
|
-
assume_role_args
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
res = Aws::ASMR.assume_role(assume_role_arn, **assume_role_args)
|
|
78
|
-
cache = Aws::ASMR::Cache.new(**res.credentials.to_h)
|
|
79
|
-
cache.save!(assume_role_arn)
|
|
80
|
-
run(command_args, cache.shell_variables)
|
|
81
|
-
rescue => e
|
|
82
|
-
STDERR.puts e.message
|
|
83
|
-
if options[:verbose]
|
|
84
|
-
STDERR.puts e.backtrace
|
|
85
|
-
else
|
|
86
|
-
STDERR.puts "Add --verbose for more error information."
|
|
87
|
-
end
|
|
88
|
-
exit(1)
|
|
89
|
-
end
|
|
25
|
+
Aws::ASMR::CLI.main(ARGV) do |cache, command_args, _options|
|
|
26
|
+
run(cache, command_args)
|
|
27
|
+
end
|
data/bin/asmr-login
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require "aws/asmr/cli"
|
|
4
|
+
|
|
5
|
+
# Session duration is read from the alias only; nil lets WebLogin fall back to
|
|
6
|
+
# its default.
|
|
7
|
+
Aws::ASMR::CLI.main(ARGV) do |cache, _command_args, _options, asmr_alias|
|
|
8
|
+
args = { region: asmr_alias&.region }
|
|
9
|
+
duration = asmr_alias&.duration_seconds
|
|
10
|
+
args[:session_duration] = duration if duration
|
|
11
|
+
url = Aws::ASMR::WebLogin.signin_url(cache, **args)
|
|
12
|
+
Aws::ASMR::WebLogin.open_browser(url)
|
|
13
|
+
end
|
data/lib/aws/asmr/alias.rb
CHANGED
|
@@ -3,7 +3,7 @@ require 'aws/asmr'
|
|
|
3
3
|
|
|
4
4
|
module Aws
|
|
5
5
|
module ASMR
|
|
6
|
-
class Alias < Struct.new(:arn, :access_key_id, :secret_access_key, :profile, :external_id, :role_session_name, keyword_init: true)
|
|
6
|
+
class Alias < Struct.new(:arn, :access_key_id, :secret_access_key, :profile, :external_id, :role_session_name, :region, :session_duration, keyword_init: true)
|
|
7
7
|
|
|
8
8
|
PATH = "#{Aws::ASMR::ROOT}/alias"
|
|
9
9
|
|
|
@@ -58,7 +58,13 @@ module Aws
|
|
|
58
58
|
end
|
|
59
59
|
|
|
60
60
|
def assume_role_args
|
|
61
|
-
{external_id: external_id, role_session_name: role_session_name}.compact
|
|
61
|
+
{external_id: external_id, role_session_name: role_session_name, duration_seconds: duration_seconds}.compact
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# session_duration as an Integer, or nil when unset. Used both as
|
|
65
|
+
# DurationSeconds of assume_role and as SessionDuration of the console.
|
|
66
|
+
def duration_seconds
|
|
67
|
+
session_duration.to_i if session_duration && !session_duration.empty?
|
|
62
68
|
end
|
|
63
69
|
end
|
|
64
70
|
end
|
data/lib/aws/asmr/cache.rb
CHANGED
|
@@ -52,12 +52,18 @@ module Aws
|
|
|
52
52
|
end
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
# Environment for a process running as the assumed role, ready for
|
|
56
|
+
# Kernel#exec / Kernel#spawn.
|
|
57
|
+
def environment
|
|
56
58
|
{
|
|
57
|
-
AWS_ACCESS_KEY_ID
|
|
58
|
-
AWS_SECRET_ACCESS_KEY
|
|
59
|
-
AWS_SESSION_TOKEN
|
|
60
|
-
}
|
|
59
|
+
"AWS_ACCESS_KEY_ID" => access_key_id,
|
|
60
|
+
"AWS_SECRET_ACCESS_KEY" => secret_access_key,
|
|
61
|
+
"AWS_SESSION_TOKEN" => session_token,
|
|
62
|
+
}
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def shell_variables
|
|
66
|
+
environment.map{|k,v| "#{k}=#{v}"}
|
|
61
67
|
end
|
|
62
68
|
end
|
|
63
69
|
end
|
data/lib/aws/asmr/cli.rb
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
require "aws/asmr"
|
|
2
|
+
require "aws/asmr/options"
|
|
3
|
+
require "aws/asmr/prompt"
|
|
4
|
+
|
|
5
|
+
module Aws
|
|
6
|
+
module ASMR
|
|
7
|
+
# The parts shared by every asmr executable: option parsing, --version /
|
|
8
|
+
# --clear, resolving the role (alias or ARN), and obtaining temporary
|
|
9
|
+
# credentials via assume_role (with MFA prompt and caching).
|
|
10
|
+
#
|
|
11
|
+
# Each executable is just a thin wrapper that decides what to do with the
|
|
12
|
+
# resolved credentials:
|
|
13
|
+
#
|
|
14
|
+
# Aws::ASMR::CLI.main(ARGV) do |cache, command_args, options, asmr_alias|
|
|
15
|
+
# # ... use cache.shell_variables / build a login URL / etc.
|
|
16
|
+
# end
|
|
17
|
+
module CLI
|
|
18
|
+
# Parses asmr-level args, handles --version/--clear, resolves credentials
|
|
19
|
+
# and yields (cache, command_args, options, asmr_alias) to the block; the
|
|
20
|
+
# resolved alias is nil when an ARN was given directly. Top-level errors
|
|
21
|
+
# are reported here (with a backtrace under --verbose) and exit non-zero.
|
|
22
|
+
def main(argv)
|
|
23
|
+
options, command_args = Options.parse(argv)
|
|
24
|
+
Local.run(command_args.drop(1)) if command_args.first == "local"
|
|
25
|
+
|
|
26
|
+
if options[:version]
|
|
27
|
+
require "aws/asmr/version"
|
|
28
|
+
puts VERSION
|
|
29
|
+
exit(0)
|
|
30
|
+
elsif options[:clear]
|
|
31
|
+
Cache.destroy!
|
|
32
|
+
exit(0)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
prompt = Prompt.safe
|
|
36
|
+
begin
|
|
37
|
+
cache, asmr_alias = resolve_credentials(options, prompt)
|
|
38
|
+
yield cache, command_args, options, asmr_alias
|
|
39
|
+
rescue => e
|
|
40
|
+
STDERR.puts e.message
|
|
41
|
+
if options[:verbose]
|
|
42
|
+
STDERR.puts e.backtrace
|
|
43
|
+
else
|
|
44
|
+
STDERR.puts "Add --verbose for more error information."
|
|
45
|
+
end
|
|
46
|
+
exit(1)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Resolves the role to assume (from --name/-n, the nearest .asmr pin, or an
|
|
51
|
+
# interactive alias selection) and returns [cache, asmr_alias]: a Cache
|
|
52
|
+
# holding temporary credentials (reusing a valid cache entry or performing
|
|
53
|
+
# assume_role with an MFA prompt), and the resolved Alias (nil when an ARN
|
|
54
|
+
# was given directly).
|
|
55
|
+
def resolve_credentials(options, prompt)
|
|
56
|
+
name = options[:name] || pinned_name || begin
|
|
57
|
+
alias_keys = Alias.base.keys
|
|
58
|
+
if alias_keys.empty?
|
|
59
|
+
STDERR.puts "Please specify --name=ARN to assume_role or make alias at #{ROOT}/alias"
|
|
60
|
+
exit(1)
|
|
61
|
+
end
|
|
62
|
+
prompt.select("Choose a role you're going to assume:", alias_keys)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
asmr_alias = Alias.get(name)
|
|
66
|
+
assume_role_arn = if asmr_alias
|
|
67
|
+
asmr_alias.set_environment_variables!
|
|
68
|
+
asmr_alias.arn
|
|
69
|
+
else
|
|
70
|
+
name
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
if cache = Cache.get(assume_role_arn)
|
|
74
|
+
return [cache, asmr_alias]
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
serial_number = Aws::ASMR.detect_mfa_device_serial_number
|
|
78
|
+
assume_role_args = asmr_alias ? asmr_alias.assume_role_args : {}
|
|
79
|
+
if serial_number
|
|
80
|
+
token_code = prompt.ask("Type MFA token code:")
|
|
81
|
+
assume_role_args = assume_role_args.merge(serial_number: serial_number, token_code: token_code)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
res = perform_assume_role(assume_role_arn, assume_role_args, mfa: !serial_number.nil?)
|
|
85
|
+
cache = Cache.new(**res.credentials.to_h)
|
|
86
|
+
cache.save!(assume_role_arn)
|
|
87
|
+
[cache, asmr_alias]
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Name pinned by the nearest .asmr (see Local), validated so that a stale
|
|
91
|
+
# pin fails with a clear message instead of an obscure assume_role error.
|
|
92
|
+
def pinned_name
|
|
93
|
+
name, file = Local.find
|
|
94
|
+
return nil unless name
|
|
95
|
+
Local.validate!(name, source: file)
|
|
96
|
+
name
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Calls assume_role. When DurationSeconds is rejected (the role's max
|
|
100
|
+
# session duration is shorter, or the source credentials are temporary so
|
|
101
|
+
# role chaining caps it at 1h), retries without it -- unless an MFA code
|
|
102
|
+
# was consumed, since a TOTP code cannot be reused; then it fails with
|
|
103
|
+
# guidance rather than asking for another code.
|
|
104
|
+
def perform_assume_role(arn, args, mfa:)
|
|
105
|
+
Aws::ASMR.assume_role(arn, **args)
|
|
106
|
+
rescue Aws::STS::Errors::ValidationError => e
|
|
107
|
+
raise unless args[:duration_seconds] && e.message.match?(/durationseconds/i)
|
|
108
|
+
hint = "assume_role with DurationSeconds=#{args[:duration_seconds]} was rejected: #{e.message}"
|
|
109
|
+
if mfa
|
|
110
|
+
raise "#{hint}\nLower session_duration in the alias (or raise the role's maximum session duration) and try again."
|
|
111
|
+
end
|
|
112
|
+
STDERR.puts "#{hint} Retrying without DurationSeconds..."
|
|
113
|
+
Aws::ASMR.assume_role(arn, **args.reject { |k, _| k == :duration_seconds })
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
module_function :main, :resolve_credentials, :pinned_name, :perform_assume_role
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
require 'pathname'
|
|
2
|
+
require 'aws/asmr'
|
|
3
|
+
|
|
4
|
+
module Aws
|
|
5
|
+
module ASMR
|
|
6
|
+
# Per-directory pinning of the role to assume, modelled after `rbenv local`.
|
|
7
|
+
#
|
|
8
|
+
# asmr local NAME # write NAME (alias or ARN) to ./.asmr
|
|
9
|
+
# asmr local # print the name pinned in the current directory
|
|
10
|
+
# asmr local --unset # remove ./.asmr
|
|
11
|
+
#
|
|
12
|
+
# When asmr runs without --name, the nearest .asmr found in the current
|
|
13
|
+
# directory or any of its parents decides the role, so the interactive
|
|
14
|
+
# alias selection is skipped.
|
|
15
|
+
module Local
|
|
16
|
+
FILE_NAME = ".asmr"
|
|
17
|
+
ARN_PATTERN = %r{\Aarn:aws[\w-]*:iam::\d{12}:role/}
|
|
18
|
+
|
|
19
|
+
# Runs the `local` subcommand with its remaining args and exits.
|
|
20
|
+
def run(args)
|
|
21
|
+
if args.empty?
|
|
22
|
+
name = read(Dir.pwd)
|
|
23
|
+
abort "asmr: no local role configured for this directory" unless name
|
|
24
|
+
puts name
|
|
25
|
+
elsif args == ["--unset"]
|
|
26
|
+
unset(Dir.pwd)
|
|
27
|
+
elsif args.length == 1 && !args.first.start_with?('-')
|
|
28
|
+
write(args.first, Dir.pwd)
|
|
29
|
+
else
|
|
30
|
+
abort "Usage: asmr local [NAME|--unset]"
|
|
31
|
+
end
|
|
32
|
+
exit(0)
|
|
33
|
+
rescue RuntimeError => e
|
|
34
|
+
abort e.message
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def path(dir)
|
|
38
|
+
File.join(dir, FILE_NAME)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Name pinned exactly in dir (first non-empty, non-comment line), or nil.
|
|
42
|
+
def read(dir)
|
|
43
|
+
file = path(dir)
|
|
44
|
+
return nil unless File.file?(file)
|
|
45
|
+
File.readlines(file).map(&:strip).find { |l| !l.empty? && !l.start_with?('#') }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Walks up from dir to the filesystem root and returns [name, file] of the
|
|
49
|
+
# nearest .asmr, or nil when none is found.
|
|
50
|
+
def find(dir = Dir.pwd)
|
|
51
|
+
Pathname.new(dir).expand_path.ascend do |d|
|
|
52
|
+
name = read(d.to_s)
|
|
53
|
+
return [name, path(d.to_s)] if name
|
|
54
|
+
end
|
|
55
|
+
nil
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def write(name, dir)
|
|
59
|
+
validate!(name)
|
|
60
|
+
File.write(path(dir), "#{name}\n")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def unset(dir)
|
|
64
|
+
file = path(dir)
|
|
65
|
+
File.delete(file) if File.exist?(file)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# A name must be a defined alias or a role ARN, the same way rbenv refuses
|
|
69
|
+
# to pin a version that is not installed.
|
|
70
|
+
def validate!(name, source: nil)
|
|
71
|
+
return if Alias.get(name) || name.match?(ARN_PATTERN)
|
|
72
|
+
where = source ? " (pinned at #{source})" : ""
|
|
73
|
+
raise "asmr: '#{name}'#{where} is neither an alias defined at #{Alias::PATH} nor a role ARN"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
module_function :run, :path, :read, :find, :write, :unset, :validate!
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
data/lib/aws/asmr/options.rb
CHANGED
|
@@ -3,27 +3,24 @@ require "aws/asmr"
|
|
|
3
3
|
|
|
4
4
|
module Aws::ASMR
|
|
5
5
|
module Options
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
end
|
|
16
|
-
[asmr_args, command_args]
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def parse(args)
|
|
6
|
+
# Parses asmr's own options from the head of argv and returns
|
|
7
|
+
# [options, command_args]. Parsing stops at the first non-option argument
|
|
8
|
+
# (or after a literal "--"), so anything from there on -- including
|
|
9
|
+
# arguments like --filter that start with "-" -- belongs to the command:
|
|
10
|
+
#
|
|
11
|
+
# asmr --name=foo aws ec2 describe-instances --filter '...'
|
|
12
|
+
# asmr --name foo aws sts get-caller-identity
|
|
13
|
+
# asmr -n foo -- aws sts get-caller-identity
|
|
14
|
+
def parse(argv)
|
|
20
15
|
options = {}
|
|
16
|
+
command_args = argv.dup
|
|
21
17
|
OptionParser.new do |opts|
|
|
22
18
|
# opts.banner = "Usage: asmr [options]"
|
|
23
19
|
opts.banner = <<~EOS
|
|
24
20
|
You can use ALIAS to shortcut name input by setting it at #{Aws::ASMR::ROOT}/alias
|
|
25
21
|
|
|
26
|
-
Usage: asmr [options] [command] [arg...]
|
|
22
|
+
Usage: asmr [options] [--] [command] [arg...]
|
|
23
|
+
asmr local [NAME|--unset] Pin NAME (alias or ARN) to the current directory
|
|
27
24
|
EOS
|
|
28
25
|
|
|
29
26
|
opts.on("-nNAME", "--name=NAME", "Name to perform assume role with ARN or ALIAS") do |name|
|
|
@@ -42,14 +39,11 @@ module Aws::ASMR
|
|
|
42
39
|
end
|
|
43
40
|
opts.on("--clear", "Clear cache") do
|
|
44
41
|
options[:clear] = true
|
|
45
|
-
# require "aws/asmr/version"
|
|
46
|
-
# puts Aws::ASMR::VERSION
|
|
47
|
-
# exit(0)
|
|
48
42
|
end
|
|
49
|
-
end.
|
|
50
|
-
options
|
|
43
|
+
end.order!(command_args)
|
|
44
|
+
[options, command_args]
|
|
51
45
|
end
|
|
52
46
|
|
|
53
|
-
module_function :
|
|
47
|
+
module_function :parse
|
|
54
48
|
end
|
|
55
49
|
end
|
data/lib/aws/asmr/version.rb
CHANGED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
require 'uri'
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'rbconfig'
|
|
5
|
+
require 'aws/asmr'
|
|
6
|
+
|
|
7
|
+
module Aws
|
|
8
|
+
module ASMR
|
|
9
|
+
# Builds a sign-in URL for the AWS Management Console out of the temporary
|
|
10
|
+
# credentials obtained by assume_role, using the AWS federation endpoint.
|
|
11
|
+
# See: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_enable-console-custom-url.html
|
|
12
|
+
module WebLogin
|
|
13
|
+
ENDPOINT = "https://signin.aws.amazon.com/federation"
|
|
14
|
+
DEFAULT_DESTINATION = "https://console.aws.amazon.com/"
|
|
15
|
+
DEFAULT_ISSUER = "aws-asmr"
|
|
16
|
+
# Console session duration. Up to 43200 (12h), but it must be LESS than the
|
|
17
|
+
# max session duration setting of the role being assumed (default 1h), so
|
|
18
|
+
# request_signin_token falls back to omitting it when the endpoint rejects it.
|
|
19
|
+
DEFAULT_SESSION_DURATION = 43200
|
|
20
|
+
|
|
21
|
+
# Exchanges the temporary credentials for a sign-in token at the federation
|
|
22
|
+
# endpoint. Retries without SessionDuration when it is rejected (e.g. the
|
|
23
|
+
# role's max session duration is shorter, or the credentials come from role
|
|
24
|
+
# chaining, both of which make SessionDuration invalid).
|
|
25
|
+
def signin_token(cache, session_duration: DEFAULT_SESSION_DURATION)
|
|
26
|
+
res = request_signin_token(cache, session_duration)
|
|
27
|
+
if !res.is_a?(Net::HTTPSuccess) && session_duration
|
|
28
|
+
STDERR.puts "getSigninToken with SessionDuration=#{session_duration} was rejected (HTTP #{res.code}). Retrying without SessionDuration..."
|
|
29
|
+
res = request_signin_token(cache, nil)
|
|
30
|
+
end
|
|
31
|
+
unless res.is_a?(Net::HTTPSuccess)
|
|
32
|
+
raise "Federation endpoint returned HTTP #{res.code}: #{res.body}"
|
|
33
|
+
end
|
|
34
|
+
token = JSON.parse(res.body)["SigninToken"]
|
|
35
|
+
raise "Federation endpoint did not return a SigninToken: #{res.body}" unless token
|
|
36
|
+
token
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Builds the final console login URL. Valid for 15 minutes after creation.
|
|
40
|
+
# The landing page defaults to the given region's console home, or the
|
|
41
|
+
# global console home when no region is given (e.g. ARN passed directly).
|
|
42
|
+
def signin_url(cache, region: nil, issuer: DEFAULT_ISSUER, session_duration: DEFAULT_SESSION_DURATION)
|
|
43
|
+
token = signin_token(cache, session_duration: session_duration)
|
|
44
|
+
build_url(
|
|
45
|
+
"Action" => "login",
|
|
46
|
+
"Issuer" => issuer,
|
|
47
|
+
"Destination" => destination_for(region),
|
|
48
|
+
"SigninToken" => token,
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Console home URL to land on after sign-in.
|
|
53
|
+
def destination_for(region)
|
|
54
|
+
return DEFAULT_DESTINATION if region.nil? || region.empty?
|
|
55
|
+
"https://#{region}.console.aws.amazon.com/console/home?region=#{region}"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Opens the given URL in the default browser. Falls back to printing it
|
|
59
|
+
# (e.g. on a headless host where no opener is available).
|
|
60
|
+
def open_browser(url)
|
|
61
|
+
opener = browser_opener
|
|
62
|
+
if opener && system(*opener, url)
|
|
63
|
+
STDERR.puts "Opened the AWS Management Console in your browser."
|
|
64
|
+
else
|
|
65
|
+
STDERR.puts "Open the following URL in your browser to sign in (valid for 15 minutes):"
|
|
66
|
+
puts url
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def request_signin_token(cache, session_duration)
|
|
71
|
+
session = {
|
|
72
|
+
sessionId: cache.access_key_id,
|
|
73
|
+
sessionKey: cache.secret_access_key,
|
|
74
|
+
sessionToken: cache.session_token,
|
|
75
|
+
}.to_json
|
|
76
|
+
|
|
77
|
+
params = { "Action" => "getSigninToken", "Session" => session }
|
|
78
|
+
params["SessionDuration"] = session_duration.to_s if session_duration
|
|
79
|
+
Net::HTTP.get_response(URI(build_url(params)))
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def build_url(params)
|
|
83
|
+
uri = URI(ENDPOINT)
|
|
84
|
+
uri.query = URI.encode_www_form(params)
|
|
85
|
+
uri.to_s
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def browser_opener
|
|
89
|
+
case RbConfig::CONFIG['host_os']
|
|
90
|
+
when /darwin/ then ["open"]
|
|
91
|
+
when /mswin|mingw|cygwin/ then ["cmd", "/c", "start", ""]
|
|
92
|
+
else ["xdg-open"]
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
module_function :signin_token, :signin_url, :destination_for, :open_browser, :request_signin_token, :build_url, :browser_opener
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
data/lib/aws/asmr.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: aws-asmr
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.7
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- metheglin
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-09-03 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rake
|
|
@@ -70,17 +70,22 @@ description: ''
|
|
|
70
70
|
email: pigmybank@gmail.com
|
|
71
71
|
executables:
|
|
72
72
|
- asmr
|
|
73
|
+
- asmr-login
|
|
73
74
|
extensions: []
|
|
74
75
|
extra_rdoc_files: []
|
|
75
76
|
files:
|
|
76
77
|
- README.md
|
|
77
78
|
- bin/asmr
|
|
79
|
+
- bin/asmr-login
|
|
78
80
|
- lib/aws/asmr.rb
|
|
79
81
|
- lib/aws/asmr/alias.rb
|
|
80
82
|
- lib/aws/asmr/cache.rb
|
|
83
|
+
- lib/aws/asmr/cli.rb
|
|
84
|
+
- lib/aws/asmr/local.rb
|
|
81
85
|
- lib/aws/asmr/options.rb
|
|
82
86
|
- lib/aws/asmr/prompt.rb
|
|
83
87
|
- lib/aws/asmr/version.rb
|
|
88
|
+
- lib/aws/asmr/web_login.rb
|
|
84
89
|
homepage: https://rubygems.org/gems/aws-asmr
|
|
85
90
|
licenses:
|
|
86
91
|
- MIT
|