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 +4 -4
- data/.rubocop.yml +1 -1
- data/README.md +23 -25
- data/RELEASE_NOTES.md +17 -0
- data/lib/invar/file_locator.rb +4 -3
- data/lib/invar/private_file.rb +57 -0
- data/lib/invar/rake/tasks.rb +96 -60
- data/lib/invar/reality.rb +0 -16
- data/lib/invar/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 881d31e6095f54b8a1167b7e06324397dcd823098fa25846a49cbe085679ee81
|
4
|
+
data.tar.gz: c6c4732702207659323642ef6522c0bd4ba3b4fde7054c9f9eee7a3baa9e5cea
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 167260ced258a4792560570c395a17267f5ffe4b78ffda5b40804b018a6e1ee5697b1389fa5d631048ac9c62f89a2e988b038e3d02b9b214c9682447a0d01b2c
|
7
|
+
data.tar.gz: 2d49307b7c02060d112dbd8d703a21b13bc1d7b80836c24012680dd3c737c715a0784e3402cbcf341f481091584562e1e8be3fc519f503f67de6fdd41d9c7e43
|
data/.rubocop.yml
CHANGED
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
|
151
|
+
# Your application uses Invar as normal:
|
152
152
|
require 'invar'
|
153
153
|
|
154
154
|
invar = Invar.new(namespace: 'my-app')
|
155
155
|
|
156
|
-
# ...
|
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
|
160
|
-
# like Cucumber's `BeforeAll` or RSpec's `before(:all)`
|
161
|
-
invar[:config][:
|
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][:
|
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 / :
|
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
|
197
|
+
#### Create Config and Secrets Files
|
198
198
|
|
199
|
-
|
199
|
+
To create a `config.yml` file and an encrypted `secrets.yml` file:
|
200
200
|
|
201
|
-
|
201
|
+
bundle exec rake invar:init
|
202
202
|
|
203
|
-
|
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
|
-
|
206
|
+
bundle exec rake invar:init > master_key
|
210
207
|
|
211
|
-
|
208
|
+
Then handle the `master_key` file as needed.
|
212
209
|
|
213
|
-
|
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
|
-
|
213
|
+
#### Edit Config File
|
216
214
|
|
217
|
-
|
215
|
+
To open the file your system's default text editor (eg. nano):
|
218
216
|
|
219
|
-
|
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:
|
223
|
+
bundle exec rake invar:secrets
|
226
224
|
|
227
|
-
The file will be decrypted and opened in your default editor
|
228
|
-
re-encrypted
|
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
|
data/lib/invar/file_locator.rb
CHANGED
@@ -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
|
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 [
|
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
|
data/lib/invar/rake/tasks.rb
CHANGED
@@ -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
|
-
|
48
|
-
|
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
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
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
|
-
|
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
|
-
|
68
|
-
|
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
|
75
|
-
|
76
|
-
|
77
|
-
|
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
|
-
|
89
|
-
|
90
|
-
|
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
|
-
|
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
|
-
|
158
|
+
config_dir.mkpath
|
159
|
+
file_path.write CONFIG_TEMPLATE
|
160
|
+
file_path.chmod 0o600
|
125
161
|
|
126
|
-
warn "Created 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
|
-
|
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
|
-
|
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(
|
202
|
+
write_encrypted_file(file_path, encryption_key, SECRETS_TEMPLATE)
|
172
203
|
|
173
|
-
warn "Created 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
|
-
|
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
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.
|
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:
|
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
|