invar 0.6.1 → 0.7.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: 1f8761298571a16c1752a4413d4288095301381a3a3ad323ea9e0cf75c48a5f7
4
- data.tar.gz: b5a573c3eb0b93e3774a504fc467f9349eb242979a900d3b6ac463d609d75257
3
+ metadata.gz: 62fcbaf0763469c256af4915bdae35ded8a2691c647356a24bd6e68635bf3030
4
+ data.tar.gz: a54dce1295a10292f208318faeefe43b0c279934154f2c6f112e5b22278126a6
5
5
  SHA512:
6
- metadata.gz: a7f52110ae1fe1580047d74b957c6da15208a82faec856d795177deb3f745778ef8f79d2a05e422e54ba06b2ea581978783e773d9eafb52e23e29ca1d2948b06
7
- data.tar.gz: 0c35ac70036bc6c28e1444d6136fd27621d281d0ec54ba6e2c3b26a93e393eb1ce711226c081d8fba0178497fc869aea81232059746ba7afd91cddb9117cd4c1
6
+ metadata.gz: 5c0d9b6ae497f3e1ca0ba6701cdc99c1b8408ceabcdfd1970b67c465576f2cd0439caf92fc6413eb40db21d7289d5f5cab495adeb3499bdeef3c3a4da43a0e2b
7
+ data.tar.gz: 206b350e643afabd1892db2cf393b375e7555fe85f65d56705c718757ce172d4360fb9f6c4102b70da64223ee42a9d0ef960298240bce9e755b91512f7d1406a
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- ruby-2.7.1
1
+ ruby-2.7.8
data/Gemfile CHANGED
@@ -2,5 +2,14 @@
2
2
 
3
3
  source 'https://rubygems.org'
4
4
 
5
+ group :development do
6
+ gem 'bundler', '~> 2.3'
7
+ gem 'fakefs', '~> 2.5'
8
+ gem 'rake', '~> 13.0'
9
+ gem 'rspec', '~> 3.12'
10
+ gem 'simplecov', '~> 0.22'
11
+ gem 'yard', '~> 0.9'
12
+ end
13
+
5
14
  # Specify your gem's dependencies in invar.gemspec
6
15
  gemspec
data/RELEASE_NOTES.md CHANGED
@@ -13,11 +13,13 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
13
13
 
14
14
  ### Minor Changes
15
15
 
16
- * none
16
+ * Tweaked file missing error messages
17
+ * Extracted testing helper features into a separate module
18
+ * Added support for multiple after_load hooks
17
19
 
18
20
  ### Bugfixes
19
21
 
20
- * none
22
+ * No longer attempts to set secrets file permissions on every edit
21
23
 
22
24
  ## [0.6.1] - 2023-05-22
23
25
 
@@ -31,7 +33,7 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
31
33
 
32
34
  ### Bugfixes
33
35
 
34
- * Fixed minor logic error in permissions checking, improved testing
36
+ * Fixed minor logic error in permissions checking, improved testing
35
37
 
36
38
  ## [0.6.0] - 2023-05-21
37
39
 
data/invar.gemspec CHANGED
@@ -31,11 +31,4 @@ Gem::Specification.new do |spec|
31
31
 
32
32
  spec.add_dependency 'dry-schema', '>= 1.0'
33
33
  spec.add_dependency 'lockbox', '>= 1.0'
34
-
35
- spec.add_development_dependency 'bundler', '~> 2.3'
36
- spec.add_development_dependency 'fakefs', '~> 1.9'
37
- spec.add_development_dependency 'rake', '~> 13.0'
38
- spec.add_development_dependency 'rspec', '~> 3.12'
39
- spec.add_development_dependency 'simplecov', '~> 0.21'
40
- spec.add_development_dependency 'yard', '~> 0.9'
41
34
  end
@@ -0,0 +1,51 @@
1
+ module Invar
2
+
3
+ # Raised when no config file can be found within the search paths.
4
+ class MissingConfigFileError < RuntimeError
5
+ end
6
+
7
+ # Raised when no secrets file can be found within the search paths.
8
+ class MissingSecretsFileError < RuntimeError
9
+ end
10
+
11
+ # Raised when an error is encountered during secrets file encryption
12
+ class SecretsFileEncryptionError < RuntimeError
13
+ end
14
+
15
+ # Raised when an error is encountered during secrets file decryption
16
+ class SecretsFileDecryptionError < RuntimeError
17
+ end
18
+
19
+ # Raised when a key is defined in both the environment and the configuration file.
20
+ class EnvConfigCollisionError < RuntimeError
21
+ # Message hinting at possible solution
22
+ HINT = 'Either rename your config entry or remove the environment variable.'
23
+ end
24
+
25
+ # Raised when there are config or secrets files found at multiple locations. You can resolve this by deciding on
26
+ # one correct location and removing the alternate file(s).
27
+ class AmbiguousSourceError < RuntimeError
28
+ # Message hinting at possible solution
29
+ HINT = 'Choose 1 correct one and delete the others.'
30
+ end
31
+
32
+ # Raised when #pretend is called but the testing extension has not been loaded.
33
+ #
34
+ # When raised during normal operation, it may mean the application is calling #pretend directly, which is strongly
35
+ # discouraged. The feature is meant for testing.
36
+ #
37
+ # @see Invar#pretend
38
+ class ImmutableRealityError < NoMethodError
39
+ HINT = <<~HINT
40
+ Try adding this to your test suite config file:
41
+ require 'invar/test'
42
+ HINT
43
+
44
+ PRETEND_MSG = "Method 'Invar::Scope#pretend' is defined in the testing extension. #{ HINT }"
45
+ HOOK_MSG = "Methods 'Invar.after_load and clear_hooks' are defined in the testing extension. #{ HINT }"
46
+ end
47
+
48
+ # Raised when schema validation fails
49
+ class SchemaValidationError < RuntimeError
50
+ end
51
+ end
@@ -7,13 +7,18 @@ require 'delegate'
7
7
 
8
8
  module Invar
9
9
  # Verifies a file is secure
10
- class PrivateFile #< SimpleDelegator
10
+ class PrivateFile
11
11
  extend Forwardable
12
12
  def_delegators :@delegate_sd_obj, :stat, :to_s, :basename, :==, :chmod
13
13
 
14
+ # Mask for limiting to the lowest three octal digits (which store permissions)
15
+ PERMISSIONS_MASK = 0o777
16
+
14
17
  ALLOWED_USER_MODES = [0o600, 0o400].freeze
15
18
  ALLOWED_GROUP_MODES = [0o060, 0o040, 0o000].freeze
16
19
 
20
+ DEFAULT_PERMISSIONS = 0o600
21
+
17
22
  # Allowed permissions modes for lockfile. Readable or read-writable by the user or group only
18
23
  ALLOWED_MODES = ALLOWED_USER_MODES.product(ALLOWED_GROUP_MODES).collect do |u, g|
19
24
  u | g # bitwise OR
@@ -45,9 +50,8 @@ module Invar
45
50
  #
46
51
  # @raise [FilePermissionsError] if the file has insecure permissions
47
52
  def verify_permissions!
48
- permissions_mask = 0o777 # only the lowest three digits are perms, so masking
49
53
  # stat = @delegate_sd_obj.stat
50
- file_mode = stat.mode & permissions_mask
54
+ file_mode = stat.mode & PERMISSIONS_MASK
51
55
  # TODO: use stat.world_readable? etc instead
52
56
  return if ALLOWED_MODES.include? file_mode
53
57
 
@@ -199,7 +199,10 @@ module Invar
199
199
 
200
200
  encryption_key = Lockbox.generate_key
201
201
 
202
- write_encrypted_file(file_path, encryption_key, SECRETS_TEMPLATE)
202
+ write_encrypted_file(file_path,
203
+ encryption_key: encryption_key,
204
+ content: SECRETS_TEMPLATE,
205
+ permissions: PrivateFile::DEFAULT_PERMISSIONS)
203
206
 
204
207
  warn "Created file: #{ file_path }"
205
208
 
@@ -232,7 +235,7 @@ module Invar
232
235
  'secrets.yml'
233
236
  end
234
237
 
235
- def write_encrypted_file(file_path, encryption_key, content)
238
+ def write_encrypted_file(file_path, encryption_key:, content:, permissions: nil)
236
239
  lockbox = Lockbox.new(key: encryption_key)
237
240
 
238
241
  encrypted_data = lockbox.encrypt(content)
@@ -240,7 +243,7 @@ module Invar
240
243
  config_dir.mkpath
241
244
  # TODO: replace File.opens with photo_path.binwrite(uri.data) once FakeFS can handle it
242
245
  File.open(file_path.to_s, 'wb') { |f| f.write encrypted_data }
243
- file_path.chmod 0o600
246
+ file_path.chmod permissions if permissions
244
247
  end
245
248
 
246
249
  def edit_encrypted_file(file_path)
@@ -257,7 +260,7 @@ module Invar
257
260
  tmp_file.read
258
261
  end
259
262
 
260
- write_encrypted_file(file_path, encryption_key, file_str)
263
+ write_encrypted_file(file_path, encryption_key: encryption_key, content: file_str)
261
264
  end
262
265
 
263
266
  def determine_key(file_path)
data/lib/invar/reality.rb CHANGED
@@ -72,27 +72,21 @@ module Invar
72
72
  @configs = Scope.new(load_configs(locator))
73
73
  rescue FileLocator::FileNotFoundError
74
74
  raise MissingConfigFileError,
75
- "No config file found. Create config.yml in one of these locations: #{ search_paths }"
75
+ "No Invar config file found. Create config.yml in one of these locations: #{ search_paths }"
76
76
  end
77
77
 
78
78
  begin
79
79
  @secrets = Scope.new(load_secrets(locator, decryption_keyfile || DEFAULT_KEY_FILE_NAME))
80
80
  rescue FileLocator::FileNotFoundError
81
81
  raise MissingSecretsFileError,
82
- "No secrets file found. Create encrypted secrets.yml in one of these locations: #{ search_paths }"
82
+ "No Invar secrets file found. Create encrypted secrets.yml in one of these locations: #{ search_paths }"
83
83
  end
84
84
 
85
85
  freeze
86
- # instance_eval(&self.class.__override_block__)
87
- self.class.__override_block__&.call(self)
88
86
 
89
87
  RealityValidator.new(configs_schema, secrets_schema).validate(@configs, @secrets)
90
88
  end
91
89
 
92
- class << self
93
- attr_accessor :__override_block__
94
- end
95
-
96
90
  # Fetch from one of the two base scopes: :config or :secret.
97
91
  # Plural names are also accepted (ie. :configs and :secrets).
98
92
  #
@@ -224,51 +218,4 @@ module Invar
224
218
  end
225
219
  end
226
220
  end
227
-
228
- # Raised when no config file can be found within the search paths.
229
- class MissingConfigFileError < RuntimeError
230
- end
231
-
232
- # Raised when no secrets file can be found within the search paths.
233
- class MissingSecretsFileError < RuntimeError
234
- end
235
-
236
- # Raised when an error is encountered during secrets file encryption
237
- class SecretsFileEncryptionError < RuntimeError
238
- end
239
-
240
- # Raised when an error is encountered during secrets file decryption
241
- class SecretsFileDecryptionError < RuntimeError
242
- end
243
-
244
- # Raised when a key is defined in both the environment and the configuration file.
245
- class EnvConfigCollisionError < RuntimeError
246
- # Message hinting at possible solution
247
- HINT = 'Either rename your config entry or remove the environment variable.'
248
- end
249
-
250
- # Raised when there are config or secrets files found at multiple locations. You can resolve this by deciding on
251
- # one correct location and removing the alternate file(s).
252
- class AmbiguousSourceError < RuntimeError
253
- # Message hinting at possible solution
254
- HINT = 'Choose 1 correct one and delete the others.'
255
- end
256
-
257
- # Raised when #pretend is called but the testing extension has not been loaded.
258
- #
259
- # When raised during normal operation, it may mean the application is calling #pretend directly, which is strongly
260
- # discouraged. The feature is meant for testing.
261
- #
262
- # @see Invar#pretend
263
- class ImmutableRealityError < NoMethodError
264
- # Message and hint for a possible solution
265
- MSG = <<~MSG
266
- Method 'pretend' is defined in the testing extension. Try adding this to your test suite config file:
267
- require 'invar/test'
268
- MSG
269
- end
270
-
271
- # Raised when schema validation fails
272
- class SchemaValidationError < RuntimeError
273
- end
274
221
  end
data/lib/invar/scope.rb CHANGED
@@ -7,7 +7,6 @@ module Invar
7
7
  @data = convert(data)
8
8
 
9
9
  @data.freeze
10
- @data_override = {}
11
10
  freeze
12
11
  end
13
12
 
@@ -18,23 +17,27 @@ module Invar
18
17
  # @see #override
19
18
  def fetch(key)
20
19
  key = key.downcase.to_sym
21
- @data_override.fetch(key, @data.fetch(key))
20
+ @data.fetch key
22
21
  rescue KeyError => e
23
- raise KeyError, "#{ e.message }. Known keys are #{ @data.keys.sort.collect { |k| ":#{ k }" }.join(', ') }"
22
+ raise KeyError, "#{ e.message }. Known keys are #{ known_keys }"
24
23
  end
25
24
 
26
25
  alias / fetch
27
26
  alias [] fetch
28
27
 
29
- def pretend(**)
30
- raise ::Invar::ImmutableRealityError, ::Invar::ImmutableRealityError::MSG
28
+ def method_missing(symbol, *args)
29
+ if symbol == :pretend
30
+ raise ::Invar::ImmutableRealityError, ::Invar::ImmutableRealityError::PRETEND_MSG
31
+ else
32
+ super
33
+ end
31
34
  end
32
35
 
33
36
  # Returns a hash representation of this scope and subscopes.
34
37
  #
35
38
  # @return [Hash] a hash representation of this scope
36
39
  def to_h
37
- @data.merge(@data_override).to_h.transform_values do |value|
40
+ @data.to_h.transform_values do |value|
38
41
  case value
39
42
  when Scope
40
43
  value.to_h
@@ -50,11 +53,15 @@ module Invar
50
53
 
51
54
  private
52
55
 
56
+ def known_keys
57
+ @data.keys.sort.collect { |k| ":#{ k }" }.join(', ')
58
+ end
59
+
53
60
  def convert(data)
54
61
  (data || {}).dup.each_with_object({}) do |pair, agg|
55
62
  key, value = pair
56
63
 
57
- agg[key] = value.is_a?(Hash) ? Scope.new(value) : value
64
+ agg[key.to_s.downcase.to_sym] = value.is_a?(Hash) ? Scope.new(value) : value
58
65
  end
59
66
  end
60
67
  end
data/lib/invar/test.rb CHANGED
@@ -1,16 +1,101 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'invar'
3
+ # Specifically not calling require 'invar' here to force applications to need to include it themselves,
4
+ # preventing the situation where test suites include application dependencies for them and breaking when
5
+ # the app is run without the test suite
4
6
 
5
7
  module Invar
6
- # Extension to the standard class
7
- class Scope
8
- # Overrides the given set of key-value pairs. This is intended to only be used in testing environments,
9
- # where you may need contextual adjustments to suit the test situation.
10
- #
11
- # @param [Hash] pairs the hash of pairs to override.
12
- def pretend(pairs)
13
- @data_override.merge!(pairs.transform_keys(&:to_sym))
8
+ # Namespace module containing mixins for parts of the main gem to enable modifications and data control
9
+ # in automated testing, while remaining immutable in the main gem and real runtime usage.
10
+ module TestExtension
11
+ module RealityMethods
12
+ class << self
13
+ attr_accessor :__after_load_hooks__
14
+ RealityMethods.__after_load_hooks__ = []
15
+ end
16
+
17
+ def initialize(**)
18
+ super
19
+
20
+ # instance_eval(&self.class.__override_block__)
21
+ RealityMethods.__after_load_hooks__.each { |hook| hook.call(self) }
22
+ end
23
+ end
24
+
25
+ # Adds methods to the main Invar module itself for a global-access hook to be used in application init phase.
26
+ module LoadHook
27
+ def clear_hooks
28
+ RealityMethods.__after_load_hooks__.clear
29
+ end
30
+
31
+ # Block that will be run after loading from config files.
32
+ #
33
+ # It is intended to allow test suites to tweak configurations without having to duplicate the entire config file.
34
+ #
35
+ # @yieldparam the configs from the Invar
36
+ # @return [void]
37
+ def after_load(&block)
38
+ RealityMethods.__after_load_hooks__.push(block)
39
+ end
14
40
  end
41
+
42
+ # Methods mixin for the Invar::Scope class
43
+ module ScopeMethods
44
+ def initialize(data)
45
+ @pretend_data = {}
46
+ super
47
+ end
48
+
49
+ # Overrides the given set of key-value pairs. This is intended to only be used in testing environments,
50
+ # where you may need contextual adjustments to suit the test situation.
51
+ #
52
+ # @param [Hash] pairs the hash of pairs to override.
53
+ def pretend(pairs)
54
+ @pretend_data.merge! convert(pairs)
55
+ end
56
+
57
+ def fetch(key)
58
+ @pretend_data.fetch(key.downcase.to_sym) do
59
+ super
60
+ end
61
+ rescue KeyError => e
62
+ raise KeyError, "#{ e.message }. Pretend keys are: #{ pretend_keys }."
63
+ end
64
+
65
+ # Duplicated to refer to the override version
66
+ alias / fetch
67
+ alias [] fetch
68
+
69
+ # Returns a hash representation of this scope and subscopes.
70
+ #
71
+ # @return [Hash] a hash representation of this scope
72
+ def to_h
73
+ super.merge(@pretend_data).to_h
74
+ end
75
+
76
+ private
77
+
78
+ def pretend_keys
79
+ keys = @pretend_data.keys
80
+
81
+ if keys.empty?
82
+ '(none)'
83
+ else
84
+ keys.sort.collect { |k| ":#{ k }" }.join(', ')
85
+ end
86
+ end
87
+ end
88
+ end
89
+
90
+ # Extension to the base library class that provides additional methods relevant only to automated testing
91
+ extend TestExtension::LoadHook
92
+
93
+ # Extension to the base library class that provides additional methods relevant only to automated testing
94
+ class Scope
95
+ prepend TestExtension::ScopeMethods
96
+ end
97
+
98
+ class Reality
99
+ prepend TestExtension::RealityMethods
15
100
  end
16
101
  end
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.6.1'
5
+ VERSION = '0.7.0'
6
6
  end
data/lib/invar.rb CHANGED
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'invar/version'
4
- require 'invar/reality'
3
+ require_relative 'invar/version'
4
+ require_relative 'invar/errors'
5
+ require_relative 'invar/reality'
5
6
 
6
7
  # Invar is a Ruby Gem that provides a single source of truth for application configuration, secrets, and environment
7
8
  # variables.
@@ -14,13 +15,12 @@ module Invar
14
15
  end
15
16
 
16
17
  class << self
17
- # Block that will be run after loading from config files and prior to freezing. It is intended to allow
18
- # for test suites to tweak configurations without having to duplicate the entire config file.
19
- #
20
- # @yieldparam the configs from the Invar
21
- # @return [void]
22
- def after_load(&block)
23
- ::Invar::Reality.__override_block__ = block
18
+ def method_missing(meth)
19
+ if [:after_load, :clear_hooks].include? meth
20
+ raise ::Invar::ImmutableRealityError, ::Invar::ImmutableRealityError::HOOK_MSG
21
+ else
22
+ super
23
+ end
24
24
  end
25
25
  end
26
26
  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.6.1
4
+ version: 0.7.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: 2023-05-22 00:00:00.000000000 Z
11
+ date: 2023-08-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dry-schema
@@ -38,90 +38,6 @@ dependencies:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: '1.0'
41
- - !ruby/object:Gem::Dependency
42
- name: bundler
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - "~>"
46
- - !ruby/object:Gem::Version
47
- version: '2.3'
48
- type: :development
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - "~>"
53
- - !ruby/object:Gem::Version
54
- version: '2.3'
55
- - !ruby/object:Gem::Dependency
56
- name: fakefs
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - "~>"
60
- - !ruby/object:Gem::Version
61
- version: '1.9'
62
- type: :development
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - "~>"
67
- - !ruby/object:Gem::Version
68
- version: '1.9'
69
- - !ruby/object:Gem::Dependency
70
- name: rake
71
- requirement: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - "~>"
74
- - !ruby/object:Gem::Version
75
- version: '13.0'
76
- type: :development
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - "~>"
81
- - !ruby/object:Gem::Version
82
- version: '13.0'
83
- - !ruby/object:Gem::Dependency
84
- name: rspec
85
- requirement: !ruby/object:Gem::Requirement
86
- requirements:
87
- - - "~>"
88
- - !ruby/object:Gem::Version
89
- version: '3.12'
90
- type: :development
91
- prerelease: false
92
- version_requirements: !ruby/object:Gem::Requirement
93
- requirements:
94
- - - "~>"
95
- - !ruby/object:Gem::Version
96
- version: '3.12'
97
- - !ruby/object:Gem::Dependency
98
- name: simplecov
99
- requirement: !ruby/object:Gem::Requirement
100
- requirements:
101
- - - "~>"
102
- - !ruby/object:Gem::Version
103
- version: '0.21'
104
- type: :development
105
- prerelease: false
106
- version_requirements: !ruby/object:Gem::Requirement
107
- requirements:
108
- - - "~>"
109
- - !ruby/object:Gem::Version
110
- version: '0.21'
111
- - !ruby/object:Gem::Dependency
112
- name: yard
113
- requirement: !ruby/object:Gem::Requirement
114
- requirements:
115
- - - "~>"
116
- - !ruby/object:Gem::Version
117
- version: '0.9'
118
- type: :development
119
- prerelease: false
120
- version_requirements: !ruby/object:Gem::Requirement
121
- requirements:
122
- - - "~>"
123
- - !ruby/object:Gem::Version
124
- version: '0.9'
125
41
  description: |
126
42
  Locates and loads config YAML files based on XDG standard with the encrypted secrets file kept separately.
127
43
  Includes useful rake tasks to make management easier. No code execution in config. Rails-independent. Gluten free.
@@ -143,6 +59,7 @@ files:
143
59
  - Rakefile
144
60
  - invar.gemspec
145
61
  - lib/invar.rb
62
+ - lib/invar/errors.rb
146
63
  - lib/invar/file_locator.rb
147
64
  - lib/invar/private_file.rb
148
65
  - lib/invar/rake/tasks.rb
@@ -170,7 +87,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
170
87
  - !ruby/object:Gem::Version
171
88
  version: '0'
172
89
  requirements: []
173
- rubygems_version: 3.1.2
90
+ rubygems_version: 3.4.10
174
91
  signing_key:
175
92
  specification_version: 4
176
93
  summary: Single source of truth for environmental configuration.