secret_config 0.6.4 → 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,44 @@
1
+ module SecretConfig
2
+ class Config
3
+ extend Forwardable
4
+ def_delegator :registry, :configuration
5
+ def_delegator :registry, :refresh!
6
+
7
+ def initialize(path, registry)
8
+ @path = path
9
+ @registry = registry
10
+ end
11
+
12
+ def fetch(sub_path, **options)
13
+ registry.fetch(join_path(sub_path), **options)
14
+ end
15
+
16
+ def [](sub_path)
17
+ registry[join_path(sub_path)]
18
+ end
19
+
20
+ def []=(sub_path, value)
21
+ registry[join_path(sub_path)] = value
22
+ end
23
+
24
+ def key?(sub_path)
25
+ registry.key?(join_path(sub_path))
26
+ end
27
+
28
+ def set(sub_path, value)
29
+ registry.set(join_path(sub_path), value)
30
+ end
31
+
32
+ def delete(sub_path)
33
+ registry.delete(join_path(sub_path))
34
+ end
35
+
36
+ private
37
+
38
+ attr_reader :path, :registry
39
+
40
+ def join_path(sub_path)
41
+ File.join(path, sub_path)
42
+ end
43
+ end
44
+ end
@@ -10,4 +10,7 @@ module SecretConfig
10
10
 
11
11
  class ConfigurationError < Error
12
12
  end
13
+
14
+ class InvalidInterpolation < Error
15
+ end
13
16
  end
@@ -0,0 +1,76 @@
1
+ module SecretConfig
2
+ class Parser
3
+ attr_reader :tree, :path, :registry, :interpolator
4
+
5
+ def initialize(path, registry, interpolate: true)
6
+ @path = path
7
+ @registry = registry
8
+ @fetch_list = {}
9
+ @import_list = {}
10
+ @tree = {}
11
+ @interpolator = interpolate ? SettingInterpolator.new : nil
12
+ end
13
+
14
+ # Returns a flat path of keys and values from the provider without looking in the local path.
15
+ # Keys are returned with path names relative to the supplied path.
16
+ def parse(key, value)
17
+ relative_key = relative_key?(key) ? key : key.sub("#{path}/", "")
18
+ value = interpolator.parse(value) if interpolator && value.is_a?(String) && value.include?("%{")
19
+ tree[relative_key] = value
20
+ end
21
+
22
+ # Returns a flat Hash of the rendered paths.
23
+ def render
24
+ apply_imports if interpolator
25
+ tree
26
+ end
27
+
28
+ private
29
+
30
+ # def apply_fetches
31
+ # tree[key] = relative_key?(fetch_key) ? registry[fetch_key] : registry.provider.fetch(fetch_key)
32
+ # end
33
+
34
+ # Import from the current registry as well as new fetches.
35
+ #
36
+ # Notes:
37
+ # - A lot of absolute key lookups can be expensive since each one is a separate call.
38
+ # - Imports cannot reference other imports at this time.
39
+ def apply_imports
40
+ tree.keys.each do |key|
41
+ next unless (key =~ /\/__import__\Z/) || (key == "__import__")
42
+
43
+ import_key = tree.delete(key)
44
+ key, _ = ::File.split(key)
45
+ key = nil if key == "."
46
+
47
+ # binding.irb
48
+
49
+ # With a relative key, look for the values in the current registry.
50
+ # With an absolute key call the provider and fetch the value directly.
51
+
52
+ if relative_key?(import_key)
53
+ tree.keys.each do |current_key|
54
+ match = current_key.match(/\A#{import_key}\/(.*)/)
55
+ next unless match
56
+
57
+ imported_key = key.nil? ? match[1] : ::File.join(key, match[1])
58
+ tree[imported_key] = tree[current_key] unless tree.key?(imported_key)
59
+ end
60
+ else
61
+ relative_paths = registry.send(:fetch_path, import_key)
62
+ relative_paths.each_pair do |relative_key, value|
63
+ imported_key = key.nil? ? relative_key : ::File.join(key, relative_key)
64
+ tree[imported_key] = value unless tree.key?(imported_key)
65
+ end
66
+ end
67
+ end
68
+ end
69
+
70
+ # Returns [true|false] whether the supplied key is considered a relative key.
71
+ def relative_key?(key)
72
+ !key.start_with?("/")
73
+ end
74
+
75
+ end
76
+ end
@@ -12,17 +12,30 @@ module SecretConfig
12
12
  raise(ConfigurationError, "Cannot find config file: #{file_name}") unless ::File.exist?(file_name)
13
13
  end
14
14
 
15
+ # Yields the key with its absolute path and corresponding string value
15
16
  def each(path, &block)
16
- config = YAML.load(ERB.new(::File.new(file_name).read).result)
17
-
18
- paths = path.sub(%r{\A/*}, "").sub(%r{/*\Z}, "").split("/")
19
- settings = config.dig(*paths)
17
+ settings = fetch_path(path)
20
18
 
21
19
  raise(ConfigurationError, "Path #{paths.join('/')} not found in file: #{file_name}") unless settings
22
20
 
23
21
  Utils.flatten_each(settings, path, &block)
24
22
  nil
25
23
  end
24
+
25
+ # Returns the value or `nil` if not found
26
+ def fetch(key)
27
+ values = fetch_path(path)
28
+ value.is_a?(Hash) ? nil : value
29
+ end
30
+
31
+ private
32
+
33
+ def fetch_path(path)
34
+ config = YAML.load(ERB.new(::File.new(file_name).read).result)
35
+
36
+ paths = path.sub(%r{\A/*}, "").sub(%r{/*\Z}, "").split("/")
37
+ config.dig(*paths)
38
+ end
26
39
  end
27
40
  end
28
41
  end
@@ -10,7 +10,13 @@ module SecretConfig
10
10
  class Ssm < Provider
11
11
  attr_reader :client, :key_id, :retry_count, :retry_max_ms, :logger
12
12
 
13
- def initialize(key_id: ENV["AWS_ACCESS_KEY_ID"], key_alias: ENV["AWS_ACCESS_KEY_ALIAS"], retry_count: 10, retry_max_ms: 3_000)
13
+ def initialize(
14
+ key_id: ENV["SECRET_CONFIG_KEY_ID"],
15
+ key_alias: ENV["SECRET_CONFIG_KEY_ALIAS"],
16
+ retry_count: 25,
17
+ retry_max_ms: 10_000,
18
+ **args
19
+ )
14
20
  @key_id =
15
21
  if key_alias
16
22
  key_alias =~ %r{^alias/} ? key_alias : "alias/#{key_alias}"
@@ -20,9 +26,10 @@ module SecretConfig
20
26
  @retry_count = retry_count
21
27
  @retry_max_ms = retry_max_ms
22
28
  @logger = SemanticLogger["Aws::SSM"] if defined?(SemanticLogger)
23
- @client = Aws::SSM::Client.new(logger: logger)
29
+ @client = Aws::SSM::Client.new({logger: logger}.merge!(args))
24
30
  end
25
31
 
32
+ # Yields the key with its absolute path and corresponding string value
26
33
  def each(path)
27
34
  retries = 0
28
35
  token = nil
@@ -62,7 +69,8 @@ module SecretConfig
62
69
  value: value.to_s,
63
70
  type: "SecureString",
64
71
  key_id: key_id,
65
- overwrite: true
72
+ overwrite: true,
73
+ tier: "Intelligent-Tiering"
66
74
  )
67
75
  end
68
76
 
@@ -4,26 +4,31 @@ require "concurrent-ruby"
4
4
  module SecretConfig
5
5
  # Centralized configuration with values stored in AWS System Manager Parameter Store
6
6
  class Registry
7
- attr_reader :provider
7
+ attr_reader :provider, :interpolate
8
8
  attr_accessor :path
9
9
 
10
- def initialize(path: nil, provider: nil, provider_args: nil)
10
+ def initialize(path: nil, provider: nil, provider_args: nil, interpolate: true)
11
11
  @path = default_path(path)
12
12
  raise(UndefinedRootError, "Root must start with /") unless @path.start_with?("/")
13
13
 
14
14
  resolved_provider = default_provider(provider)
15
15
  provider_args = nil if resolved_provider != provider
16
16
 
17
- @provider = create_provider(resolved_provider, provider_args)
18
- @cache = Concurrent::Map.new
17
+ @provider = create_provider(resolved_provider, provider_args)
18
+ @cache = Concurrent::Map.new
19
+ @interpolate = interpolate
19
20
  refresh!
20
21
  end
21
22
 
22
23
  # Returns [Hash] a copy of the in memory configuration data.
23
- def configuration(relative: true, filters: SecretConfig.filters)
24
+ #
25
+ # Supply the relative path to start from so that only keys and values in that
26
+ # path will be returned.
27
+ def configuration(path: nil, filters: SecretConfig.filters)
24
28
  h = {}
25
29
  cache.each_pair do |key, value|
26
- key = relative_key(key) if relative
30
+ next if path && !key.start_with?(path)
31
+
27
32
  value = filter_value(key, value, filters)
28
33
  Utils.decompose(key, value, h)
29
34
  end
@@ -32,25 +37,35 @@ module SecretConfig
32
37
 
33
38
  # Returns [String] configuration value for the supplied key, or nil when missing.
34
39
  def [](key)
35
- cache[expand_key(key)]
40
+ value = cache[key]
41
+ if value.nil? && SecretConfig.check_env_var?
42
+ value = env_var_override(key, value)
43
+ cache[key] = value unless value.nil?
44
+ end
45
+ value.nil? ? nil : value.to_s
36
46
  end
37
47
 
38
48
  # Returns [String] configuration value for the supplied key, or nil when missing.
39
49
  def key?(key)
40
- cache.key?(expand_key(key))
50
+ cache.key?(key)
41
51
  end
42
52
 
43
53
  # Returns [String] configuration value for the supplied key
44
- def fetch(key, default: :no_default_supplied, type: :string, encoding: nil)
54
+ # Convert the string value into an array of values by supplying a `separator`.
55
+ def fetch(key, default: :no_default_supplied, type: :string, encoding: nil, separator: nil)
45
56
  value = self[key]
46
57
  if value.nil?
47
58
  raise(MissingMandatoryKey, "Missing configuration value for #{path}/#{key}") if default == :no_default_supplied
48
59
 
49
- value = default.respond_to?(:call) ? default.call : default
60
+ value = block_given? ? yield : default
50
61
  end
51
62
 
52
63
  value = convert_encoding(encoding, value) if encoding
53
- type == :string ? value : convert_type(type, value)
64
+
65
+ return convert_type(type, value) unless separator
66
+ return value if value.is_a?(Array)
67
+
68
+ value.to_s.split(separator).collect { |element| convert_type(type, element.strip) }
54
69
  end
55
70
 
56
71
  # Set the value for a key in the centralized configuration store.
@@ -60,24 +75,24 @@ module SecretConfig
60
75
 
61
76
  # Set the value for a key in the centralized configuration store.
62
77
  def set(key, value)
63
- key = expand_key(key)
64
- provider.set(key, value)
78
+ full_key = expand_key(key)
79
+ provider.set(full_key, value)
65
80
  cache[key] = value
66
81
  end
67
82
 
68
83
  # Delete a key from the centralized configuration store.
69
84
  def delete(key)
70
- key = expand_key(key)
71
- provider.delete(key)
85
+ full_key = expand_key(key)
86
+ provider.delete(full_key)
72
87
  cache.delete(key)
73
88
  end
74
89
 
75
90
  # Refresh the in-memory cached copy of the centralized configuration information.
76
- # Environment variable values will take precendence over the central store values.
91
+ # Environment variable values will take precedence over the central store values.
77
92
  def refresh!
78
93
  existing_keys = cache.keys
79
94
  updated_keys = []
80
- provider.each(path) do |key, value|
95
+ fetch_path(path).each_pair do |key, value|
81
96
  cache[key] = env_var_override(key, value)
82
97
  updated_keys << key
83
98
  end
@@ -92,21 +107,31 @@ module SecretConfig
92
107
 
93
108
  attr_reader :cache
94
109
 
110
+ # Returns [true|false] whether the supplied key is considered a relative key.
111
+ def relative_key?(key)
112
+ !key.start_with?("/")
113
+ end
114
+
115
+ # Returns a flat path of keys and values from the provider without looking in the local path.
116
+ # Keys are returned with path names relative to the supplied path.
117
+ def fetch_path(path)
118
+ parser = Parser.new(path, self, interpolate: interpolate)
119
+ provider.each(path) { |key, value| parser.parse(key, value) }
120
+ parser.render
121
+ end
122
+
95
123
  # Returns the value from an env var if it is present,
96
124
  # Otherwise the value is returned unchanged.
97
125
  def env_var_override(key, value)
98
- env_var_name = relative_key(key).upcase.gsub("/", "_")
126
+ return value unless SecretConfig.check_env_var?
127
+
128
+ env_var_name = key.upcase.gsub("/", "_")
99
129
  ENV[env_var_name] || value
100
130
  end
101
131
 
102
132
  # Add the path to the path if it is a relative path.
103
133
  def expand_key(key)
104
- key.start_with?("/") ? key : "#{path}/#{key}"
105
- end
106
-
107
- # Convert the key to a relative path by removing the path.
108
- def relative_key(key)
109
- key.start_with?("/") ? key.sub("#{path}/", "") : key
134
+ relative_key?(key) ? "#{path}/#{key}" : key
110
135
  end
111
136
 
112
137
  def filter_value(key, value, filters)
@@ -128,12 +153,12 @@ module SecretConfig
128
153
 
129
154
  def convert_type(type, value)
130
155
  case type
156
+ when :string
157
+ value.to_s
131
158
  when :integer
132
159
  value.to_i
133
160
  when :float
134
161
  value.to_f
135
- when :string
136
- value
137
162
  when :boolean
138
163
  %w[true 1 t].include?(value.to_s.downcase)
139
164
  when :symbol
@@ -159,7 +184,7 @@ module SecretConfig
159
184
 
160
185
  raise(UndefinedRootError, "Either set env var 'SECRET_CONFIG_PATH' or call SecretConfig.use") unless path
161
186
 
162
- path.start_with?("/") ? path : "/#{path}"
187
+ relative_key?(path) ? "/#{path}" : path
163
188
  end
164
189
 
165
190
  def default_provider(provider)
@@ -0,0 +1,45 @@
1
+ require "date"
2
+ require "socket"
3
+ require "securerandom"
4
+ # * SecretConfig Interpolations
5
+ #
6
+ # Expanding values inline for date, time, hostname, pid and random values.
7
+ # %{date} # Current date in the format of "%Y%m%d" (CCYYMMDD)
8
+ # %{date:format} # Current date in the supplied format. See strftime
9
+ # %{time} # Current date and time down to ms in the format of "%Y%m%d%Y%H%M%S%L" (CCYYMMDDHHMMSSmmm)
10
+ # %{time:format} # Current date and time in the supplied format. See strftime
11
+ # %{env:name} # Extract value from the named environment value.
12
+ # %{hostname} # Full name of this host.
13
+ # %{hostname:short} # Short name of this host. Everything up to the first period.
14
+ # %{pid} # Process Id for this process.
15
+ # %{random} # URL safe Random 32 byte value.
16
+ # %{random:size} # URL safe Random value of `size` bytes.
17
+ module SecretConfig
18
+ class SettingInterpolator < StringInterpolator
19
+ def date(format = "%Y%m%d")
20
+ Date.today.strftime(format)
21
+ end
22
+
23
+ def time(format = "%Y%m%d%H%M%S%L")
24
+ Time.now.strftime(format)
25
+ end
26
+
27
+ def env(name)
28
+ ENV[name]
29
+ end
30
+
31
+ def hostname(format = nil)
32
+ name = Socket.gethostname
33
+ name = name.split(".")[0] if format == "short"
34
+ name
35
+ end
36
+
37
+ def pid
38
+ $$
39
+ end
40
+
41
+ def random(size = 32)
42
+ SecureRandom.urlsafe_base64(size)
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,33 @@
1
+ # Parse strings containing %{key:value1,value2,value3}
2
+ # Where `key` is a method implemented by a class inheriting from this class
3
+ #
4
+ # The following `key`s are reserved:
5
+ # * parse
6
+ # * initialize
7
+ #
8
+ # Notes:
9
+ # * To prevent interpolation use %%{...}
10
+ # * %% is not touched, only %{...} is identified.
11
+ module SecretConfig
12
+ class StringInterpolator
13
+ def initialize(pattern = nil)
14
+ @pattern = pattern || /%{1,2}\{([^}]+)\}/
15
+ end
16
+
17
+ def parse(string)
18
+ string.gsub(/%{1,2}\{([^}]+)\}/) do |match|
19
+ if match.start_with?("%%")
20
+ match[1..-1]
21
+ else
22
+ expr = Regexp.last_match(1) || Regexp.last_match(2) || match.tr("%{}", "")
23
+ key, args_str = expr.split(":")
24
+ key = key.strip.to_sym
25
+ arguments = args_str&.split(",")&.map { |v| v.strip == "" ? nil : v.strip } || []
26
+ raise(InvalidInterpolation, "Invalid key: #{key} in string: #{match}") unless respond_to?(key)
27
+
28
+ public_send(key, *arguments)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end