aws-asmr 0.0.5 → 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: 6cc279ebf6f98696525bd333297e3d9e07f4546235685e4cc56ede2f6e5936df
4
- data.tar.gz: 5e8ba9aa25320658bbe860b0733141d4bda426a75330bd52603922bb6e0274fb
3
+ metadata.gz: 5f82db6a4f331d524f5c56dab14452abe7e6b727e4870990fec14410eb728cdd
4
+ data.tar.gz: 62c1380dbe38966db335122e19e285fb1b3c6d0b3ba960c1e9d3023312d689ba
5
5
  SHA512:
6
- metadata.gz: 3ac9215a9c0ddb69a23eb94e7aa30eb91b07ea2e4bdf1fe2be1f03f2ec4fdbb4890bd329432e443044c70360b4aba568f03e5294a9f311779aac95ec334273f2
7
- data.tar.gz: 52d2bf0b95b0f377a2507ed9e72372edce4a6dcd1aeacc1e231558bf1f4b2b228a9798c9e6d63f54f18b02c28d77c572b405aa0272ce134aaa2417f84d76254f
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.
@@ -95,6 +100,39 @@ Or you can specify alias name.
95
100
  asmr --name=awesome-app-staging aws sts get-caller-identity
96
101
  ```
97
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
+
98
136
  ## Web Login (AWS Management Console)
99
137
 
100
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.
@@ -120,14 +158,6 @@ Then `asmr-login --name=my-awesome-project` opens the console home of `ap-northe
120
158
 
121
159
  ### Session duration
122
160
 
123
- The console session duration (seconds, 900-43200) is set via the alias's optional `session_duration`. When omitted, it defaults to 43200 (12h).
124
-
125
- ```
126
- [my-awesome-project]
127
- arn = arn:aws:iam::xxxx:role/AdminRole
128
- profile = smcdk-prejp
129
- region = ap-northeast-1
130
- session_duration = 3600
131
- ```
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).
132
162
 
133
- 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 accordingly.
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
@@ -2,28 +2,26 @@
2
2
 
3
3
  require "aws/asmr/cli"
4
4
 
5
- def run(command, shell_variables=[])
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)
6
15
  if command.empty?
7
- exec("echo \"#{shell_variables.join($/)}\"")
16
+ puts cache.shell_variables
8
17
  else
9
- command = if command.length == 1
10
- # Ex: asmr "aws sts get-caller-identity | grep Arn"
11
- command
12
- else
13
- # Ex: asmr aws ec2 describe-instances --filter '[{"Name":"instance-state-name","Values":["stopped"]}]'
14
- command.map{|plain|
15
- if plain.match?(/[\"\'\ ]/)
16
- quoted = plain.gsub("'"){"\\'"}
17
- "'#{quoted}'"
18
- else
19
- plain
20
- end
21
- }
22
- end
23
- exec([*shell_variables, *command].join(' '))
18
+ exec(cache.environment, *command)
24
19
  end
20
+ rescue Errno::ENOENT
21
+ STDERR.puts "asmr: command not found: #{command.first}"
22
+ exit(127)
25
23
  end
26
24
 
27
25
  Aws::ASMR::CLI.main(ARGV) do |cache, command_args, _options|
28
- run(command_args, cache.shell_variables)
26
+ run(cache, command_args)
29
27
  end
data/bin/asmr-login CHANGED
@@ -2,16 +2,11 @@
2
2
 
3
3
  require "aws/asmr/cli"
4
4
 
5
- # Session duration is read from the alias only (a raw, possibly empty string).
6
- # Returns nil to let WebLogin fall back to its default.
7
- def alias_session_duration(asmr_alias)
8
- raw = asmr_alias&.session_duration
9
- raw.to_i if raw && !raw.empty?
10
- end
11
-
5
+ # Session duration is read from the alias only; nil lets WebLogin fall back to
6
+ # its default.
12
7
  Aws::ASMR::CLI.main(ARGV) do |cache, _command_args, _options, asmr_alias|
13
8
  args = { region: asmr_alias&.region }
14
- duration = alias_session_duration(asmr_alias)
9
+ duration = asmr_alias&.duration_seconds
15
10
  args[:session_duration] = duration if duration
16
11
  url = Aws::ASMR::WebLogin.signin_url(cache, **args)
17
12
  Aws::ASMR::WebLogin.open_browser(url)
@@ -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
data/lib/aws/asmr/cli.rb CHANGED
@@ -20,8 +20,8 @@ module Aws
20
20
  # resolved alias is nil when an ARN was given directly. Top-level errors
21
21
  # are reported here (with a backtrace under --verbose) and exit non-zero.
22
22
  def main(argv)
23
- asmr_args, command_args = Options.partition(argv)
24
- options = Options.parse(asmr_args)
23
+ options, command_args = Options.parse(argv)
24
+ Local.run(command_args.drop(1)) if command_args.first == "local"
25
25
 
26
26
  if options[:version]
27
27
  require "aws/asmr/version"
@@ -47,12 +47,13 @@ module Aws
47
47
  end
48
48
  end
49
49
 
50
- # Resolves the role to assume (from --name/-n or an interactive alias
51
- # selection) and returns [cache, asmr_alias]: a Cache holding temporary
52
- # credentials (reusing a valid cache entry or performing assume_role with an
53
- # MFA prompt), and the resolved Alias (nil when an ARN was given directly).
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).
54
55
  def resolve_credentials(options, prompt)
55
- name = options[:name] || begin
56
+ name = options[:name] || pinned_name || begin
56
57
  alias_keys = Alias.base.keys
57
58
  if alias_keys.empty?
58
59
  STDERR.puts "Please specify --name=ARN to assume_role or make alias at #{ROOT}/alias"
@@ -80,13 +81,39 @@ module Aws
80
81
  assume_role_args = assume_role_args.merge(serial_number: serial_number, token_code: token_code)
81
82
  end
82
83
 
83
- res = Aws::ASMR.assume_role(assume_role_arn, **assume_role_args)
84
+ res = perform_assume_role(assume_role_arn, assume_role_args, mfa: !serial_number.nil?)
84
85
  cache = Cache.new(**res.credentials.to_h)
85
86
  cache.save!(assume_role_arn)
86
87
  [cache, asmr_alias]
87
88
  end
88
89
 
89
- module_function :main, :resolve_credentials
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
90
117
  end
91
118
  end
92
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.5"
3
+ VERSION = "0.0.7"
4
4
  end
5
5
  end
data/lib/aws/asmr.rb CHANGED
@@ -34,4 +34,5 @@ end
34
34
 
35
35
  require 'aws/asmr/cache'
36
36
  require 'aws/asmr/alias'
37
+ require 'aws/asmr/local'
37
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.5
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-06-04 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
@@ -81,6 +81,7 @@ files:
81
81
  - lib/aws/asmr/alias.rb
82
82
  - lib/aws/asmr/cache.rb
83
83
  - lib/aws/asmr/cli.rb
84
+ - lib/aws/asmr/local.rb
84
85
  - lib/aws/asmr/options.rb
85
86
  - lib/aws/asmr/prompt.rb
86
87
  - lib/aws/asmr/version.rb