invar 0.5.0 → 0.6.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: 6e3d07cd5d6d704c0bec06fe0803e0a5fd553f69e54f74501ddd7e9639353172
4
- data.tar.gz: 3ecb948cb3b856d9f15d0d1d985f5e59db62532a724e8c4c748287532d1efac2
3
+ metadata.gz: 881d31e6095f54b8a1167b7e06324397dcd823098fa25846a49cbe085679ee81
4
+ data.tar.gz: c6c4732702207659323642ef6522c0bd4ba3b4fde7054c9f9eee7a3baa9e5cea
5
5
  SHA512:
6
- metadata.gz: 66ebee132d5bc1ba58e5876594435514d19b177fda6820d1ebe05ad6908b590f48b4466c30af950127cf3178a43eeb74573535fcfd32aa9f2165e378b657f4be
7
- data.tar.gz: 3972595f9f126f76707b1bf6c2d76ef6803d5056808cf06977106256524be109ed9b0975a99931cb9acf694d9e1fb934916f5de0939e6dc65fc920e61c412195
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
@@ -9,6 +9,51 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
9
9
 
10
10
  ### Major Changes
11
11
 
12
+ * none
13
+
14
+ ### Minor Changes
15
+
16
+ * none
17
+
18
+ ### Bugfixes
19
+
20
+ * none
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
+
39
+ ## [0.5.1] - 2022-12-10
40
+
41
+ ### Major Changes
42
+
43
+ *none
44
+
45
+ ### Minor Changes
46
+
47
+ * none
48
+
49
+ ### Bugfixes
50
+
51
+ * Fixed error message that showed blank filename when looking for lockbox master keyfile
52
+
53
+ ## [0.5.0] - 2022-12-09
54
+
55
+ ### Major Changes
56
+
12
57
  * Renamed `Invar::Invar` to `Invar::Reality`
13
58
  * Maintenance Rake task inclusion now requires explicit define call
14
59
 
@@ -50,7 +95,7 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
50
95
 
51
96
  * none
52
97
 
53
- ## [0.2.0] - Unreleased beta
98
+ ## [0.2.0] - Unreleased Prototype
54
99
 
55
100
  ### Major Changes
56
101
 
@@ -66,7 +111,7 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66
111
 
67
112
  * none
68
113
 
69
- ## [0.1.0] - Unreleased beta
114
+ ## [0.1.0] - Unreleased Prototype
70
115
 
71
116
  ### Major Changes
72
117
 
@@ -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
 
@@ -79,7 +76,7 @@ module Invar
79
76
  end
80
77
 
81
78
  begin
82
- @secrets = Scope.new(load_secrets(locator, decryption_keyfile))
79
+ @secrets = Scope.new(load_secrets(locator, decryption_keyfile || DEFAULT_KEY_FILE_NAME))
83
80
  rescue FileLocator::FileNotFoundError
84
81
  raise MissingSecretsFileError,
85
82
  "No secrets file found. Create encrypted secrets.yml in one of these locations: #{ search_paths }"
@@ -160,7 +157,7 @@ module Invar
160
157
  end
161
158
 
162
159
  def resolve_key(pathname, locator, prompt)
163
- key_file = locator.find(pathname || DEFAULT_KEY_FILE_NAME)
160
+ key_file = locator.find(pathname)
164
161
 
165
162
  read_keyfile(key_file)
166
163
  rescue FileLocator::FileNotFoundError
@@ -169,24 +166,11 @@ module Invar
169
166
  $stdin.noecho(&:gets).strip
170
167
  else
171
168
  raise SecretsFileDecryptionError,
172
- "Could not find file '#{ pathname }'. Searched in: #{ locator.search_paths }"
169
+ "Could not find file '#{ pathname }'. Searched in: #{ locator.search_paths.join(', ') }"
173
170
  end
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.0'
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.0
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