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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b9804add634c49c85745aa41741d6558b7dc5e0cab20426aaa9827eb06ae6808
4
- data.tar.gz: 8ebaaabd5a6a65c5609ff08a2723a4fa135298667b842cd7175549dec6069b96
3
+ metadata.gz: 5f82db6a4f331d524f5c56dab14452abe7e6b727e4870990fec14410eb728cdd
4
+ data.tar.gz: 62c1380dbe38966db335122e19e285fb1b3c6d0b3ba960c1e9d3023312d689ba
5
5
  SHA512:
6
- metadata.gz: 0345bb76d7e174c1a18debd29a262d85506cb95f4e2c76c7f4aa86a1ca9bd6de6c09c5a040d12794cae2af1ea18582d311789bf458fb935a1525d0c30dd5ab37
7
- data.tar.gz: 4fb26c3b1aeb378ff4da4e47b0bf5450c70223d781f1993c1dbb03353755eb59781d8ba7a61e5798d19e003491cca53d6a3307afd2adff7312e04b52d9374fd4
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), you MUST set `name` option with a form like `--name=<arn>` NOT a form like `--name <arn>`. For short version, `-n<arn>` works, `-n <arn>` doesn't. You must be wasting time for this pitfall, sorry!
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
- Unfortunatelly you need quote and appropriate escape to run piped command as subsequential.
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
- require "aws/asmr/options"
5
- require "aws/asmr/prompt"
6
-
7
- asmr_args, command_args = Aws::ASMR::Options.partition(ARGV)
8
- options = Aws::ASMR::Options.parse(asmr_args)
9
-
10
- if options[:version]
11
- require "aws/asmr/version"
12
- puts Aws::ASMR::VERSION
13
- exit(0)
14
- elsif options[:clear]
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
- exec("echo \"#{shell_variables.join($/)}\"")
16
+ puts cache.shell_variables
43
17
  else
44
- command = if command.length == 1
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
- end
61
-
62
- if cache = Aws::ASMR::Cache.get(assume_role_arn)
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
- begin
68
- serial_number = Aws::ASMR.detect_mfa_device_serial_number
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
@@ -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
@@ -52,12 +52,18 @@ module Aws
52
52
  end
53
53
  end
54
54
 
55
- def shell_variables
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: access_key_id,
58
- AWS_SECRET_ACCESS_KEY: secret_access_key,
59
- AWS_SESSION_TOKEN: session_token,
60
- }.map{|k,v| "#{k}=#{v}"}
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
@@ -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
@@ -3,27 +3,24 @@ require "aws/asmr"
3
3
 
4
4
  module Aws::ASMR
5
5
  module Options
6
- def partition(args)
7
- _idx, asmr_args, command_args = args.reduce([1, [], []]) do |acc,i|
8
- idx, _, _ = acc
9
- unless i.start_with?('-')
10
- idx = 2
11
- acc[0] = idx
12
- end
13
- acc[idx] << i
14
- acc
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.parse(args)
50
- options
43
+ end.order!(command_args)
44
+ [options, command_args]
51
45
  end
52
46
 
53
- module_function :partition, :parse
47
+ module_function :parse
54
48
  end
55
49
  end
@@ -1,5 +1,5 @@
1
1
  module Aws
2
2
  module ASMR
3
- VERSION = "0.0.4"
3
+ VERSION = "0.0.7"
4
4
  end
5
5
  end
@@ -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
@@ -34,3 +34,5 @@ end
34
34
 
35
35
  require 'aws/asmr/cache'
36
36
  require 'aws/asmr/alias'
37
+ require 'aws/asmr/local'
38
+ require 'aws/asmr/web_login'
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
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-04-10 00:00:00.000000000 Z
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