prx-ruby-aws-creds 0.0.35

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.
Files changed (3) hide show
  1. checksums.yaml +7 -0
  2. data/lib/prx-ruby-aws-creds.rb +157 -0
  3. metadata +43 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7cad954221929f68817be8f590c4325fdd8a27a07d9d4211da6ffd46638218d8
4
+ data.tar.gz: b86bda7e8969566f8a1326ec5474de5d1fc36a5cea83ccb6ffe8fb4e5cb720a8
5
+ SHA512:
6
+ metadata.gz: f967ee46512a111bcdbfc1399ed763c1f2694e40cf1d27d44720f29bd790374f4061d03f1cf4f7747122df56200fea15b3383e21d6d770029f8eb12860e39bb9
7
+ data.tar.gz: f70427461271f3a1bfafdef651af893632fc6495a5b32b1c5852d5f5c9907ec9970902a2c5f7a57cb5353f9aeb2782ef791629669d4153623e7991f977eee8cb
@@ -0,0 +1,157 @@
1
+ AWS_CONFIG_FILE = ENV["AWS_CONFIG_FILE"] || "#{Dir.home}/.aws/config"
2
+
3
+ class PrxRubyAwsCreds
4
+ class << self
5
+ def cache_directory
6
+ "#{Dir.home}/.aws/ruby/cache"
7
+ end
8
+
9
+ def cache_key_path(assume_role_options)
10
+ # The cache key is based on the parameters used for the AssumeRole call.
11
+ # The role session name is removed if it's randomly generated (which it
12
+ # always is for us). If the options were ever to include a policy document,
13
+ # that should get sorted before hashing.
14
+ # https://github.com/boto/botocore/blob/88d780dea1684da00689f2eef388fa4c782ced08/botocore/credentials.py#L700
15
+ key_opts = assume_role_options.clone
16
+ key_opts.delete(:role_session_name)
17
+ cache_key = Digest::SHA1.hexdigest(JSON.dump(key_opts))
18
+
19
+ "#{cache_directory}/#{cache_key}.json"
20
+ end
21
+
22
+ # Returns the options passed to SSO#get_role_credentials. This is used when the
23
+ # profile uses an SSO, rather than a key/secret. If the selected profile is
24
+ # not configured for SSO, returns nil.
25
+ def sso_get_role_options
26
+ aws_config_file = IniFile.load(AWS_CONFIG_FILE)
27
+ aws_config_file_section = aws_config_file["profile #{OPTS[:profile]}"]
28
+
29
+ if aws_config_file_section["sso_start_url"]
30
+ profile_start_url = aws_config_file_section["sso_start_url"]
31
+
32
+ sso_access_token = nil
33
+ Dir["#{Dir.home}/.aws/sso/cache/*.json"].each do |path|
34
+ data = JSON.parse(File.read(path))
35
+ if data["startUrl"] && data["startUrl"] == profile_start_url
36
+ sso_access_token = data["accessToken"]
37
+ break
38
+ end
39
+ end
40
+
41
+ if !sso_access_token
42
+ puts "No SSO access token was found for this profile."
43
+ puts "Press RETURN to request a token in a web browser."
44
+ puts "You can do this manually with: 'aws sso login --profile #{OPTS[:profile]}'"
45
+ inp = $stdin.gets.chomp
46
+ `aws sso login --profile #{OPTS[:profile]}` if inp.empty?
47
+ end
48
+
49
+ {
50
+ role_name: aws_config_file_section["sso_role_name"],
51
+ account_id: aws_config_file_section["sso_account_id"].to_s,
52
+ access_token: sso_access_token
53
+ }
54
+ end
55
+ end
56
+
57
+ # Returns the options passed to AssumeRole. This is used when the profile uses
58
+ # a key/secret. If the selected profile is not configured for key/secret,
59
+ # returns nil.
60
+ def assume_role_options
61
+ aws_config_file = IniFile.load(AWS_CONFIG_FILE)
62
+ aws_config_file_section = aws_config_file["profile #{OPTS[:profile]}"]
63
+ role_arn = aws_config_file_section["role_arn"]
64
+ role_name = role_arn.split("role/")[1]
65
+ account_id = role_arn.split(":")[4]
66
+
67
+ {
68
+ role_arn: "arn:aws:sts::#{account_id}:role/#{role_name}",
69
+ role_session_name: "ruby-sdk-session-#{Time.now.to_i}",
70
+ duration_seconds: 3600
71
+ }
72
+ end
73
+
74
+ def get_and_cache_credentials
75
+ FileUtils.mkdir_p cache_directory
76
+
77
+ aws_config_file = IniFile.load(AWS_CONFIG_FILE)
78
+ aws_config_file_section = aws_config_file["profile #{OPTS[:profile]}"]
79
+
80
+ if aws_config_file_section["sso_role_name"]
81
+ opts = sso_get_role_options
82
+
83
+ sso = Aws::SSO::Client.new(region: aws_config_file_section["region"])
84
+ credentials = sso.get_role_credentials(opts)
85
+
86
+ File.write(cache_key_path(opts), JSON.dump({"credentials" => {
87
+ "access_key_id" => credentials.role_credentials.access_key_id,
88
+ "secret_access_key" => credentials.role_credentials.secret_access_key,
89
+ "session_token" => credentials.role_credentials.session_token
90
+ }}))
91
+
92
+ Aws::Credentials.new(credentials.role_credentials.access_key_id, credentials.role_credentials.secret_access_key, credentials.role_credentials.session_token)
93
+ elsif aws_config_file_section["mfa_serial"]
94
+ mfa_serial = aws_config_file_section["mfa_serial"]
95
+
96
+ mfa_code = $stdin.getpass("Enter MFA code for #{mfa_serial}: ")
97
+ credentials = Aws.shared_config.assume_role_credentials_from_config(profile: OPTS[:profile], token_code: mfa_code.chomp)
98
+ sts = Aws::STS::Client.new(
99
+ region: "us-east-1",
100
+ credentials: credentials
101
+ )
102
+ _id = sts.get_caller_identity
103
+
104
+ opts = assume_role_options
105
+ cacheable_role = sts.assume_role(assume_role_options)
106
+ File.write(cache_key_path(opts), JSON.dump(cacheable_role.to_h))
107
+
108
+ Aws::Credentials.new(cacheable_role["credentials"]["access_key_id"], cacheable_role["credentials"]["secret_access_key"], cacheable_role["credentials"]["session_token"])
109
+ end
110
+ rescue Aws::SSO::Errors::UnauthorizedException
111
+ raise "The SSO access token for this profile is invalid. Run 'aws sso login --profile #{OPTS[:profile]}' to fetch a valid token."
112
+ end
113
+
114
+ def load_and_verify_cached_credentials
115
+ # Look up the cache file based on the options for the seleted profile.
116
+ options = sso_get_role_options || assume_role_options
117
+
118
+ cached_role_json = File.read(cache_key_path(options))
119
+ cached_role = JSON.parse(cached_role_json)
120
+
121
+ credentials = Aws::Credentials.new(cached_role["credentials"]["access_key_id"], cached_role["credentials"]["secret_access_key"], cached_role["credentials"]["session_token"])
122
+
123
+ # Verify that the credentials still work; this will raise an error if they're
124
+ # bad, which we can catch
125
+ sts = Aws::STS::Client.new(region: "us-east-1", credentials: credentials)
126
+ sts.get_caller_identity
127
+
128
+ credentials
129
+ rescue Aws::STS::Errors::ExpiredToken
130
+ get_and_cache_credentials
131
+ rescue Aws::STS::Errors::InvalidClientTokenId
132
+ get_and_cache_credentials
133
+ rescue Errno::ENOENT
134
+ get_and_cache_credentials
135
+ end
136
+
137
+ # Returns temporary client credentials for the profile selected with --profile
138
+ # when the command was run.
139
+ def client_credentials
140
+ # For the selected profile, get the appropriate set of options.
141
+ options = sso_get_role_options || assume_role_options
142
+
143
+ return if !options
144
+
145
+ # Check for a cache file with a name derived from those options.
146
+ if !File.file?(cache_key_path(options))
147
+ # When no cache exists for these options, fetch new credentials, cache them
148
+ # and return them.
149
+ get_and_cache_credentials
150
+ else
151
+ # When there is a cache for these options, return them if they are still
152
+ # valid, otherwise refresh them and return the new credentials.
153
+ load_and_verify_cached_credentials
154
+ end
155
+ end
156
+ end
157
+ end
metadata ADDED
@@ -0,0 +1,43 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: prx-ruby-aws-creds
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.35
5
+ platform: ruby
6
+ authors:
7
+ - Christopher Kalafarski
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-04-04 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: tktk
14
+ email: chris.kalafarski@prx.org
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - lib/prx-ruby-aws-creds.rb
20
+ homepage: https://github.com/PRX/homebrew-dev-tools/tree/main/lib/prx-ruby-aws-creds
21
+ licenses:
22
+ - MIT
23
+ metadata: {}
24
+ post_install_message:
25
+ rdoc_options: []
26
+ require_paths:
27
+ - lib
28
+ required_ruby_version: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ required_rubygems_version: !ruby/object:Gem::Requirement
34
+ requirements:
35
+ - - ">="
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ requirements: []
39
+ rubygems_version: 3.4.10
40
+ signing_key:
41
+ specification_version: 4
42
+ summary: tktk
43
+ test_files: []