invar 0.5.1 → 0.6.0

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: d7402299987dd129eaa1626ca46ea07f90fb15d8dec329a83ee8a33ea81974ee
4
- data.tar.gz: b47b0b5678fc96626d36c796d26472ff384ef70630c208b49e5591dcb441806b
3
+ metadata.gz: 881d31e6095f54b8a1167b7e06324397dcd823098fa25846a49cbe085679ee81
4
+ data.tar.gz: c6c4732702207659323642ef6522c0bd4ba3b4fde7054c9f9eee7a3baa9e5cea
5
5
  SHA512:
6
- metadata.gz: d2973d08e11198e26e528dc858344fac2ba78a92841fbe4448e37a251a86bf6d96855f28655ef9953732456f240454d9597c72aeb0d8d722d993c4f23abbeb15
7
- data.tar.gz: e3671eaea2a99e28ec28a647d2dcd70b29980c073888248b091f5ddf02f9d6b57485b911029ae072b6408c3ae1af6982555fe111a6cf405f47092e476ab3dbb6
6
+ metadata.gz: 167260ced258a4792560570c395a17267f5ffe4b78ffda5b40804b018a6e1ee5697b1389fa5d631048ac9c62f89a2e988b038e3d02b9b214c9682447a0d01b2c
7
+ data.tar.gz: 2d49307b7c02060d112dbd8d703a21b13bc1d7b80836c24012680dd3c737c715a0784e3402cbcf341f481091584562e1e8be3fc519f503f67de6fdd41d9c7e43
data/.rubocop.yml CHANGED
@@ -1,4 +1,4 @@
1
- inherit_from: ../.rubocop.yml
1
+ inherit_from: ~/.config/rubocop/config.yml
2
2
 
3
3
  AllCops:
4
4
  Exclude:
data/README.md CHANGED
@@ -148,32 +148,32 @@ next section), but tests may need to override some of those values.
148
148
  Call `#pretend` on the relevant selector:
149
149
 
150
150
  ```ruby
151
- # Your application require Invar as normal:
151
+ # Your application uses Invar as normal:
152
152
  require 'invar'
153
153
 
154
154
  invar = Invar.new(namespace: 'my-app')
155
155
 
156
- # ... then, in your test suite:
156
+ # ... but in your test suite, require the testing extension
157
157
  require 'invar/test'
158
158
 
159
- # Usually this would be in a test suite hook,
160
- # like Cucumber's `BeforeAll` or RSpec's `before(:all)`
161
- invar[:config][:theme].pretend dark_mode: true
159
+ # And override the values as needed. Usually this would be in a test
160
+ # suite hook, like Cucumber's `BeforeAll` or RSpec's `before(:all)`
161
+ invar[:config][:mysql].pretend database: 'myapp_test'
162
162
  ```
163
163
 
164
- Calling `#pretend` without requiring `invar/test` will raise an `ImmutableRealityError`.
164
+ **Note**: Calling `#pretend` without requiring `invar/test` will raise an `ImmutableRealityError`.
165
165
 
166
166
  To override values immediately after the config files are read, use an `Invar.after_load` block:
167
167
 
168
168
  ```ruby
169
169
  Invar.after_load do |invar|
170
- invar[:config][:database].pretend name: 'my_app_test'
170
+ invar[:config][:postgres].pretend database: 'my_app_test'
171
171
  end
172
172
 
173
173
  # This Invar will return database name 'my_app_test'
174
174
  invar = Invar.new(namespace: 'my-app')
175
175
 
176
- puts invar / :config / :database
176
+ puts invar / :config / :postgres
177
177
  ```
178
178
 
179
179
  ### Rake Tasks
@@ -194,38 +194,36 @@ This will show you the XDG search locations.
194
194
 
195
195
  bundle exec rake invar:paths
196
196
 
197
- #### Create Config File
197
+ #### Create Config and Secrets Files
198
198
 
199
- bundle exec rake invar:create:configs
199
+ To create a `config.yml` file and an encrypted `secrets.yml` file:
200
200
 
201
- If a config file already exists in any of the search path locations, it will yell at you.
201
+ bundle exec rake invar:init
202
202
 
203
- #### Edit Config File
204
-
205
- bundle exec rake invar:edit:configs
206
-
207
- #### Create Secrets File
203
+ It will also output to terminal the generated master key used to decrypt the secrets file. Save this key in a password
204
+ manager. If you do not want it to be displayed (eg. you're in public), you can pipe STDOUT to a file:
208
205
 
209
- To create the config file, run this in a terminal:
206
+ bundle exec rake invar:init > master_key
210
207
 
211
- bundle exec rake invar:create:secrets
208
+ Then handle the `master_key` file as needed.
212
209
 
213
- It will print out the generated master key. Save it to a password manager.
210
+ If either a pre-existing config or secrets file is found in any of the search path locations, it will yell at you with
211
+ an `AmbiguousSourceError`.
214
212
 
215
- If you do not want it to be displayed (eg. you're in public), you can pipe it to a file:
213
+ #### Edit Config File
216
214
 
217
- bundle exec rake invar:create:secrets > master_key
215
+ To open the file your system's default text editor (eg. nano):
218
216
 
219
- Then handle the `master_key` file as needed.
217
+ bundle exec rake invar:config
220
218
 
221
219
  #### Edit Secrets File
222
220
 
223
221
  To edit the secrets file, run this and provide the file's encryption key:
224
222
 
225
- bundle exec rake invar:edit:secrets
223
+ bundle exec rake invar:secrets
226
224
 
227
- The file will be decrypted and opened in your default editor (eg. nano). Once you have exited the editor, it will be
228
- re-encrypted (remember to save, too!).
225
+ 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!
229
227
 
230
228
  ### Code
231
229
 
data/RELEASE_NOTES.md CHANGED
@@ -19,6 +19,23 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
19
19
 
20
20
  * none
21
21
 
22
+ ## [0.6.0] - 2023-05-21
23
+
24
+ ### Major Changes
25
+
26
+ * Simplified rake task syntax
27
+ * Added `rake invar:init`
28
+ * Now use `rake invar:config` and `rake invar:secrets`to edit
29
+
30
+ ### Minor Changes
31
+
32
+ * Loosened file permission restrictions to allow group access as well
33
+ * Added file permissions checking to `config.yml` and `secrets.yml`
34
+
35
+ ### Bugfixes
36
+
37
+ * none
38
+
22
39
  ## [0.5.1] - 2022-12-10
23
40
 
24
41
  ### Major Changes
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'invar/version'
4
4
  require 'invar/scope'
5
+ require 'invar/private_file'
5
6
 
6
7
  require 'yaml'
7
8
  require 'lockbox'
@@ -50,7 +51,7 @@ module Invar
50
51
  freeze
51
52
  end
52
53
 
53
- # Locates the file with the given same. You may optionally provide an extension as a second argument.
54
+ # Locates the file with the given name. You may optionally provide an extension as a second argument.
54
55
  #
55
56
  # These are equivalent:
56
57
  # find('config.yml')
@@ -58,7 +59,7 @@ module Invar
58
59
  #
59
60
  # @param [String] basename The file's basename
60
61
  # @param [String] ext the file extension, excluding the dot.
61
- # @return [Pathname] the path of the located file
62
+ # @return [PrivateFile] the path of the located file
62
63
  # @raise [AmbiguousSourceError] if the file is found in multiple locations
63
64
  # @raise [FileNotFoundError] if the file cannot be found
64
65
  def find(basename, ext = nil)
@@ -72,7 +73,7 @@ module Invar
72
73
  raise AmbiguousSourceError, "#{ msg } #{ AmbiguousSourceError::HINT }"
73
74
  end
74
75
 
75
- files.first || raise(FileNotFoundError, "Could not find #{ basename }")
76
+ PrivateFile.new(files.first || raise(FileNotFoundError, "Could not find #{ basename }"))
76
77
  end
77
78
 
78
79
  # Raised when the file cannot be found in any of the XDG search locations.
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'invar/version'
4
+ require 'invar/scope'
5
+
6
+ require 'delegate'
7
+
8
+ module Invar
9
+ # Verifies a file is secure
10
+ class PrivateFile #< SimpleDelegator
11
+ extend Forwardable
12
+ def_delegators :@delegate_sd_obj, :stat, :to_s, :basename, :==, :chmod
13
+
14
+ # Allowed permissions modes for lockfile. Readable or read-writable by the user or group only
15
+ ALLOWED_MODES = [0o600, 0o400, 0o060, 0o040].freeze
16
+
17
+ def initialize(file_path)
18
+ @delegate_sd_obj = file_path
19
+ end
20
+
21
+ def read(**args)
22
+ verify_permissions!
23
+
24
+ @delegate_sd_obj.read(**args)
25
+ end
26
+
27
+ def binread(**args)
28
+ verify_permissions!
29
+
30
+ @delegate_sd_obj.binread(**args)
31
+ end
32
+
33
+ # Raised when the file has improper permissions
34
+ class FilePermissionsError < RuntimeError
35
+ end
36
+
37
+ private
38
+
39
+ # Verifies the file has proper permissions
40
+ #
41
+ # @raise [FilePermissionsError] if the file has insecure permissions
42
+ def verify_permissions!
43
+ permissions_mask = 0o777 # only the lowest three digits are perms, so masking
44
+ # stat = @delegate_sd_obj.stat
45
+ file_mode = stat.mode & permissions_mask
46
+ # TODO: use stat.world_readable? etc instead
47
+ return if ALLOWED_MODES.include? file_mode
48
+
49
+ msg = format("File '%<path>s' has improper permissions (%<mode>04o). %<hint>s",
50
+ path: @delegate_sd_obj,
51
+ mode: file_mode,
52
+ hint: "Try: chmod 600 #{ @delegate_sd_obj }")
53
+
54
+ raise FilePermissionsError, msg
55
+ end
56
+ end
57
+ end
@@ -22,6 +22,10 @@ module Invar
22
22
  ---
23
23
  YML
24
24
 
25
+ CREATE_SUGGESTION = <<~SUGGESTION
26
+ Maybe you used the wrong namespace or need to create the file with bundle exec rake invar:init?
27
+ SUGGESTION
28
+
25
29
  # Shorthand for Invar::Rake::Tasks.new.define
26
30
  #
27
31
  # @param (see #define)
@@ -44,51 +48,57 @@ module Invar
44
48
 
45
49
  def define_all_tasks(app_namespace)
46
50
  namespace :invar do
47
- define_config_tasks(app_namespace)
48
- define_secrets_tasks(app_namespace)
51
+ define_init_task(app_namespace)
52
+
53
+ define_config_task(app_namespace)
54
+ define_secrets_task(app_namespace)
49
55
 
50
56
  define_info_tasks(app_namespace)
51
57
  end
52
58
  end
53
59
 
54
- def define_config_tasks(app_namespace)
55
- namespace :configs do
56
- desc 'Create a new configuration file'
57
- task :create do
58
- ::Invar::Rake::Tasks::ConfigTask.new(app_namespace).create
60
+ def define_init_task(app_namespace)
61
+ desc 'Create new configuration and encrypted secrets files'
62
+ task :init, [:mode] do |_task, args|
63
+ mode = args.mode
64
+
65
+ config = ::Invar::Rake::Tasks::ConfigTask.new(app_namespace)
66
+ secrets = ::Invar::Rake::Tasks::SecretTask.new(app_namespace)
67
+
68
+ case mode
69
+ when 'config'
70
+ secrets = nil
71
+ when 'secrets'
72
+ config = nil
73
+ else
74
+ raise "unknown mode #{ mode }. Must be one of 'config' or 'secrets'" unless mode.nil?
59
75
  end
60
76
 
61
- desc 'Edit the config in your default editor'
62
- task :edit do
63
- ::Invar::Rake::Tasks::ConfigTask.new(app_namespace).edit
64
- end
65
- end
77
+ assert_init_conditions(config&.file_path, secrets&.file_path)
66
78
 
67
- # alias
68
- namespace :config do
69
- task create: ['configs:create']
70
- task edit: ['configs:edit']
79
+ config&.create
80
+ secrets&.create
71
81
  end
72
82
  end
73
83
 
74
- def define_secrets_tasks(app_namespace)
75
- namespace :secrets do
76
- desc 'Create a new encrypted secrets file'
77
- task :create do
78
- ::Invar::Rake::Tasks::SecretTask.new(app_namespace).create
79
- end
80
-
81
- desc 'Edit the encrypted secrets file in your default editor'
82
- task :edit do
83
- ::Invar::Rake::Tasks::SecretTask.new(app_namespace).edit
84
- end
84
+ def define_config_task(app_namespace)
85
+ desc 'Edit the config in your default editor'
86
+ task :configs do
87
+ ::Invar::Rake::Tasks::ConfigTask.new(app_namespace).edit
85
88
  end
86
89
 
87
90
  # alias
88
- namespace :secret do
89
- task create: ['secrets:create']
90
- task edit: ['secrets:edit']
91
+ task config: ['configs']
92
+ end
93
+
94
+ def define_secrets_task(app_namespace)
95
+ desc 'Edit the encrypted secrets file in your default editor'
96
+ task :secrets do
97
+ ::Invar::Rake::Tasks::SecretTask.new(app_namespace).edit
91
98
  end
99
+
100
+ # alias
101
+ task secret: ['secrets']
92
102
  end
93
103
 
94
104
  def define_info_tasks(app_namespace)
@@ -98,32 +108,58 @@ module Invar
98
108
  end
99
109
  end
100
110
 
111
+ def assert_init_conditions(config_file, secrets_file)
112
+ return unless config_file&.exist? || secrets_file&.exist?
113
+
114
+ msg = if !config_file&.exist?
115
+ <<~MSG
116
+ Abort: Secrets file already exists (#{ secrets_file })
117
+ Run this to init only the config file: bundle exec rake tasks invar:init[config]
118
+ MSG
119
+ elsif !secrets_file&.exist?
120
+ <<~MSG
121
+ Abort: Config file already exists (#{ config_file })
122
+ Run this to init only the secrets file: bundle exec rake tasks invar:init[secrets]
123
+ MSG
124
+ else
125
+ <<~MSG
126
+ Abort: Files already exist (#{ config_file }, #{ secrets_file })
127
+ Maybe you meant to edit the file using rake tasks invar:config or invar:secrets?
128
+ MSG
129
+ end
130
+
131
+ warn msg
132
+ exit 1
133
+ end
134
+
101
135
  # Tasks that use a namespace for file searching
102
136
  class NamespacedTask
103
137
  def initialize(namespace)
104
138
  @locator = FileLocator.new(namespace)
105
139
  end
140
+
141
+ def file_path
142
+ config_dir / filename
143
+ end
144
+
145
+ private
146
+
147
+ def config_dir
148
+ @locator.search_paths.first
149
+ end
106
150
  end
107
151
 
108
152
  # Configuration file actions.
109
153
  class ConfigTask < NamespacedTask
110
154
  # Creates a config file in the appropriate location
111
155
  def create
112
- config_dir = @locator.search_paths.first
113
- config_dir.mkpath
114
-
115
- file = config_dir / 'config.yml'
116
- if file.exist?
117
- warn <<~MSG
118
- Abort: File exists. (#{ file })
119
- Maybe you meant to edit the file with bundle exec rake invar:secrets:edit?
120
- MSG
121
- exit 1
122
- end
156
+ raise 'File already exists' if file_path.exist?
123
157
 
124
- file.write CONFIG_TEMPLATE
158
+ config_dir.mkpath
159
+ file_path.write CONFIG_TEMPLATE
160
+ file_path.chmod 0o600
125
161
 
126
- warn "Created file: #{ file }"
162
+ warn "Created file: #{ file_path }"
127
163
  end
128
164
 
129
165
  # Edits the existing config file in the appropriate location
@@ -133,7 +169,7 @@ module Invar
133
169
  rescue ::Invar::FileLocator::FileNotFoundError => e
134
170
  warn <<~ERR
135
171
  Abort: #{ e.message }. Searched in: #{ @locator.search_paths.join(', ') }
136
- Maybe you used the wrong namespace or need to create the file with bundle exec rake invar:configs:create?
172
+ #{ CREATE_SUGGESTION }
137
173
  ERR
138
174
  exit 1
139
175
  end
@@ -142,6 +178,12 @@ module Invar
142
178
 
143
179
  warn "File saved to: #{ configs_file }"
144
180
  end
181
+
182
+ private
183
+
184
+ def filename
185
+ 'config.yml'
186
+ end
145
187
  end
146
188
 
147
189
  # Secrets file actions.
@@ -153,24 +195,13 @@ module Invar
153
195
 
154
196
  # Creates a new encrypted secrets file and prints the generated encryption key to STDOUT
155
197
  def create
156
- config_dir = @locator.search_paths.first
157
- config_dir.mkpath
158
-
159
- file = config_dir / 'secrets.yml'
160
-
161
- if file.exist?
162
- warn <<~ERR
163
- Abort: File exists. (#{ file })
164
- Maybe you meant to edit the file with bundle exec rake invar:secrets:edit?
165
- ERR
166
- exit 1
167
- end
198
+ raise 'File already exists' if file_path.exist?
168
199
 
169
200
  encryption_key = Lockbox.generate_key
170
201
 
171
- write_encrypted_file(file, encryption_key, SECRETS_TEMPLATE)
202
+ write_encrypted_file(file_path, encryption_key, SECRETS_TEMPLATE)
172
203
 
173
- warn "Created file #{ file }"
204
+ warn "Created file: #{ file_path }"
174
205
 
175
206
  warn SECRETS_INSTRUCTIONS
176
207
  warn 'Generated key is:'
@@ -185,7 +216,7 @@ module Invar
185
216
  rescue ::Invar::FileLocator::FileNotFoundError => e
186
217
  warn <<~ERR
187
218
  Abort: #{ e.message }. Searched in: #{ @locator.search_paths.join(', ') }
188
- Maybe you used the wrong namespace or need to create the file with bundle exec rake invar:secrets:create?
219
+ #{ CREATE_SUGGESTION }
189
220
  ERR
190
221
  exit 1
191
222
  end
@@ -197,13 +228,19 @@ module Invar
197
228
 
198
229
  private
199
230
 
231
+ def filename
232
+ 'secrets.yml'
233
+ end
234
+
200
235
  def write_encrypted_file(file_path, encryption_key, content)
201
236
  lockbox = Lockbox.new(key: encryption_key)
202
237
 
203
238
  encrypted_data = lockbox.encrypt(content)
204
239
 
240
+ config_dir.mkpath
205
241
  # TODO: replace File.opens with photo_path.binwrite(uri.data) once FakeFS can handle it
206
242
  File.open(file_path.to_s, 'wb') { |f| f.write encrypted_data }
243
+ file_path.chmod 0o600
207
244
  end
208
245
 
209
246
  def edit_encrypted_file(file_path)
@@ -251,4 +288,3 @@ module Invar
251
288
  end
252
289
  end
253
290
  end
254
-
data/lib/invar/reality.rb CHANGED
@@ -42,9 +42,6 @@ module Invar
42
42
  # @see Reality#after_load
43
43
  # @see Reality#pretend
44
44
  class Reality
45
- # Allowed permissions modes for lockfile. Readable or read-writable by the current user only
46
- ALLOWED_LOCKFILE_MODES = [0o600, 0o400].freeze
47
-
48
45
  # Name of the default key file to be searched for within config directories
49
46
  DEFAULT_KEY_FILE_NAME = 'master_key'
50
47
 
@@ -174,19 +171,6 @@ module Invar
174
171
  end
175
172
 
176
173
  def read_keyfile(key_file)
177
- permissions_mask = 0o777 # only the lowest three digits are perms, so masking
178
- stat = key_file.stat
179
- file_mode = stat.mode & permissions_mask
180
- # TODO: use stat.world_readable? etc instead
181
- unless ALLOWED_LOCKFILE_MODES.include? file_mode
182
- hint = "Try: chmod 600 #{ key_file }"
183
- raise SecretsFileDecryptionError,
184
- format("File '%<path>s' has improper permissions (%<mode>04o). %<hint>s",
185
- path: key_file,
186
- mode: file_mode,
187
- hint: hint)
188
- end
189
-
190
174
  key_file.read.strip
191
175
  end
192
176
 
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.5.1'
5
+ VERSION = '0.6.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.5.1
4
+ version: 0.6.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: 2022-12-10 00:00:00.000000000 Z
11
+ date: 2023-05-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dry-schema
@@ -144,6 +144,7 @@ files:
144
144
  - invar.gemspec
145
145
  - lib/invar.rb
146
146
  - lib/invar/file_locator.rb
147
+ - lib/invar/private_file.rb
147
148
  - lib/invar/rake/tasks.rb
148
149
  - lib/invar/reality.rb
149
150
  - lib/invar/scope.rb