invar 0.8.0 → 0.9.1
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/.simplecov +8 -0
- data/README.md +27 -2
- data/RELEASE_NOTES.md +30 -0
- data/lib/invar/private_file.rb +1 -1
- data/lib/invar/rake/task/config.rb +51 -0
- data/lib/invar/rake/task/namespaced.rb +26 -0
- data/lib/invar/rake/task/secrets.rb +130 -0
- data/lib/invar/rake/task/status.rb +17 -0
- data/lib/invar/rake/tasks.rb +33 -178
- data/lib/invar/reality.rb +2 -4
- data/lib/invar/scope.rb +12 -2
- data/lib/invar/version.rb +1 -1
- data/lib/invar.rb +20 -4
- metadata +7 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2346259fc26689ffca5386f39e982de60dc9f2d9c03399f85023e05a47802f7a
|
4
|
+
data.tar.gz: b6e2cea0c52fd0b52eb833036844c03d3a714d83e0572dc5dcbb4ae083c61cf7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1a9556b30d2c17fda9373551ecf6fffeaacabd35aa0e40bb883c78e8bd94642bf4bebbeea054b5f9a9681b0bb23b6ee8c30860afb732f94b9d7887e62e0c0c47
|
7
|
+
data.tar.gz: a80954a8dbfc4c42d3f57b94cb2830a8e598eda85cd95f37ff431a1ab2b253fa64d4c65f9aacba8cfa718f5b5f1082b05f78c0d1f97d2ae7bc58b4392c4e19f0
|
data/.simplecov
ADDED
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
|
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,36 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
19
19
|
|
20
20
|
* none
|
21
21
|
|
22
|
+
## [0.9.1] - 2023-09-30
|
23
|
+
|
24
|
+
### Major Changes
|
25
|
+
|
26
|
+
* none
|
27
|
+
|
28
|
+
### Minor Changes
|
29
|
+
|
30
|
+
* none
|
31
|
+
|
32
|
+
### Bugfixes
|
33
|
+
|
34
|
+
* Improved handling of test-only methods when trying to access them via `#method`
|
35
|
+
* Fixed TTY detection
|
36
|
+
|
37
|
+
## [0.9.0] - 2023-09-24
|
38
|
+
|
39
|
+
### Major Changes
|
40
|
+
|
41
|
+
* none
|
42
|
+
|
43
|
+
### Minor Changes
|
44
|
+
|
45
|
+
* Added Rake task for secrets file key rotation
|
46
|
+
* Added ability for invar:config and invar:secrets to take replacement content over `STDIN` instead of live editing
|
47
|
+
|
48
|
+
### Bugfixes
|
49
|
+
|
50
|
+
* none
|
51
|
+
|
22
52
|
## [0.8.0] - 2023-09-13
|
23
53
|
|
24
54
|
### Major Changes
|
data/lib/invar/private_file.rb
CHANGED
@@ -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
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'namespaced'
|
4
|
+
|
5
|
+
module Invar
|
6
|
+
module Rake
|
7
|
+
module Task
|
8
|
+
# Rake task handler for actions to do with configuration files
|
9
|
+
class ConfigFileHandler < NamespacedFileTask
|
10
|
+
# Creates a config file in the appropriate location
|
11
|
+
def create
|
12
|
+
config_dir.mkpath
|
13
|
+
file_path.write CONFIG_TEMPLATE
|
14
|
+
file_path.chmod 0o600
|
15
|
+
|
16
|
+
warn "Created file: #{ file_path }"
|
17
|
+
end
|
18
|
+
|
19
|
+
# Edits the existing config file in the appropriate location
|
20
|
+
def edit
|
21
|
+
content = $stdin.stat.pipe? ? $stdin.read : nil
|
22
|
+
file_path = configs_file
|
23
|
+
|
24
|
+
if content
|
25
|
+
file_path.write content
|
26
|
+
else
|
27
|
+
system ENV.fetch('EDITOR', 'editor'), file_path.to_s, exception: true
|
28
|
+
end
|
29
|
+
|
30
|
+
warn "File saved to: #{ file_path }"
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def configs_file
|
36
|
+
@locator.find 'config.yml'
|
37
|
+
rescue ::Invar::FileLocator::FileNotFoundError => e
|
38
|
+
warn <<~ERR
|
39
|
+
Abort: #{ e.message }. Searched in: #{ @locator.search_paths.join(', ') }
|
40
|
+
#{ CREATE_SUGGESTION }
|
41
|
+
ERR
|
42
|
+
exit 1
|
43
|
+
end
|
44
|
+
|
45
|
+
def filename
|
46
|
+
'config.yml'
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../tasks'
|
4
|
+
|
5
|
+
module Invar
|
6
|
+
module Rake
|
7
|
+
module Task
|
8
|
+
# Abstract class for tasks that use a namespace for file searching
|
9
|
+
class NamespacedFileTask
|
10
|
+
def initialize(namespace)
|
11
|
+
@locator = FileLocator.new(namespace)
|
12
|
+
end
|
13
|
+
|
14
|
+
def file_path
|
15
|
+
config_dir / filename
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def config_dir
|
21
|
+
@locator.search_paths.first
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'namespaced'
|
4
|
+
|
5
|
+
module Invar
|
6
|
+
module Rake
|
7
|
+
module Task
|
8
|
+
# Rake task handler for actions on the secrets file.
|
9
|
+
class SecretsFileHandler < NamespacedFileTask
|
10
|
+
# Instructions hint for how to handle secret keys.
|
11
|
+
SECRETS_INSTRUCTIONS = <<~INST
|
12
|
+
Generated key. Save this key to a secure password manager, you will need it to edit the secrets.yml file:
|
13
|
+
INST
|
14
|
+
|
15
|
+
SWAP_EXT = 'tmp'
|
16
|
+
|
17
|
+
# Creates a new encrypted secrets file and prints the generated encryption key to STDOUT
|
18
|
+
def create(content: SECRETS_TEMPLATE)
|
19
|
+
encryption_key = Lockbox.generate_key
|
20
|
+
|
21
|
+
write_encrypted_file(file_path,
|
22
|
+
encryption_key: encryption_key,
|
23
|
+
content: content,
|
24
|
+
permissions: PrivateFile::DEFAULT_PERMISSIONS)
|
25
|
+
|
26
|
+
warn SECRETS_INSTRUCTIONS
|
27
|
+
puts encryption_key
|
28
|
+
end
|
29
|
+
|
30
|
+
# Updates the file with new content.
|
31
|
+
#
|
32
|
+
# Either the content is provided over STDIN or the default editor is opened with the decrypted contents of
|
33
|
+
# the secrets file. After closing the editor, the file will be updated with the new encrypted contents.
|
34
|
+
def edit
|
35
|
+
content = $stdin.stat.pipe? ? $stdin.read : nil
|
36
|
+
|
37
|
+
edit_encrypted_file(secrets_file, content: content)
|
38
|
+
|
39
|
+
warn "File saved to #{ secrets_file }"
|
40
|
+
end
|
41
|
+
|
42
|
+
def rotate
|
43
|
+
file_path = secrets_file
|
44
|
+
|
45
|
+
decrypted = read_encrypted_file(file_path, encryption_key: determine_key(file_path))
|
46
|
+
|
47
|
+
swap_file = file_path.dirname / [file_path.basename, SWAP_EXT].join('.')
|
48
|
+
file_path.rename swap_file
|
49
|
+
|
50
|
+
begin
|
51
|
+
create content: decrypted
|
52
|
+
swap_file.delete
|
53
|
+
rescue StandardError
|
54
|
+
swap_file.rename file_path.to_s
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def secrets_file
|
61
|
+
@locator.find 'secrets.yml'
|
62
|
+
rescue ::Invar::FileLocator::FileNotFoundError => e
|
63
|
+
warn <<~ERR
|
64
|
+
Abort: #{ e.message }. Searched in: #{ @locator.search_paths.join(', ') }
|
65
|
+
#{ CREATE_SUGGESTION }
|
66
|
+
ERR
|
67
|
+
exit 1
|
68
|
+
end
|
69
|
+
|
70
|
+
def filename
|
71
|
+
'secrets.yml'
|
72
|
+
end
|
73
|
+
|
74
|
+
def read_encrypted_file(file_path, encryption_key:)
|
75
|
+
lockbox = build_lockbox(encryption_key)
|
76
|
+
lockbox.decrypt(file_path.binread)
|
77
|
+
end
|
78
|
+
|
79
|
+
def write_encrypted_file(file_path, encryption_key:, content:, permissions: nil)
|
80
|
+
lockbox = build_lockbox(encryption_key)
|
81
|
+
|
82
|
+
encrypted_data = lockbox.encrypt(content)
|
83
|
+
|
84
|
+
config_dir.mkpath
|
85
|
+
# TODO: replace File.opens with photo_path.binwrite(uri.data) once FakeFS can handle it
|
86
|
+
File.open(file_path.to_s, 'wb') { |f| f.write encrypted_data }
|
87
|
+
file_path.chmod permissions if permissions
|
88
|
+
|
89
|
+
warn "Saved file: #{ file_path }"
|
90
|
+
end
|
91
|
+
|
92
|
+
def edit_encrypted_file(file_path, content: nil)
|
93
|
+
encryption_key = determine_key(file_path)
|
94
|
+
|
95
|
+
content ||= invoke_editor(file_path, encryption_key: encryption_key)
|
96
|
+
|
97
|
+
write_encrypted_file(file_path, encryption_key: encryption_key, content: content)
|
98
|
+
end
|
99
|
+
|
100
|
+
def invoke_editor(file_path, encryption_key:)
|
101
|
+
Tempfile.create(file_path.basename.to_s) do |tmp_file|
|
102
|
+
decrypted = read_encrypted_file(file_path, encryption_key: encryption_key)
|
103
|
+
|
104
|
+
tmp_file.write(decrypted)
|
105
|
+
tmp_file.rewind # rewind needed because file does not get closed after write
|
106
|
+
system ENV.fetch('EDITOR', 'editor'), tmp_file.path, exception: true
|
107
|
+
tmp_file.read
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def determine_key(file_path)
|
112
|
+
encryption_key = Lockbox.master_key
|
113
|
+
|
114
|
+
if encryption_key.nil? && $stdin.tty?
|
115
|
+
warn "Enter master key to decrypt #{ file_path }:"
|
116
|
+
encryption_key = $stdin.noecho(&:gets).strip
|
117
|
+
end
|
118
|
+
|
119
|
+
encryption_key
|
120
|
+
end
|
121
|
+
|
122
|
+
def build_lockbox(encryption_key)
|
123
|
+
Lockbox.new(key: encryption_key)
|
124
|
+
rescue ArgumentError => e
|
125
|
+
raise SecretsFileEncryptionError, e
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'namespaced'
|
4
|
+
|
5
|
+
module Invar
|
6
|
+
module Rake
|
7
|
+
module Task
|
8
|
+
# Rake task handler for actions that just show information about the system
|
9
|
+
class StatusHandler < NamespacedFileTask
|
10
|
+
# Prints the current paths to be searched in
|
11
|
+
def show_paths
|
12
|
+
warn @locator.search_paths.join("\n")
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/invar/rake/tasks.rb
CHANGED
@@ -3,29 +3,24 @@
|
|
3
3
|
require 'invar'
|
4
4
|
|
5
5
|
require 'rake'
|
6
|
-
require 'io/console'
|
7
6
|
require 'tempfile'
|
8
7
|
|
8
|
+
require_relative 'task/config'
|
9
|
+
require_relative 'task/secrets'
|
10
|
+
require_relative 'task/status'
|
11
|
+
|
9
12
|
module Invar
|
10
|
-
# Rake task
|
13
|
+
# Rake task module for Invar-related tasks.
|
14
|
+
#
|
15
|
+
# The specific rake task implementations are delegated to handlers in Invar::Rake::Task
|
11
16
|
#
|
12
|
-
#
|
13
|
-
# path is nice and short)
|
17
|
+
# @see Invar::Rake::Tasks.define
|
14
18
|
module Rake
|
15
19
|
# RakeTask builder class. Use Tasks.define to generate the needed tasks.
|
16
20
|
class Tasks
|
17
21
|
include ::Rake::Cloneable
|
18
22
|
include ::Rake::DSL
|
19
23
|
|
20
|
-
# Template config YAML file
|
21
|
-
CONFIG_TEMPLATE = SECRETS_TEMPLATE = <<~YML
|
22
|
-
---
|
23
|
-
YML
|
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
|
-
|
29
24
|
# Shorthand for Invar::Rake::Tasks.new.define
|
30
25
|
#
|
31
26
|
# @param (see #define)
|
@@ -51,7 +46,7 @@ module Invar
|
|
51
46
|
define_init_task(app_namespace)
|
52
47
|
|
53
48
|
define_config_task(app_namespace)
|
54
|
-
|
49
|
+
define_secrets_tasks(app_namespace)
|
55
50
|
|
56
51
|
define_info_tasks(app_namespace)
|
57
52
|
end
|
@@ -62,8 +57,8 @@ module Invar
|
|
62
57
|
task :init, [:mode] do |_task, args|
|
63
58
|
mode = args.mode
|
64
59
|
|
65
|
-
config = ::Invar::Rake::
|
66
|
-
secrets = ::Invar::Rake::
|
60
|
+
config = ::Invar::Rake::Task::ConfigFileHandler.new(app_namespace)
|
61
|
+
secrets = ::Invar::Rake::Task::SecretsFileHandler.new(app_namespace)
|
67
62
|
|
68
63
|
case mode
|
69
64
|
when 'config'
|
@@ -71,7 +66,7 @@ module Invar
|
|
71
66
|
when 'secrets'
|
72
67
|
config = nil
|
73
68
|
else
|
74
|
-
raise "unknown mode #{ mode }. Must be one of 'config' or 'secrets'" unless mode.nil?
|
69
|
+
raise ArgumentError, "unknown mode '#{ mode }'. Must be one of 'config' or 'secrets'" unless mode.nil?
|
75
70
|
end
|
76
71
|
|
77
72
|
assert_init_conditions(config&.file_path, secrets&.file_path)
|
@@ -84,39 +79,44 @@ module Invar
|
|
84
79
|
def define_config_task(app_namespace)
|
85
80
|
desc 'Edit the config in your default editor'
|
86
81
|
task :configs do
|
87
|
-
::Invar::Rake::
|
82
|
+
::Invar::Rake::Task::ConfigFileHandler.new(app_namespace).edit
|
88
83
|
end
|
89
84
|
|
90
85
|
# alias
|
91
86
|
task config: ['configs']
|
92
87
|
end
|
93
88
|
|
94
|
-
def
|
89
|
+
def define_secrets_tasks(app_namespace)
|
95
90
|
desc 'Edit the encrypted secrets file in your default editor'
|
96
91
|
task :secrets do
|
97
|
-
::Invar::Rake::
|
92
|
+
::Invar::Rake::Task::SecretsFileHandler.new(app_namespace).edit
|
98
93
|
end
|
99
94
|
|
100
95
|
# alias
|
101
96
|
task secret: ['secrets']
|
97
|
+
|
98
|
+
desc 'Encrypt the secrets file with a new generated key'
|
99
|
+
task :rotate do
|
100
|
+
::Invar::Rake::Task::SecretsFileHandler.new(app_namespace).rotate
|
101
|
+
end
|
102
102
|
end
|
103
103
|
|
104
104
|
def define_info_tasks(app_namespace)
|
105
105
|
desc 'Show directories to be searched for the given namespace'
|
106
106
|
task :paths do
|
107
|
-
::Invar::Rake::
|
107
|
+
::Invar::Rake::Task::StatusHandler.new(app_namespace).show_paths
|
108
108
|
end
|
109
109
|
end
|
110
110
|
|
111
111
|
def assert_init_conditions(config_file, secrets_file)
|
112
112
|
return unless config_file&.exist? || secrets_file&.exist?
|
113
113
|
|
114
|
-
msg = if !config_file
|
114
|
+
msg = if !config_file.exist?
|
115
115
|
<<~MSG
|
116
116
|
Abort: Secrets file already exists (#{ secrets_file })
|
117
117
|
Run this to init only the config file: bundle exec rake tasks invar:init[config]
|
118
118
|
MSG
|
119
|
-
elsif !secrets_file
|
119
|
+
elsif !secrets_file.exist?
|
120
120
|
<<~MSG
|
121
121
|
Abort: Config file already exists (#{ config_file })
|
122
122
|
Run this to init only the secrets file: bundle exec rake tasks invar:init[secrets]
|
@@ -131,163 +131,18 @@ module Invar
|
|
131
131
|
warn msg
|
132
132
|
exit 1
|
133
133
|
end
|
134
|
+
end
|
134
135
|
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
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
|
150
|
-
end
|
151
|
-
|
152
|
-
# Configuration file actions.
|
153
|
-
class ConfigTask < NamespacedTask
|
154
|
-
# Creates a config file in the appropriate location
|
155
|
-
def create
|
156
|
-
raise 'File already exists' if file_path.exist?
|
157
|
-
|
158
|
-
config_dir.mkpath
|
159
|
-
file_path.write CONFIG_TEMPLATE
|
160
|
-
file_path.chmod 0o600
|
161
|
-
|
162
|
-
warn "Created file: #{ file_path }"
|
163
|
-
end
|
164
|
-
|
165
|
-
# Edits the existing config file in the appropriate location
|
166
|
-
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 }"
|
180
|
-
end
|
181
|
-
|
182
|
-
private
|
183
|
-
|
184
|
-
def filename
|
185
|
-
'config.yml'
|
186
|
-
end
|
187
|
-
end
|
188
|
-
|
189
|
-
# Secrets file actions.
|
190
|
-
class SecretTask < NamespacedTask
|
191
|
-
# Instructions hint for how to handle secret keys.
|
192
|
-
SECRETS_INSTRUCTIONS = <<~INST
|
193
|
-
Save this key to a secure password manager. You will need it to edit the secrets.yml file.
|
194
|
-
INST
|
195
|
-
|
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?
|
199
|
-
|
200
|
-
encryption_key = Lockbox.generate_key
|
201
|
-
|
202
|
-
write_encrypted_file(file_path,
|
203
|
-
encryption_key: encryption_key,
|
204
|
-
content: SECRETS_TEMPLATE,
|
205
|
-
permissions: PrivateFile::DEFAULT_PERMISSIONS)
|
206
|
-
|
207
|
-
warn "Created file: #{ file_path }"
|
208
|
-
|
209
|
-
warn SECRETS_INSTRUCTIONS
|
210
|
-
warn 'Generated key is:'
|
211
|
-
puts encryption_key
|
212
|
-
end
|
213
|
-
|
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.
|
216
|
-
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)
|
228
|
-
|
229
|
-
warn "File saved to #{ secrets_file }"
|
230
|
-
end
|
231
|
-
|
232
|
-
private
|
233
|
-
|
234
|
-
def filename
|
235
|
-
'secrets.yml'
|
236
|
-
end
|
237
|
-
|
238
|
-
def write_encrypted_file(file_path, encryption_key:, content:, permissions: nil)
|
239
|
-
lockbox = Lockbox.new(key: encryption_key)
|
240
|
-
|
241
|
-
encrypted_data = lockbox.encrypt(content)
|
242
|
-
|
243
|
-
config_dir.mkpath
|
244
|
-
# TODO: replace File.opens with photo_path.binwrite(uri.data) once FakeFS can handle it
|
245
|
-
File.open(file_path.to_s, 'wb') { |f| f.write encrypted_data }
|
246
|
-
file_path.chmod permissions if permissions
|
247
|
-
end
|
248
|
-
|
249
|
-
def edit_encrypted_file(file_path)
|
250
|
-
encryption_key = determine_key(file_path)
|
251
|
-
|
252
|
-
lockbox = build_lockbox(encryption_key)
|
253
|
-
|
254
|
-
file_str = Tempfile.create(file_path.basename.to_s) do |tmp_file|
|
255
|
-
decrypted = lockbox.decrypt(file_path.binread)
|
256
|
-
|
257
|
-
tmp_file.write(decrypted)
|
258
|
-
tmp_file.rewind # rewind needed because file does not get closed after write
|
259
|
-
system(ENV.fetch('EDITOR', 'editor'), tmp_file.path, exception: true)
|
260
|
-
tmp_file.read
|
261
|
-
end
|
262
|
-
|
263
|
-
write_encrypted_file(file_path, encryption_key: encryption_key, content: file_str)
|
264
|
-
end
|
265
|
-
|
266
|
-
def determine_key(file_path)
|
267
|
-
encryption_key = Lockbox.master_key
|
268
|
-
|
269
|
-
if encryption_key.nil? && $stdin.respond_to?(:noecho)
|
270
|
-
warn "Enter master key to decrypt #{ file_path }:"
|
271
|
-
encryption_key = $stdin.noecho(&:gets).strip
|
272
|
-
end
|
273
|
-
|
274
|
-
encryption_key
|
275
|
-
end
|
276
|
-
|
277
|
-
def build_lockbox(encryption_key)
|
278
|
-
Lockbox.new(key: encryption_key)
|
279
|
-
rescue ArgumentError => e
|
280
|
-
raise SecretsFileEncryptionError, e
|
281
|
-
end
|
282
|
-
end
|
136
|
+
# Namespace module for task handler implementations
|
137
|
+
module Task
|
138
|
+
# Template config YAML file
|
139
|
+
CONFIG_TEMPLATE = SECRETS_TEMPLATE = <<~YML
|
140
|
+
---
|
141
|
+
YML
|
283
142
|
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
def show_paths
|
288
|
-
warn @locator.search_paths.join("\n")
|
289
|
-
end
|
290
|
-
end
|
143
|
+
CREATE_SUGGESTION = <<~SUGGESTION
|
144
|
+
Maybe you used the wrong namespace or need to create the file with bundle exec rake invar:init?
|
145
|
+
SUGGESTION
|
291
146
|
end
|
292
147
|
end
|
293
148
|
end
|
data/lib/invar/reality.rb
CHANGED
@@ -152,11 +152,9 @@ module Invar
|
|
152
152
|
end
|
153
153
|
|
154
154
|
def resolve_key(pathname, locator, prompt)
|
155
|
-
|
156
|
-
|
157
|
-
read_keyfile(key_file)
|
155
|
+
read_keyfile locator.find pathname
|
158
156
|
rescue FileLocator::FileNotFoundError
|
159
|
-
if $stdin.
|
157
|
+
if $stdin.tty?
|
160
158
|
warn prompt
|
161
159
|
$stdin.noecho(&:gets).strip
|
162
160
|
else
|
data/lib/invar/scope.rb
CHANGED
@@ -25,12 +25,18 @@ module Invar
|
|
25
25
|
alias / fetch
|
26
26
|
alias [] fetch
|
27
27
|
|
28
|
-
def method_missing(
|
29
|
-
|
28
|
+
def method_missing(method_name, *args)
|
29
|
+
guard_test_methods method_name
|
30
30
|
|
31
31
|
super
|
32
32
|
end
|
33
33
|
|
34
|
+
def respond_to_missing?(method_name, include_all)
|
35
|
+
guard_test_methods method_name
|
36
|
+
|
37
|
+
super method_name, include_all
|
38
|
+
end
|
39
|
+
|
34
40
|
# Returns a hash representation of this scope and subscopes.
|
35
41
|
#
|
36
42
|
# @return [Hash] a hash representation of this scope
|
@@ -51,6 +57,10 @@ module Invar
|
|
51
57
|
|
52
58
|
private
|
53
59
|
|
60
|
+
def guard_test_methods(method_name)
|
61
|
+
raise ::Invar::ImmutableRealityError, ::Invar::ImmutableRealityError::PRETEND_MSG if method_name == :pretend
|
62
|
+
end
|
63
|
+
|
54
64
|
def known_keys
|
55
65
|
@data.keys.sort.collect { |k| ":#{ k }" }.join(', ')
|
56
66
|
end
|
data/lib/invar/version.rb
CHANGED
data/lib/invar.rb
CHANGED
@@ -1,5 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
# Needed for TTY input handling
|
4
|
+
# Do not remove; it missing will not always break tests due to test environments requiring it themselves
|
5
|
+
require 'io/console'
|
6
|
+
|
3
7
|
require_relative 'invar/version'
|
4
8
|
require_relative 'invar/errors'
|
5
9
|
require_relative 'invar/reality'
|
@@ -15,12 +19,24 @@ module Invar
|
|
15
19
|
end
|
16
20
|
|
17
21
|
class << self
|
18
|
-
def method_missing(
|
19
|
-
|
20
|
-
raise ::Invar::ImmutableRealityError, ::Invar::ImmutableRealityError::HOOK_MSG
|
21
|
-
end
|
22
|
+
def method_missing(method_name)
|
23
|
+
guard_test_hooks method_name
|
22
24
|
|
23
25
|
super
|
24
26
|
end
|
27
|
+
|
28
|
+
def respond_to_missing?(method_name, include_all)
|
29
|
+
guard_test_hooks method_name
|
30
|
+
|
31
|
+
super method_name, include_all
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def guard_test_hooks(method_name)
|
37
|
+
return unless [:after_load, :clear_hooks].include? method_name
|
38
|
+
|
39
|
+
raise ::Invar::ImmutableRealityError, ::Invar::ImmutableRealityError::HOOK_MSG
|
40
|
+
end
|
25
41
|
end
|
26
42
|
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.
|
4
|
+
version: 0.9.1
|
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-
|
11
|
+
date: 2023-09-30 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
|
@@ -62,6 +63,10 @@ files:
|
|
62
63
|
- lib/invar/errors.rb
|
63
64
|
- lib/invar/file_locator.rb
|
64
65
|
- lib/invar/private_file.rb
|
66
|
+
- lib/invar/rake/task/config.rb
|
67
|
+
- lib/invar/rake/task/namespaced.rb
|
68
|
+
- lib/invar/rake/task/secrets.rb
|
69
|
+
- lib/invar/rake/task/status.rb
|
65
70
|
- lib/invar/rake/tasks.rb
|
66
71
|
- lib/invar/reality.rb
|
67
72
|
- lib/invar/scope.rb
|