invar 0.8.0 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a4826c528fc5940bcc955a1fc762abc2b4800a30eb4ce5b34af0ea313dd990ab
4
- data.tar.gz: f463ed247adc47ea89d7b6965cd427d21a98de125271b262541eb0703a792d62
3
+ metadata.gz: c7bb218e77d7ce1acc82bf0160cc7386529f7e32e5a56cd2eea608bd2a191ad6
4
+ data.tar.gz: 6b209fa6041be75736d28783d996c8fe0ee1e529bdde7d94dcb8e1bbda23423d
5
5
  SHA512:
6
- metadata.gz: 6bbc1f4df04c3f4917a2cfcce611acde0859ccd74b412c0b9faa021770c4f01d246defb0131fc8052d0e2aeb40d904d40a043ecccb7a872f966b4a9264e812a3
7
- data.tar.gz: 32a1754130909b04d488adb88b67e5efe193c2173103d9041338652026ef45a55b3e4fbaa49bed2863f8b5d0d70a257ace52b5bbff913236f4c82b4a90af06bf
6
+ metadata.gz: da15b60c94678b1dc9a5e002b3f4bba02d7b2e1a77923483a2a5427839b0cff0fd0e137ce9e1ca847e6b493bb134be97f419a20bef7903adabe8de8b0f163ae9
7
+ data.tar.gz: 2d60ab3a7f5374478fe2ab872083ef4159620097e0cb65575eb6e6956b669c9fceaa86a39c085c637df125cebfc33f9ba52170a1a2389dce713de9d77da09c9d
data/.simplecov ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ SimpleCov.start do
4
+ coverage_dir '.coverage'
5
+ enable_coverage :branch
6
+
7
+ root __dir__
8
+ end
data/README.md CHANGED
@@ -26,7 +26,8 @@ variables have some downsides:
26
26
  * Cannot be easily checked against a schema for early error detection
27
27
  * Ruby's core ENV does not accept symbols as keys (a minor nuisance, but it counts)
28
28
 
29
- > **Fun Fact:** Invar is named for an [alloy used in clockmaking](https://en.wikipedia.org/wiki/Invar) - it's short for "**invar**iable".
29
+ > **Fun Fact:** Invar is named for an [alloy used in clockmaking](https://en.wikipedia.org/wiki/Invar) - it's short
30
+ > for "**invar**iable".
30
31
 
31
32
  ### Features
32
33
 
@@ -216,6 +217,10 @@ To open the file your system's default text editor (eg. nano):
216
217
 
217
218
  bundle exec rake invar:config
218
219
 
220
+ > **Automation tip** You can provide new content over STDIN
221
+ >
222
+ > bundle exec rake invar:config < config_template.yml
223
+
219
224
  #### Edit Secrets File
220
225
 
221
226
  To edit the secrets file, run this and provide the file's encryption key:
@@ -223,7 +228,27 @@ To edit the secrets file, run this and provide the file's encryption key:
223
228
  bundle exec rake invar:secrets
224
229
 
225
230
  The file will be decrypted and opened in your default editor like the config file. Once you have exited the editor, it
226
- will be re-encrypted. Remember to save your changes!
231
+ will be re-encrypted. **Remember to save your changes!**
232
+
233
+ > **Automation tip** You can set the current encryption key in the `LOCKBOX_MASTER_KEY` environment variable:
234
+ >
235
+ > LOCKBOX_MASTER_KEY=sooper_sekret_key_here bundle exec rake invar:secrets
236
+
237
+ > **Automation tip** Like invar:config, you can provide new content over STDIN
238
+ >
239
+ > bundle exec rake invar:secrets < secrets_template.yml
240
+
241
+ #### Rotating the Secrets File Encryption Key
242
+
243
+ To re-encrypt the secrets file, run this and provide the file's current encryption key:
244
+
245
+ bundle exec rake invar:rotate
246
+
247
+ A new encryption key will be generated just like using `invar:init`.
248
+
249
+ > **Automation tip** you can set the current encryption key in the `LOCKBOX_MASTER_KEY` environment variable:
250
+ >
251
+ > LOCKBOX_MASTER_KEY=sooper_sekret_key_here bundle exec rake invar:rotate
227
252
 
228
253
  ### Code
229
254
 
data/RELEASE_NOTES.md CHANGED
@@ -19,6 +19,21 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
19
19
 
20
20
  * none
21
21
 
22
+ ## [0.9.0] - 2023-09-24
23
+
24
+ ### Major Changes
25
+
26
+ * none
27
+
28
+ ### Minor Changes
29
+
30
+ * Added Rake task for secrets file key rotation
31
+ * Added ability for invar:config and invar:secrets to take replacement content over `STDIN` instead of live editing
32
+
33
+ ### Bugfixes
34
+
35
+ * none
36
+
22
37
  ## [0.8.0] - 2023-09-13
23
38
 
24
39
  ### Major Changes
@@ -9,7 +9,7 @@ module Invar
9
9
  # Verifies a file is secure
10
10
  class PrivateFile
11
11
  extend Forwardable
12
- def_delegators :@delegate_sd_obj, :stat, :to_s, :basename, :==, :chmod
12
+ def_delegators :@delegate_sd_obj, :stat, :to_s, :basename, :dirname, :extname, :==, :chmod, :rename, :write
13
13
 
14
14
  # Mask for limiting to the lowest three octal digits (which store permissions)
15
15
  PERMISSIONS_MASK = 0o777
@@ -51,7 +51,7 @@ module Invar
51
51
  define_init_task(app_namespace)
52
52
 
53
53
  define_config_task(app_namespace)
54
- define_secrets_task(app_namespace)
54
+ define_secrets_tasks(app_namespace)
55
55
 
56
56
  define_info_tasks(app_namespace)
57
57
  end
@@ -62,8 +62,8 @@ module Invar
62
62
  task :init, [:mode] do |_task, args|
63
63
  mode = args.mode
64
64
 
65
- config = ::Invar::Rake::Tasks::ConfigTask.new(app_namespace)
66
- secrets = ::Invar::Rake::Tasks::SecretTask.new(app_namespace)
65
+ config = ::Invar::Rake::Tasks::ConfigFileHandler.new(app_namespace)
66
+ secrets = ::Invar::Rake::Tasks::SecretsFileHandler.new(app_namespace)
67
67
 
68
68
  case mode
69
69
  when 'config'
@@ -71,7 +71,7 @@ module Invar
71
71
  when 'secrets'
72
72
  config = nil
73
73
  else
74
- raise "unknown mode #{ mode }. Must be one of 'config' or 'secrets'" unless mode.nil?
74
+ raise ArgumentError, "unknown mode '#{ mode }'. Must be one of 'config' or 'secrets'" unless mode.nil?
75
75
  end
76
76
 
77
77
  assert_init_conditions(config&.file_path, secrets&.file_path)
@@ -84,39 +84,44 @@ module Invar
84
84
  def define_config_task(app_namespace)
85
85
  desc 'Edit the config in your default editor'
86
86
  task :configs do
87
- ::Invar::Rake::Tasks::ConfigTask.new(app_namespace).edit
87
+ ::Invar::Rake::Tasks::ConfigFileHandler.new(app_namespace).edit
88
88
  end
89
89
 
90
90
  # alias
91
91
  task config: ['configs']
92
92
  end
93
93
 
94
- def define_secrets_task(app_namespace)
94
+ def define_secrets_tasks(app_namespace)
95
95
  desc 'Edit the encrypted secrets file in your default editor'
96
96
  task :secrets do
97
- ::Invar::Rake::Tasks::SecretTask.new(app_namespace).edit
97
+ ::Invar::Rake::Tasks::SecretsFileHandler.new(app_namespace).edit
98
98
  end
99
99
 
100
100
  # alias
101
101
  task secret: ['secrets']
102
+
103
+ desc 'Encrypt the secrets file with a new generated key'
104
+ task :rotate do
105
+ ::Invar::Rake::Tasks::SecretsFileHandler.new(app_namespace).rotate
106
+ end
102
107
  end
103
108
 
104
109
  def define_info_tasks(app_namespace)
105
110
  desc 'Show directories to be searched for the given namespace'
106
111
  task :paths do
107
- ::Invar::Rake::Tasks::StateTask.new(app_namespace).show_paths
112
+ ::Invar::Rake::Tasks::StatusHandler.new(app_namespace).show_paths
108
113
  end
109
114
  end
110
115
 
111
116
  def assert_init_conditions(config_file, secrets_file)
112
117
  return unless config_file&.exist? || secrets_file&.exist?
113
118
 
114
- msg = if !config_file&.exist?
119
+ msg = if !config_file.exist?
115
120
  <<~MSG
116
121
  Abort: Secrets file already exists (#{ secrets_file })
117
122
  Run this to init only the config file: bundle exec rake tasks invar:init[config]
118
123
  MSG
119
- elsif !secrets_file&.exist?
124
+ elsif !secrets_file.exist?
120
125
  <<~MSG
121
126
  Abort: Config file already exists (#{ config_file })
122
127
  Run this to init only the secrets file: bundle exec rake tasks invar:init[secrets]
@@ -133,7 +138,7 @@ module Invar
133
138
  end
134
139
 
135
140
  # Tasks that use a namespace for file searching
136
- class NamespacedTask
141
+ class NamespacedFileTask
137
142
  def initialize(namespace)
138
143
  @locator = FileLocator.new(namespace)
139
144
  end
@@ -150,11 +155,9 @@ module Invar
150
155
  end
151
156
 
152
157
  # Configuration file actions.
153
- class ConfigTask < NamespacedTask
158
+ class ConfigFileHandler < NamespacedFileTask
154
159
  # Creates a config file in the appropriate location
155
160
  def create
156
- raise 'File already exists' if file_path.exist?
157
-
158
161
  config_dir.mkpath
159
162
  file_path.write CONFIG_TEMPLATE
160
163
  file_path.chmod 0o600
@@ -164,79 +167,108 @@ module Invar
164
167
 
165
168
  # Edits the existing config file in the appropriate location
166
169
  def edit
167
- configs_file = begin
168
- @locator.find('config.yml')
169
- rescue ::Invar::FileLocator::FileNotFoundError => e
170
- warn <<~ERR
171
- Abort: #{ e.message }. Searched in: #{ @locator.search_paths.join(', ') }
172
- #{ CREATE_SUGGESTION }
173
- ERR
174
- exit 1
175
- end
176
-
177
- system(ENV.fetch('EDITOR', 'editor'), configs_file.to_s, exception: true)
178
-
179
- warn "File saved to: #{ configs_file }"
170
+ content = $stdin.tty? ? nil : $stdin.read
171
+ file_path = configs_file
172
+
173
+ if content
174
+ file_path.write content
175
+ else
176
+ system ENV.fetch('EDITOR', 'editor'), file_path.to_s, exception: true
177
+ end
178
+
179
+ warn "File saved to: #{ file_path }"
180
180
  end
181
181
 
182
182
  private
183
183
 
184
+ def configs_file
185
+ @locator.find 'config.yml'
186
+ rescue ::Invar::FileLocator::FileNotFoundError => e
187
+ warn <<~ERR
188
+ Abort: #{ e.message }. Searched in: #{ @locator.search_paths.join(', ') }
189
+ #{ CREATE_SUGGESTION }
190
+ ERR
191
+ exit 1
192
+ end
193
+
184
194
  def filename
185
195
  'config.yml'
186
196
  end
187
197
  end
188
198
 
189
199
  # Secrets file actions.
190
- class SecretTask < NamespacedTask
200
+ class SecretsFileHandler < NamespacedFileTask
191
201
  # Instructions hint for how to handle secret keys.
192
202
  SECRETS_INSTRUCTIONS = <<~INST
193
- Save this key to a secure password manager. You will need it to edit the secrets.yml file.
203
+ Generated key. Save this key to a secure password manager, you will need it to edit the secrets.yml file:
194
204
  INST
195
205
 
196
- # Creates a new encrypted secrets file and prints the generated encryption key to STDOUT
197
- def create
198
- raise 'File already exists' if file_path.exist?
206
+ SWAP_EXT = 'tmp'
199
207
 
208
+ # Creates a new encrypted secrets file and prints the generated encryption key to STDOUT
209
+ def create(content: SECRETS_TEMPLATE)
200
210
  encryption_key = Lockbox.generate_key
201
211
 
202
212
  write_encrypted_file(file_path,
203
213
  encryption_key: encryption_key,
204
- content: SECRETS_TEMPLATE,
214
+ content: content,
205
215
  permissions: PrivateFile::DEFAULT_PERMISSIONS)
206
216
 
207
- warn "Created file: #{ file_path }"
208
-
209
217
  warn SECRETS_INSTRUCTIONS
210
- warn 'Generated key is:'
211
218
  puts encryption_key
212
219
  end
213
220
 
214
- # Opens an editor for the decrypted contents of the secrets file. After closing the editor, the file will be
215
- # updated with the new encrypted contents.
221
+ # Updates the file with new content.
222
+ #
223
+ # Either the content is provided over STDIN or the default editor is opened with the decrypted contents of
224
+ # the secrets file. After closing the editor, the file will be updated with the new encrypted contents.
216
225
  def edit
217
- secrets_file = begin
218
- @locator.find('secrets.yml')
219
- rescue ::Invar::FileLocator::FileNotFoundError => e
220
- warn <<~ERR
221
- Abort: #{ e.message }. Searched in: #{ @locator.search_paths.join(', ') }
222
- #{ CREATE_SUGGESTION }
223
- ERR
224
- exit 1
225
- end
226
-
227
- edit_encrypted_file(secrets_file)
226
+ content = $stdin.tty? ? nil : $stdin.read
227
+
228
+ edit_encrypted_file(secrets_file, content: content)
228
229
 
229
230
  warn "File saved to #{ secrets_file }"
230
231
  end
231
232
 
233
+ def rotate
234
+ file_path = secrets_file
235
+
236
+ decrypted = read_encrypted_file(file_path, encryption_key: determine_key(file_path))
237
+
238
+ swap_file = file_path.dirname / [file_path.basename, SWAP_EXT].join('.')
239
+ file_path.rename swap_file
240
+
241
+ begin
242
+ create content: decrypted
243
+ swap_file.delete
244
+ rescue StandardError
245
+ swap_file.rename file_path.to_s
246
+ end
247
+ end
248
+
232
249
  private
233
250
 
251
+ def secrets_file
252
+ @locator.find 'secrets.yml'
253
+ rescue ::Invar::FileLocator::FileNotFoundError => e
254
+ warn <<~ERR
255
+ Abort: #{ e.message }. Searched in: #{ @locator.search_paths.join(', ') }
256
+ #{ CREATE_SUGGESTION }
257
+ ERR
258
+ exit 1
259
+ end
260
+
234
261
  def filename
235
262
  'secrets.yml'
236
263
  end
237
264
 
265
+ def read_encrypted_file(file_path, encryption_key:)
266
+ lockbox = build_lockbox(encryption_key)
267
+ lockbox.decrypt(file_path.binread)
268
+ end
269
+
238
270
  def write_encrypted_file(file_path, encryption_key:, content:, permissions: nil)
239
- lockbox = Lockbox.new(key: encryption_key)
271
+ lockbox = build_lockbox(encryption_key)
240
272
 
241
273
  encrypted_data = lockbox.encrypt(content)
242
274
 
@@ -244,23 +276,27 @@ module Invar
244
276
  # TODO: replace File.opens with photo_path.binwrite(uri.data) once FakeFS can handle it
245
277
  File.open(file_path.to_s, 'wb') { |f| f.write encrypted_data }
246
278
  file_path.chmod permissions if permissions
279
+
280
+ warn "Saved file: #{ file_path }"
247
281
  end
248
282
 
249
- def edit_encrypted_file(file_path)
283
+ def edit_encrypted_file(file_path, content: nil)
250
284
  encryption_key = determine_key(file_path)
251
285
 
252
- lockbox = build_lockbox(encryption_key)
286
+ content ||= invoke_editor(file_path, encryption_key: encryption_key)
287
+
288
+ write_encrypted_file(file_path, encryption_key: encryption_key, content: content)
289
+ end
253
290
 
254
- file_str = Tempfile.create(file_path.basename.to_s) do |tmp_file|
255
- decrypted = lockbox.decrypt(file_path.binread)
291
+ def invoke_editor(file_path, encryption_key:)
292
+ Tempfile.create(file_path.basename.to_s) do |tmp_file|
293
+ decrypted = read_encrypted_file(file_path, encryption_key: encryption_key)
256
294
 
257
295
  tmp_file.write(decrypted)
258
296
  tmp_file.rewind # rewind needed because file does not get closed after write
259
297
  system(ENV.fetch('EDITOR', 'editor'), tmp_file.path, exception: true)
260
298
  tmp_file.read
261
299
  end
262
-
263
- write_encrypted_file(file_path, encryption_key: encryption_key, content: file_str)
264
300
  end
265
301
 
266
302
  def determine_key(file_path)
@@ -282,7 +318,7 @@ module Invar
282
318
  end
283
319
 
284
320
  # General status tasks
285
- class StateTask < NamespacedTask
321
+ class StatusHandler < NamespacedFileTask
286
322
  # Prints the current paths to be searched in
287
323
  def show_paths
288
324
  warn @locator.search_paths.join("\n")
data/lib/invar/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Invar
4
4
  # Current version of the gem
5
- VERSION = '0.8.0'
5
+ VERSION = '0.9.0'
6
6
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: invar
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robin Miller
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-09-13 00:00:00.000000000 Z
11
+ date: 2023-09-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dry-schema
@@ -51,6 +51,7 @@ files:
51
51
  - ".rspec"
52
52
  - ".rubocop.yml"
53
53
  - ".ruby-version"
54
+ - ".simplecov"
54
55
  - CODE_OF_CONDUCT.md
55
56
  - Gemfile
56
57
  - LICENSE.txt