invar 0.5.1 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|