Class: OpenStax::Aws::Secrets
- Inherits:
-
Object
- Object
- OpenStax::Aws::Secrets
show all
- Defined in:
- lib/openstax/aws/secrets.rb
Defined Under Namespace
Classes: ReadOnlyParameter
Constant Summary
collapse
- GENERATED_WITH_PREFIX =
We store “secrets” in the AWS Parameter store. Secrets can be truly secret values (e.g. keys) or just some configuration values.
"Generated with"
Instance Attribute Summary collapse
Class Method Summary
collapse
Instance Method Summary
collapse
-
#build_secret(secret_name, spec_value, substitutions) ⇒ Object
-
#build_secrets(specifications:, substitutions:) ⇒ Object
-
#create(specifications: nil, substitutions: nil) ⇒ Object
-
#data ⇒ Object
-
#data! ⇒ Object
-
#define(specifications:, substitutions: {}) ⇒ Object
‘create` and `update` take secrets specifications and secrets substitutions.
-
#delete ⇒ Object
-
#get(local_name) ⇒ Object
-
#initialize(region:, dry_run: true, namespace:) ⇒ Secrets
constructor
A new instance of Secrets.
-
#key_prefix ⇒ Object
-
#process_individual_spec_value(spec_value, substitutions) ⇒ Object
-
#update(specifications: nil, substitutions: nil, force_update_these: []) ⇒ Object
Constructor Details
#initialize(region:, dry_run: true, namespace:) ⇒ Secrets
Returns a new instance of Secrets.
11
12
13
14
15
16
17
|
# File 'lib/openstax/aws/secrets.rb', line 11
def initialize(region:, dry_run: true, namespace:)
@region = region
@dry_run = dry_run
@namespace = namespace
@client = Aws::SSM::Client.new(region: region)
@substitutions = {}
end
|
Instance Attribute Details
#dry_run ⇒ Object
Returns the value of attribute dry_run.
9
10
11
|
# File 'lib/openstax/aws/secrets.rb', line 9
def dry_run
@dry_run
end
|
#namespace ⇒ Object
Returns the value of attribute namespace.
9
10
11
|
# File 'lib/openstax/aws/secrets.rb', line 9
def namespace
@namespace
end
|
#region ⇒ Object
Returns the value of attribute region.
9
10
11
|
# File 'lib/openstax/aws/secrets.rb', line 9
def region
@region
end
|
Class Method Details
.changed_secrets(existing_secrets_hash, new_secrets_array) ⇒ Object
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
|
# File 'lib/openstax/aws/secrets.rb', line 82
def self.changed_secrets(existing_secrets_hash, new_secrets_array)
existing_secrets_hash = existing_secrets_hash.with_indifferent_access
new_secrets_array = new_secrets_array.map(&:with_indifferent_access)
new_secrets_array.each_with_object([]) do |new_secret, array|
existing_secret = existing_secrets_hash[new_secret[:name]]
if existing_secret
next if existing_secret[:value] == new_secret[:value]
next if new_secret[:description].try(:starts_with?, GENERATED_WITH_PREFIX) &&
new_secret[:description] == existing_secret[:description]
end
array.push(new_secret)
end
end
|
Instance Method Details
#build_secret(secret_name, spec_value, substitutions) ⇒ Object
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
|
# File 'lib/openstax/aws/secrets.rb', line 123
def build_secret(secret_name, spec_value, substitutions)
secret = {
name: "#{key_prefix}/#{secret_name}"
}
case spec_value
when Array
processed_items = spec_value.map do |item|
process_individual_spec_value(item, substitutions)[:value]
end
secret[:value] = processed_items.join(",")
secret[:type] = "StringList"
else
spec_value = spec_value.to_s.strip
secret.merge!(process_individual_spec_value(spec_value, substitutions))
end
if (generated = secret.delete(:generated))
secret[:description] = "#{GENERATED_WITH_PREFIX} #{spec_value}"
end
secret
end
|
#build_secrets(specifications:, substitutions:) ⇒ Object
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
|
# File 'lib/openstax/aws/secrets.rb', line 103
def build_secrets(specifications:, substitutions:)
specifications ||= @specifications
substitutions ||= @substitutions
specifications = [specifications].flatten
raise "Cannot build secrets without a specification" if specifications.empty?
expanded_data = {}
specifications.reverse.each do |specification|
expanded_data.merge!(specification.expanded_data)
end
expanded_data.map do |secret_name, spec_value|
build_secret(secret_name, spec_value, substitutions)
end
end
|
#create(specifications: nil, substitutions: nil) ⇒ Object
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
|
# File 'lib/openstax/aws/secrets.rb', line 34
def create(specifications: nil, substitutions: nil)
built_secrets = build_secrets(specifications: specifications, substitutions: substitutions)
OpenStax::Aws.logger.info("**** DRY RUN ****") if dry_run
OpenStax::Aws.logger.info("Creating the following secrets in the AWS parameter store: #{built_secrets}")
if !dry_run
built_secrets.each do |built_secret|
client.put_parameter(built_secret.merge(overwrite: true))
sleep(0.1)
end
end
end
|
#data ⇒ Object
205
206
207
|
# File 'lib/openstax/aws/secrets.rb', line 205
def data
@data ||= data!
end
|
#data! ⇒ Object
209
210
211
212
213
214
215
216
217
218
219
220
221
|
# File 'lib/openstax/aws/secrets.rb', line 209
def data!
{}.tap do |hash|
client.get_parameters_by_path({
path: key_prefix,
recursive: true,
with_decryption: true
}).each do |response|
response.parameters.each do |parameter|
hash[parameter.name] = ReadOnlyParameter.new(parameter, client)
end
end
end
end
|
#define(specifications:, substitutions: {}) ⇒ Object
‘create` and `update` take secrets specifications and secrets substitutions. if you just want to call `create` and `update` with no arguments, you can define the specifications and substitutions ahead of time with this method, e.g.
my_secrets.define(specifications: my_specifications, substitutions: my_substitutions)
...
my_secrets.create
See the README for more discussion about specifications and substitutions.
29
30
31
32
|
# File 'lib/openstax/aws/secrets.rb', line 29
def define(specifications:, substitutions: {})
@specifications = specifications
@substitutions = substitutions
end
|
#delete ⇒ Object
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
|
# File 'lib/openstax/aws/secrets.rb', line 270
def delete
secret_names = data!.keys
return if secret_names.empty?
OpenStax::Aws.logger.info("**** DRY RUN ****") if dry_run
OpenStax::Aws.logger.info("Deleting the following secrets in the AWS parameter store: #{secret_names}")
if !dry_run
@data = nil
secret_names.each_slice(10) do |some_secret_names|
response = client.delete_parameters({names: some_secret_names})
if response.invalid_parameters.any?
OpenStax::Aws.logger.debug("Unable to delete some secrets (likely already deleted): #{response.invalid_parameters}")
end
end
end
end
|
#get(local_name) ⇒ Object
264
265
266
267
268
|
# File 'lib/openstax/aws/secrets.rb', line 264
def get(local_name)
local_name = local_name.to_s
local_name = "/#{local_name}" unless local_name.chr == "/"
data["#{key_prefix}#{local_name}"].try(:[], :value)
end
|
#key_prefix ⇒ Object
292
293
294
|
# File 'lib/openstax/aws/secrets.rb', line 292
def key_prefix
"/" + [namespace].flatten.reject(&:blank?).join("/")
end
|
#process_individual_spec_value(spec_value, substitutions) ⇒ Object
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
|
# File 'lib/openstax/aws/secrets.rb', line 147
def process_individual_spec_value(spec_value, substitutions)
generated = false
type = "SecureString"
value = case spec_value
when /^random\(hex,(\d+)\)$/
generated = true
num_characters = $1.to_i
SecureRandom.hex(num_characters)[0..num_characters-1]
when /^random\(base64,(\d+)\)$/
generated = true
num_characters = $1.to_i
SecureRandom.urlsafe_base64(num_characters)[0..num_characters-1]
when /^rsa\((\d+)\)$/
generated = true
key_length = $1.to_i
OpenSSL::PKey::RSA.new(key_length).to_s
when "uuid"
generated = true
SecureRandom.uuid
when /{([^{}]+)}/
spec_value.gsub(/({{\W*(\w+)\W*}})/) do |match|
if (!substitutions.has_key?($2) && !substitutions.has_key?($2.to_sym))
raise "no substitution provided for #{$2}"
end
substitutions[$2] || substitutions[$2.to_sym]
end
when /^ssm\((.*)\)$/
begin
parameter_name = $1.starts_with?("/") ? $1 : (substitutions[$1] || substitutions[$1.to_sym])
if parameter_name.blank?
raise "#{$1} is neither a literal parameter name nor available in the given substitutions"
end
parameter = client.get_parameter({
name: parameter_name,
with_decryption: true
}).parameter
type = parameter.type
parameter.value
rescue Aws::SSM::Errors::ParameterNotFound => ee
raise "Could not get secret '#{$1}'"
end
else spec_value
end
{
value: value,
type: type,
generated: generated
}
end
|
#update(specifications: nil, substitutions: nil, force_update_these: []) ⇒ Object
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
|
# File 'lib/openstax/aws/secrets.rb', line 51
def update(specifications: nil, substitutions: nil, force_update_these: [])
existing_secrets = data!
built_secrets = build_secrets(specifications: specifications, substitutions: substitutions)
changed_secrets = self.class.changed_secrets(existing_secrets, built_secrets)
force_update_these.each do |force_update_this|
built_secrets.select{|built_secret| built_secret[:name].match(force_update_this)}.each do |forced|
changed_secrets.push(forced)
end
end
changed_secrets.uniq!
OpenStax::Aws.logger.info("**** DRY RUN ****") if dry_run
if changed_secrets.empty?
OpenStax::Aws.logger.info("Secrets did not change")
return false
else
OpenStax::Aws.logger.info("Updating the following secrets in the AWS parameter store: #{changed_secrets}")
if !dry_run
changed_secrets.each do |changed_secret|
client.put_parameter(changed_secret.merge(overwrite: true))
end
end
return true
end
end
|