secure_data_bag 2.2.0 → 3.0.0

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