secure_data_bag 2.2.0 → 3.0.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.
@@ -0,0 +1,37 @@
1
+ require 'json'
2
+ require 'chef/knife/data_bag_show'
3
+ require_relative 'secure_data_bag/base_mixin'
4
+ require_relative 'secure_data_bag/export_mixin'
5
+ require_relative 'secure_data_bag/secrets_mixin'
6
+
7
+ class Chef
8
+ class Knife
9
+ class SecureBagOpen < Knife::DataBagShow
10
+ include SecureDataBag::BaseMixin
11
+ include SecureDataBag::SecretsMixin
12
+
13
+ banner 'knife secure bag open PATH'
14
+ category 'secure bag'
15
+
16
+ def run
17
+ unless ::File.exist?(@name_args[0])
18
+ ui.fatal('File not found.')
19
+ show_usage
20
+ exit 1
21
+ end
22
+
23
+ display_metadata = config_metadata.dup
24
+ display_metadata[:encryption_format] ||= 'plain'
25
+
26
+ data = File.read(@name_args[0])
27
+ data = JSON.parse(data)
28
+ item = create_item('local', @name_args[0], data, display_metadata)
29
+
30
+ display_data = item.to_hash(metadata: true)
31
+ display_data = format_for_display(display_data)
32
+
33
+ output(display_data)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -1,46 +1,51 @@
1
-
2
- require 'chef/knife/secure_bag_base'
3
1
  require 'chef/knife/data_bag_show'
2
+ require_relative 'secure_data_bag/base_mixin'
3
+ require_relative 'secure_data_bag/export_mixin'
4
+ require_relative 'secure_data_bag/secrets_mixin'
5
+ require_relative 'secure_data_bag/defaults_mixin'
4
6
 
5
7
  class Chef
6
8
  class Knife
7
9
  class SecureBagShow < Knife::DataBagShow
8
- include Knife::SecureBagBase
9
-
10
- banner "knife secure bag show BAG [ITEM] (options)"
11
- category "secure bag"
12
-
13
- option :encoded,
14
- long: "--encoded",
15
- boolean: true,
16
- description: "Whether we wish to display encoded values",
17
- default: false
18
-
19
- def load_item(bag, item_name)
20
- item = SecureDataBag::Item.load(bag, item_name,
21
- key: read_secret,
22
- fields: encoded_fields
23
- )
24
-
25
- data = item.to_hash(encoded: config[:encoded])
26
- data["_encoded_fields"] = item.encoded_fields
27
- data
28
- end
10
+ include SecureDataBag::BaseMixin
11
+ include SecureDataBag::ExportMixin
12
+ include SecureDataBag::SecretsMixin
13
+ include SecureDataBag::DefaultsMixin
14
+
15
+ banner 'knife secure bag show BAG [ITEM] (options)'
16
+ category 'secure bag'
29
17
 
30
18
  def run
31
- display = case @name_args.length
32
- when 2
33
- item = load_item(@name_args[0], @name_args[1])
34
- display = format_for_display(item)
35
- when 1
36
- format_list_for_display(Chef::DataBag.load(@name_args[0]))
37
- else
38
- stdout.puts opt_parser
39
- exit(1)
40
- end
41
- output(display)
19
+ case @name_args.length
20
+ when 2
21
+ run_show
22
+ when 1
23
+ run_list
24
+ else
25
+ stdout.puts opt_parser
26
+ exit(1)
27
+ end
28
+ end
29
+
30
+ def run_show
31
+ config_defaults_for_data_bag!(@name_args[0])
32
+
33
+ display_metadata = config_metadata.dup
34
+ display_metadata[:encryption_format] ||= 'plain'
35
+
36
+ item = load_item(@name_args[0], @name_args[1], display_metadata)
37
+ data = item.to_hash(metadata: true)
38
+ data = format_for_display(data)
39
+
40
+ export!(@name_args[0], @name_args[1], item) if should_export?
41
+ output(data)
42
+ end
43
+
44
+ def run_list
45
+ data = Chef::DataBag.load(@name_args[0])
46
+ data = format_list_for_display(data)
47
+ output(data)
42
48
  end
43
49
  end
44
50
  end
45
51
  end
46
-
@@ -0,0 +1,80 @@
1
+ require 'chef/knife'
2
+
3
+ class Chef
4
+ class Knife
5
+ module SecureDataBag
6
+ module BaseMixin
7
+ # Steps to execute when the mixin is include.
8
+ # In this case specifically, add additional command line options
9
+ # related to exporting.
10
+ # @since 3.0.0
11
+ def self.included(base)
12
+ base.deps do
13
+ require 'secure_data_bag'
14
+ end
15
+
16
+ base.option :encryption_format,
17
+ description: 'The format with which to encrypt data. If unset, it will be autodetected.',
18
+ long: '--enc-format [plain|encrypted|nested]'
19
+
20
+ base.option :decryption_format,
21
+ description: 'The format with which to decrypt data. If unset, it will be autodetected.',
22
+ long: '--dec-format [plain|encrypted|nested]'
23
+
24
+ base.option :encrypted_keys,
25
+ description: 'Comma delimited list of keys which should be encrypted, in addition to what was previously there',
26
+ long: '--enc-keys FIELD1,FIELD2,FIELD3',
27
+ proc: proc { |s| s.split(',') }
28
+ end
29
+
30
+ # Metadata to use when interacting with SecureDataBag containing
31
+ # overrides specified on the command-line.
32
+ # @since 3.0.0
33
+ def config_metadata
34
+ Mash.new(
35
+ encryption_format: config[:encryption_format],
36
+ decryption_format: config[:decryption_format],
37
+ encrypted_keys: encrypted_keys,
38
+ secret: secret
39
+ )
40
+ end
41
+
42
+ # Load a data_bag_item from Chef Server
43
+ # @param data_bag [String] the data_bag to load from
44
+ # @param item_name [String] the data_bag_item name to load
45
+ # @param metadata [Hash] the optional metadata to pass to ::Item
46
+ # @return [SecureDataBag::Item]
47
+ # @since 3.0.0
48
+ def load_item(data_bag, item_name, metadata = {})
49
+ item = ::SecureDataBag::Item.load(data_bag, item_name, metadata)
50
+ item
51
+ end
52
+
53
+ # Create a new data_bag_item
54
+ # @param data_bag [String] the data_bag to load from
55
+ # @param item_name [String] the data_bag_item name to load
56
+ # @param data [Hash] the optional raw_data to use
57
+ # @param metadata [Hash] the optional metadata to pass to ::Item
58
+ # @return [SecureDataBag::Item]
59
+ # @since 3.0.0
60
+ def create_item(data_bag, item_name, data = {}, metadata = {})
61
+ item = ::SecureDataBag::Item.new(metadata)
62
+ item.raw_data = { 'id' => item_name }.merge(data)
63
+ item.data_bag data_bag
64
+ item
65
+ end
66
+
67
+ private
68
+
69
+ # Keys to encrypt which are a result of merging the arrays found within
70
+ # the the configuration file and provided over the command-line.
71
+ # @since 3.0.0
72
+ def encrypted_keys
73
+ Array(config[:encrypted_keys]).concat(
74
+ Array(Chef::Config[:knife][:secure_data_bag][:encrypted_keys])
75
+ ).uniq
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,32 @@
1
+ require 'chef/knife'
2
+
3
+ class Chef
4
+ class Knife
5
+ module SecureDataBag
6
+ module DefaultsMixin
7
+ # Apply Knife config values defined in knife.rb
8
+ # @param data_bag [String] the data_bag name
9
+ # @since 3.0.0
10
+ def config_defaults_for_data_bag!(data_bag)
11
+ config_defaults_for_data_bags(data_bag).each do |key, value|
12
+ if options.key?(key.to_sym)
13
+ config[key.to_sym] ||= value
14
+ end
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ # Defaults configuration hash for a specific data_bag
21
+ # @param data_bag [String] the data_bag name
22
+ # @return [Hash] the configuration hash
23
+ # @since 3.0.0
24
+ def config_defaults_for_data_bags(data_bag)
25
+ defaults = Chef::Config[:knife][:secure_data_bag] || {}
26
+ defaults = defaults[:defaults] || {}
27
+ defaults[data_bag.to_sym] || {}
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,85 @@
1
+ require 'chef/knife'
2
+
3
+ class Chef
4
+ class Knife
5
+ module SecureDataBag
6
+ module ExportMixin
7
+ # Steps to execute when the mixin is include.
8
+ # In this case specifically, add additional command line options
9
+ # related to exporting.
10
+ # @since 3.0.0
11
+ def self.included(base)
12
+ base.option :export,
13
+ description: 'Whether to export the data_bag item',
14
+ long: '--export',
15
+ boolean: true
16
+
17
+ base.option :export_format,
18
+ description: 'Format to export the data_bag_item as. If unset, this will default to the encryption format.',
19
+ long: '--export-format'
20
+
21
+ base.option :export_root,
22
+ long: '--export-root PATH',
23
+ description: 'Path containing data_bag folders and items'
24
+ end
25
+
26
+ # Should knife subcommands save data_bag_items to disk after uploading
27
+ # them to the Chef server.
28
+ # @returns [Boolean]
29
+ # @since 3.0.0
30
+ def should_export?
31
+ if config[:export].nil?
32
+ Chef::Config[:knife][:secure_data_bag][:export_on_upload]
33
+ else
34
+ config[:export]
35
+ end
36
+ end
37
+
38
+ # Export the item to the filesystem.
39
+ # @param data_bag [String] the data_bag to upload to
40
+ # @param item_name [String] the data_bag_item id to upload to
41
+ # @param item [SecureDataBag::Item] the item to upload
42
+ # @since 3.0.0
43
+ def export!(data_bag, item_name, item)
44
+ item.encryption_format = export_format
45
+ data = item.to_hash
46
+
47
+ if export_root.nil?
48
+ ui.fatal('Export root is not defined')
49
+ show_usage
50
+ exit 1
51
+ end
52
+
53
+ export_file_path = export_path(data_bag, item_name)
54
+ unless ::File.directory?(::File.dirname(export_file_path))
55
+ ui.fatal("Export directory does not exist: #{export_file_path}")
56
+ show_usage
57
+ exit 1
58
+ end
59
+
60
+ ::File.open(export_file_path, 'w') do |f|
61
+ f.write(Chef::JSONCompat.to_json_pretty(data))
62
+ end
63
+
64
+ display_path = export_file_path.sub(%r{/^#{export_root}/}, '')
65
+ stdout.puts("Exported to #{display_path}")
66
+ end
67
+
68
+ private
69
+
70
+ def export_root
71
+ config[:export_root] ||
72
+ Chef::Config[:knife][:secure_data_bag][:export_root]
73
+ end
74
+
75
+ def export_path(data_bag, item_name)
76
+ ::File.join(export_root, data_bag, item_name) + '.json'
77
+ end
78
+
79
+ def export_format
80
+ config[:export_format]
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,57 @@
1
+ class Chef
2
+ class Knife
3
+ module SecureDataBag
4
+ module SecretsMixin
5
+ # Steps to execute when the mixin is include.
6
+ # In this case specifically, add additional command line options
7
+ # related to exporting.
8
+ # @since 3.0.0
9
+ def self.included(base)
10
+ base.option :secret,
11
+ description: 'The secret key used to (de)encrypt data bag item values',
12
+ short: '-s SECRET',
13
+ long: '--secret '
14
+
15
+ base.option :secret_file,
16
+ description: 'The secret key file used to (de)encrypt data bag item values',
17
+ long: '--secret-file SECRET_FILE'
18
+ end
19
+
20
+ # The shared secret used to encrypt / decrypt data bag items
21
+ # @return [String] the shared secret
22
+ # @since 3.0.0
23
+ def secret
24
+ @secret ||= begin
25
+ secret = load_secret
26
+ unless secret
27
+ ui.fatal('A secret or secret_file must be specified')
28
+ show_usage
29
+ exit 1
30
+ end
31
+ secret
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ # Path to the secret_file
38
+ # @return [String]
39
+ # @since 3.0.0
40
+ def secret_file
41
+ config[:secret_file] ||
42
+ Chef::Config[:knife][:secure_data_bag][:secret_file]
43
+ end
44
+
45
+ # Load the shared secret, either from command line parameters or from
46
+ # a file on the filesystem.
47
+ # @return [String] the shared secret
48
+ # @since 3.0.0
49
+ def load_secret
50
+ if config[:secret] then config[:secret]
51
+ else ::SecureDataBag::Item.load_secret(secret_file)
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -1,8 +1,6 @@
1
+ require 'secure_data_bag/version'
2
+ require 'secure_data_bag/item'
3
+ require 'secure_data_bag/dsl/data_query'
1
4
 
2
- require "secure_data_bag/version"
3
- require "secure_data_bag/item"
4
- require "secure_data_bag/dsl/data_query"
5
-
6
- require_relative "chef/config"
7
- require_relative "chef/dsl/data_query"
8
-
5
+ require_relative 'chef/config'
6
+ require_relative 'chef/dsl/data_query'
@@ -0,0 +1,29 @@
1
+ require 'chef/encrypted_data_bag_item/check_encrypted'
2
+
3
+ module SecureDataBag
4
+ # Common code for checking if a data bag appears encrypted
5
+ module CheckEncrypted
6
+ include Chef::EncryptedDataBagItem::CheckEncrypted
7
+
8
+ # Autodetect whether the item's raw hash appears to be encrypted
9
+ def partially_encrypted?(raw_data)
10
+ data = raw_data.reject { |k, _| k == 'id' }
11
+
12
+ # Detect whether any of the raw hash keys, or their nested structures
13
+ # contain encrypted values.
14
+ data.any? do |_, v|
15
+ looks_like_partially_encrypted?(v)
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ # Chef if any of the nested data structures look like they have been
22
+ # encrypted in a manner compatible with
23
+ # Chef::EncryptedDataBagItem::Encryptor::VersionXEncryptor.
24
+ def looks_like_partially_encrypted?(data)
25
+ return false unless data.is_a?(Hash)
26
+ looks_like_encrypted?(data) || partially_encrypted?(data)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,5 @@
1
+ require 'secure_data_bag/version'
2
+
3
+ module SecureDataBag
4
+ METADATA_KEY = '_secure_metadata'.freeze
5
+ end
@@ -0,0 +1,149 @@
1
+ require 'secure_data_bag/constants'
2
+ require 'secure_data_bag/check_encrypted'
3
+
4
+ module SecureDataBag
5
+ module Decryptor
6
+ # Instantiate an Decryptor object responsable for decrypting the
7
+ # encrypted_hash with the secret. As much as possible, this method will
8
+ # attempt to auto-detect the item format to ensure compatibility.
9
+ #
10
+ # Much like with upstream, call #for_encrypted_item on the resulting
11
+ # object to decrypt and deserialize it.
12
+ #
13
+ # @param encrypted_hash [Hash] the encrypted hash to decrypt
14
+ # @param secret [String]
15
+ # @param metadata [Hash] the optional metdata to configure the decryptor
16
+ # @return [SecureDataBag::NestedDecryptor] the object capable of decrypting
17
+ # @since 3.0.0
18
+ def self.for(encrypted_hash, secret, metadata = {})
19
+ metadata = Mash.new(metadata)
20
+ NestedDecryptor.new(encrypted_hash, secret, metadata)
21
+ end
22
+ end
23
+
24
+ # Decryptor object responsable for decrypting the encrypted_hash with the
25
+ # secret. This functions similarly, to how
26
+ # Chef::EncryptedDataBagItem::Decryptor does, with the caveat that this
27
+ # is meant to decrypt entire objects and not single values.
28
+ #
29
+ # @since 3.0.0
30
+ class NestedDecryptor
31
+ include SecureDataBag::CheckEncrypted
32
+
33
+ # The encrypted hash received
34
+ # @since 3.0.0
35
+ attr_reader :encrypted_hash
36
+
37
+ # The keys found that had to be decrypted in the hash
38
+ # @since 3.0.0
39
+ attr_reader :decrypted_keys
40
+
41
+ # The decrypted hash
42
+ # @since 3.0.0
43
+ attr_reader :decrypted_hash
44
+
45
+ # The format of this DataBagItem.
46
+ # May be one of:
47
+ # - encrypted refers to an EncryptedDataBagItem
48
+ # - nested refers to a SecureDataBagItem with nested values
49
+ # - plain refers to a plain DataBagItem
50
+ # @since 3.0.0
51
+ attr_reader :format
52
+
53
+ # Initializer
54
+ # @param encrypted_hash [Hash,String] the encrypted hash to decrypt
55
+ # @param secret [String] the secret to decrypt with
56
+ # @param metadata [Hash] the optional metdata to configure the decryptor
57
+ # @since 3.0.0
58
+ def initialize(encrypted_hash, secret, metadata = {})
59
+ @secret = secret
60
+
61
+ @decrypted_keys = []
62
+ @encrypted_hash = encrypted_hash
63
+ @decrypted_hash = {}
64
+
65
+ @format = metadata[:decryption_format] ||
66
+ if @encrypted_hash.key?(SecureDataBag::METADATA_KEY)
67
+ 'nested'
68
+ elsif encrypted?(@encrypted_hash)
69
+ 'encrypted'
70
+ elsif partially_encrypted?(@encrypted_hash)
71
+ 'nested'
72
+ else
73
+ 'plain'
74
+ end
75
+ end
76
+
77
+ # Method called to decrypt the data structure and return it.
78
+ # @return [Mix] the unencrypted value
79
+ # @since 3.0.0
80
+ def decrypt!
81
+ @decrypted_hash = decrypt
82
+ end
83
+
84
+ # Method called to decrypt the data structure and return it.
85
+ # @return [Mix] the unencrypted value
86
+ # @since 3.0.0
87
+ def decrypt
88
+ decrypt_data(@encrypted_hash)
89
+ end
90
+
91
+ # Method name preserved for compatibility with
92
+ # Chef::EncryptedDataBagItem::Decryptor.
93
+ # @since 3.0.0
94
+ alias :for_decrypted_item :decrypt!
95
+
96
+ private
97
+
98
+ # Decrypt a possibly encrypted value
99
+ # @param raw_hash [Hash] a potentially encrypted hash
100
+ # @return [Hash] the unencrypted value
101
+ # @since 3.0.0
102
+ def decrypt_data(raw_hash)
103
+ if looks_like_encrypted?(raw_hash)
104
+ decrypt_value(raw_hash)
105
+ else
106
+ decrypt_hash(raw_hash)
107
+ end
108
+ end
109
+
110
+ # Decrypt a hash potentially containing nested encrypted values
111
+ #
112
+ # Additionally, this method will attempt tovkeep track of the names of
113
+ # each encrypted key.
114
+ #
115
+ # @param hash [Hash] a potentially encrypted hash
116
+ # @return [Hash] the unencrypted value
117
+ # @since 3.0.0
118
+ def decrypt_hash(hash)
119
+ decrypted_hash = Mash.new
120
+
121
+ hash.each do |key, value|
122
+ value = if looks_like_encrypted?(value)
123
+ @decrypted_keys.push(key) unless @decrypted_keys
124
+ .include?(key)
125
+ decrypt_value(value)
126
+ elsif value.is_a?(Hash)
127
+ decrypt_hash(value)
128
+ else value
129
+ end
130
+ decrypted_hash[key] = value
131
+ end
132
+
133
+ decrypted_hash
134
+ end
135
+
136
+ # Decrypt an encrypted value
137
+ # @param hash [Hash] the encrypted value as a hash
138
+ # @return [Mix] the unencrypted value
139
+ # @since 3.0.0
140
+ def decrypt_value(value)
141
+ case @format
142
+ when 'plain' then value
143
+ else
144
+ Chef::EncryptedDataBagItem::Decryptor
145
+ .for(value, @secret).for_decrypted_item
146
+ end
147
+ end
148
+ end
149
+ end