chef-encrypted-attributes 0.1.0

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