chef-encrypted-attributes 0.1.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.
Files changed (34) hide show
  1. checksums.yaml +7 -0
  2. data/API.md +163 -0
  3. data/CHANGELOG.md +7 -0
  4. data/INTERNAL.md +111 -0
  5. data/LICENSE +190 -0
  6. data/README.md +330 -0
  7. data/Rakefile +46 -0
  8. data/TESTING.md +45 -0
  9. data/TODO.md +20 -0
  10. data/lib/chef-encrypted-attributes.rb +19 -0
  11. data/lib/chef/encrypted_attribute.rb +218 -0
  12. data/lib/chef/encrypted_attribute/cache_lru.rb +74 -0
  13. data/lib/chef/encrypted_attribute/config.rb +200 -0
  14. data/lib/chef/encrypted_attribute/encrypted_mash.rb +122 -0
  15. data/lib/chef/encrypted_attribute/encrypted_mash/version0.rb +143 -0
  16. data/lib/chef/encrypted_attribute/encrypted_mash/version1.rb +140 -0
  17. data/lib/chef/encrypted_attribute/exceptions.rb +38 -0
  18. data/lib/chef/encrypted_attribute/local_node.rb +38 -0
  19. data/lib/chef/encrypted_attribute/remote_clients.rb +46 -0
  20. data/lib/chef/encrypted_attribute/remote_node.rb +111 -0
  21. data/lib/chef/encrypted_attribute/remote_users.rb +73 -0
  22. data/lib/chef/encrypted_attribute/search_helper.rb +144 -0
  23. data/lib/chef/encrypted_attribute/version.rb +23 -0
  24. data/lib/chef/knife/core/config.rb +19 -0
  25. data/lib/chef/knife/core/encrypted_attribute_editor_options.rb +100 -0
  26. data/lib/chef/knife/encrypted_attribute_create.rb +67 -0
  27. data/lib/chef/knife/encrypted_attribute_delete.rb +71 -0
  28. data/lib/chef/knife/encrypted_attribute_edit.rb +68 -0
  29. data/lib/chef/knife/encrypted_attribute_show.rb +86 -0
  30. data/lib/chef/knife/encrypted_attribute_update.rb +65 -0
  31. data/spec/benchmark_helper.rb +32 -0
  32. data/spec/integration_helper.rb +20 -0
  33. data/spec/spec_helper.rb +38 -0
  34. metadata +204 -0
@@ -0,0 +1,46 @@
1
+ #
2
+ # Author:: Xabier de Zuazo (<xabier@onddo.com>)
3
+ # Copyright:: Copyright (c) 2014 Onddo Labs, SL. (www.onddo.com)
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ require 'chef/encrypted_attribute/search_helper'
20
+ require 'chef/encrypted_attribute/cache_lru'
21
+
22
+ class Chef
23
+ class EncryptedAttribute
24
+ class RemoteClients
25
+ extend ::Chef::EncryptedAttribute::SearchHelper
26
+
27
+ def self.cache
28
+ @@cache ||= Chef::EncryptedAttribute::CacheLru.new
29
+ end
30
+
31
+ def self.get_public_keys(search='*:*', partial_search=true)
32
+ escaped_query = escape_query(search)
33
+ if cache.has_key?(escaped_query)
34
+ cache[escaped_query]
35
+ else
36
+ cache[escaped_query] = search(:client, search, {
37
+ 'public_key' => [ 'public_key' ]
38
+ }, 1000, partial_search).map do |client|
39
+ client['public_key']
40
+ end.compact
41
+ end
42
+ end
43
+
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,111 @@
1
+ #
2
+ # Author:: Xabier de Zuazo (<xabier@onddo.com>)
3
+ # Copyright:: Copyright (c) 2014 Onddo Labs, SL. (www.onddo.com)
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ require 'chef/mixin/params_validate'
20
+ require 'chef/encrypted_attribute/search_helper'
21
+ require 'chef/encrypted_attribute/cache_lru'
22
+
23
+ class Chef
24
+ class EncryptedAttribute
25
+ class RemoteNode
26
+ include ::Chef::Mixin::ParamsValidate
27
+ include ::Chef::EncryptedAttribute::SearchHelper
28
+
29
+ def initialize(name)
30
+ name(name)
31
+ end
32
+
33
+ def self.cache
34
+ @@cache ||= Chef::EncryptedAttribute::CacheLru.new(0) # disabled by default
35
+ end
36
+
37
+ def name(arg=nil)
38
+ set_or_return(
39
+ :name,
40
+ arg,
41
+ :kind_of => String
42
+ )
43
+ end
44
+
45
+ def load_attribute(attr_ary, partial_search=true)
46
+ unless attr_ary.kind_of?(Array)
47
+ raise ArgumentError, "#{self.class.to_s}##{__method__} attr_ary argument must be an array of strings. You passed #{attr_ary.inspect}."
48
+ end
49
+ cache_key = cache_key(name, attr_ary)
50
+ if self.class.cache.has_key?(cache_key)
51
+ self.class.cache[cache_key]
52
+ else
53
+ keys = { 'value' => attr_ary }
54
+ res = search(:node, "name:#{@name}", keys, 1, partial_search)
55
+ self.class.cache[cache_key] = if res.kind_of?(Array) and
56
+ res[0].kind_of?(Hash) and res[0].has_key?('value')
57
+ res[0]['value']
58
+ else
59
+ nil
60
+ end
61
+ end
62
+ end
63
+
64
+ def save_attribute(attr_ary, value)
65
+ unless attr_ary.kind_of?(Array)
66
+ raise ArgumentError, "#{self.class.to_s}##{__method__} attr_ary argument must be an array of strings. You passed #{attr_ary.inspect}."
67
+ end
68
+ cache_key = cache_key(name, attr_ary)
69
+
70
+ node = Chef::Node.load(name)
71
+ last = attr_ary.pop
72
+ node_attr = attr_ary.reduce(node.normal) do |a, k|
73
+ a[k] = Mash.new unless a.has_key?(k)
74
+ a[k]
75
+ end
76
+ node_attr[last] = value
77
+
78
+ node.save
79
+ self.class.cache[cache_key] = value
80
+ end
81
+
82
+ def delete_attribute(attr_ary)
83
+ unless attr_ary.kind_of?(Array)
84
+ raise ArgumentError, "#{self.class.to_s}##{__method__} attr_ary argument must be an array of strings. You passed #{attr_ary.inspect}."
85
+ end
86
+ cache_key = cache_key(name, attr_ary)
87
+
88
+ node = Chef::Node.load(name)
89
+ last = attr_ary.pop
90
+ node_attr = attr_ary.reduce(node.normal) do |a, k|
91
+ a.respond_to?(:has_key?) && a.has_key?(k) ? a[k] : nil
92
+ end
93
+ if node_attr.respond_to?(:has_key?) && node_attr.has_key?(last)
94
+ node_attr.delete(last)
95
+ node.save
96
+ self.class.cache.delete(cache_key)
97
+ true
98
+ else
99
+ false
100
+ end
101
+ end
102
+
103
+ protected
104
+
105
+ def cache_key(name, attr_ary)
106
+ "#{name}:#{attr_ary.inspect}" # TODO ok, this can be improved
107
+ end
108
+
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,73 @@
1
+ #
2
+ # Author:: Xabier de Zuazo (<xabier@onddo.com>)
3
+ # Copyright:: Copyright (c) 2014 Onddo Labs, SL. (www.onddo.com)
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ require 'chef/user'
20
+ require 'chef/encrypted_attribute/exceptions'
21
+ require 'chef/encrypted_attribute/cache_lru'
22
+
23
+ class Chef
24
+ class EncryptedAttribute
25
+ class RemoteUsers
26
+
27
+ def self.cache
28
+ @@cache ||= Chef::EncryptedAttribute::CacheLru.new
29
+ end
30
+
31
+ def self.get_public_keys(users=[])
32
+ if users == '*' # users are [a-z0-9\-_]+, cannot be *
33
+ if cache.has_key?('*')
34
+ cache['*']
35
+ else
36
+ cache['*'] = get_all_public_keys
37
+ end
38
+ elsif users.kind_of?(Array)
39
+ get_users_public_keys(users)
40
+ elsif not users.nil?
41
+ raise ArgumentError, "#{self.class.to_s}##{__method__} users argument must be an array or \"*\"."
42
+ end
43
+ end
44
+
45
+ protected
46
+
47
+ def self.get_user_public_key(name)
48
+ return cache[name] if cache.has_key?(name)
49
+ user = Chef::User.load(name)
50
+ cache[name] = user.public_key
51
+ rescue Net::HTTPServerException => e
52
+ case e.response.code
53
+ when '403'
54
+ raise InsufficientPrivileges, 'Your node needs admin privileges to be able to work with Chef Users.'
55
+ when '404' # Not Found
56
+ raise UserNotFound, "Chef User not found: \"#{name}\"."
57
+ else
58
+ raise e
59
+ end
60
+ end
61
+
62
+ def self.get_users_public_keys(users)
63
+ users.map { |n| get_user_public_key(n) }
64
+ end
65
+
66
+ def self.get_all_public_keys
67
+ # Chef::User.list(inflate=true) has a bug
68
+ get_users_public_keys(Chef::User.list.keys)
69
+ end
70
+
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,144 @@
1
+ #
2
+ # Author:: Xabier de Zuazo (<xabier@onddo.com>)
3
+ # Copyright:: Copyright (c) 2014 Onddo Labs, SL. (www.onddo.com)
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ require 'chef/search/query'
20
+ require 'chef/encrypted_attribute/exceptions'
21
+
22
+ class Chef
23
+ class EncryptedAttribute
24
+ module SearchHelper
25
+ extend self
26
+
27
+ def query
28
+ Chef::Search::Query.new
29
+ end
30
+
31
+ def escape(str)
32
+ URI.escape(str.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
33
+ end
34
+
35
+ def escape_query(query)
36
+ query_s = if query.kind_of?(Array)
37
+ query.map do |item|
38
+ "( #{item} )"
39
+ end.compact.join(' OR ')
40
+ else
41
+ query.to_s
42
+ end
43
+ escape(query_s)
44
+ end
45
+
46
+ def valid_search_keys?(keys)
47
+ return false unless keys.kind_of?(Hash)
48
+ keys.reduce(true) do |r, (k, v)|
49
+ r && unless k.kind_of?(String) || k.kind_of?(Symbol) and v.kind_of?(Array)
50
+ false
51
+ else
52
+ v.reduce(true) do |r, x|
53
+ r and x.kind_of?(String)
54
+ end
55
+ end
56
+ end
57
+ end
58
+
59
+ def empty_search?(query)
60
+ query.kind_of?(String) && query.empty? or
61
+ query.kind_of?(Array) && query.count == 0
62
+ end
63
+
64
+ def search(type, query, keys, rows=1000, partial_search=true)
65
+ return [] if empty_search?(query) # avoid empty searches
66
+ if partial_search
67
+ partial_search(type, query, keys, rows)
68
+ else
69
+ normal_search(type, query, keys, rows)
70
+ end
71
+ end
72
+
73
+ def normal_search(type, query, keys, rows=1000)
74
+ escaped_query = escape_query(query)
75
+ Chef::Log.info("Normal Search query: #{escaped_query}, keys: #{keys.inspect}")
76
+ unless valid_search_keys?(keys)
77
+ raise InvalidSearchKeys, "Invalid search keys: #{keys.inspect}"
78
+ end
79
+
80
+ begin
81
+ resp = self.query.search(type, escaped_query, nil, 0, rows)[0]
82
+ rescue Net::HTTPServerException => e
83
+ if e.response.kind_of?(Net::HTTPResponse) and e.response.code == '404' # Not Found
84
+ return []
85
+ else
86
+ raise SearchFailure, "Partial Search exception #{e.class.name}: #{e.to_s}"
87
+ end
88
+ rescue Net::HTTPFatalError => e
89
+ raise SearchFailure, "Normal Search exception #{e.class.name}: #{e.to_s}"
90
+ end
91
+ unless resp.kind_of?(Array)
92
+ raise SearchFatalError, "Wrong response received from Normal Search: #{resp.inspect}"
93
+ end
94
+ # TODO too complex, refactorize
95
+ resp.map do |row|
96
+ Hash[keys.map do |key_name, attr_ary|
97
+ value = attr_ary.reduce(row) do |r, attr|
98
+ if r.respond_to?(attr.to_sym)
99
+ r.send(attr.to_sym)
100
+ elsif r.respond_to?(:has_key?)
101
+ if r.has_key?(attr.to_s)
102
+ r[attr.to_s]
103
+ end
104
+ end
105
+ end
106
+ [ key_name, value ]
107
+ end]
108
+ end
109
+ end
110
+
111
+ def partial_search(type, query, keys, rows=1000)
112
+ escaped_query = "search/#{escape(type)}?q=#{escape_query(query)}&start=0&rows=#{rows}"
113
+ Chef::Log.info("Partial Search query: #{escaped_query}, keys: #{keys.inspect}")
114
+ unless valid_search_keys?(keys)
115
+ raise InvalidSearchKeys, "Invalid search keys: #{keys.inspect}"
116
+ end
117
+
118
+ rest = Chef::REST.new(Chef::Config[:chef_server_url])
119
+ begin
120
+ resp = rest.post_rest(escaped_query, keys)
121
+ rescue Net::HTTPServerException => e
122
+ if e.response.kind_of?(Net::HTTPResponse) and e.response.code == '404' # Not Found
123
+ return []
124
+ else
125
+ raise SearchFailure, "Partial Search exception #{e.class.name}: #{e.to_s}"
126
+ end
127
+ rescue Net::HTTPFatalError => e
128
+ raise SearchFailure, "Partial Search exception #{e.class.name}: #{e.to_s}"
129
+ end
130
+ unless resp.kind_of?(Hash) and resp.has_key?('rows') and resp['rows'].kind_of?(Array)
131
+ raise SearchFatalError, "Wrong response received from Partial Search: #{resp.inspect}"
132
+ end
133
+ resp['rows'].map do |row|
134
+ if row.kind_of?(Hash) and row['data'].kind_of?(Hash)
135
+ row['data']
136
+ else
137
+ raise SearchFatalError, "Wrong row format received from Partial Search: #{row.inspect}"
138
+ end
139
+ end.compact
140
+ end
141
+
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,23 @@
1
+ #
2
+ # Author:: Xabier de Zuazo (<xabier@onddo.com>)
3
+ # Copyright:: Copyright (c) 2014 Onddo Labs, SL. (www.onddo.com)
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ class Chef
20
+ class EncryptedAttribute
21
+ VERSION = '0.1.0'
22
+ end
23
+ end
@@ -0,0 +1,19 @@
1
+ #
2
+ # Author:: Xabier de Zuazo (<xabier@onddo.com>)
3
+ # Copyright:: Copyright (c) 2014 Onddo Labs, SL. (www.onddo.com)
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ Chef::Config[:knife][:encrypted_attributes] = Mash.new unless Chef::Config[:knife][:encrypted_attributes].kind_of?(Hash)
@@ -0,0 +1,100 @@
1
+ #
2
+ # Author:: Xabier de Zuazo (<xabier@onddo.com>)
3
+ # Copyright:: Copyright (c) 2014 Onddo Labs, SL. (www.onddo.com)
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ require 'chef/knife/core/config'
20
+
21
+ class Chef
22
+ class Knife
23
+ module Core
24
+
25
+ module EncryptedAttributeEditorOptions
26
+ def self.included(includer)
27
+ includer.class_eval do
28
+
29
+ deps do
30
+ require 'chef/encrypted_attribute'
31
+ require 'chef/json_compat'
32
+ end
33
+
34
+ option :encrypted_attribute_version,
35
+ :long => '--encrypted-attribute-version VERSION',
36
+ :description => 'Encrypted Attribute protocol version to use',
37
+ :proc => lambda { |i| Chef::Config[:knife][:encrypted_attributes][:version] = i }
38
+
39
+ option :encrypted_attribute_partial_search,
40
+ :short => '-P',
41
+ :long => '--disable-partial-search',
42
+ :description => 'Disable partial search',
43
+ :boolean => true,
44
+ :proc => lambda { |i| Chef::Config[:knife][:encrypted_attributes][:partial_search] = false }
45
+
46
+ option :encrypted_attribute_client_search,
47
+ :short => '-C CLIENT_SEARCH_QUERY',
48
+ :long => '--client-search CLIENT_SEARCH_QUERY',
49
+ :description => 'Client search query. Can be specified multiple times',
50
+ :proc => lambda { |i|
51
+ Chef::Config[:knife][:encrypted_attributes][:client_search] = [] unless Chef::Config[:knife][:encrypted_attributes][:client_search].kind_of?(Array)
52
+ Chef::Config[:knife][:encrypted_attributes][:client_search] << i
53
+ }
54
+
55
+ option :encrypted_attribute_users,
56
+ :short => '-U USER',
57
+ :long => '--encrypted-attribute-user USER',
58
+ :description => 'User name to allow access to. Can be specified multiple times',
59
+ :proc => lambda { |i|
60
+ Chef::Config[:knife][:encrypted_attributes][:users] = [] unless Chef::Config[:knife][:encrypted_attributes][:users].kind_of?(Array)
61
+ Chef::Config[:knife][:encrypted_attributes][:users] << i
62
+ }
63
+
64
+ # TODO option :keys
65
+
66
+ # Modified Chef::Knife::UI#edit_data method with plain text format support
67
+ def edit_data(data=nil, format='plain')
68
+ output = case format
69
+ when 'JSON', 'json'
70
+ data.nil? ? {} : Chef::JSONCompat.to_json_pretty(data, {:quirks_mode => true})
71
+ else
72
+ data.nil? ? '' : data
73
+ end
74
+
75
+ if !config[:disable_editing]
76
+ Tempfile.open([ 'knife-edit-', '.json' ]) do |tf|
77
+ tf.sync = true
78
+ tf.puts output
79
+ tf.close
80
+ raise 'Please set EDITOR environment variable' unless system("#{config[:editor]} #{tf.path}")
81
+
82
+ output = IO.read(tf.path)
83
+ tf.unlink # not needed, but recommended
84
+ end
85
+ end
86
+
87
+ case format
88
+ when 'JSON', 'json'
89
+ Yajl::Parser.parse(output)
90
+ else
91
+ output
92
+ end
93
+ end # def edit_data
94
+
95
+ end # includer.class_eval
96
+ end # self.included(includer)
97
+ end # EncryptedAttributeEditorOptions
98
+ end # Core
99
+ end # Knife
100
+ end # Chef