chef-zero 15.0.17 → 15.0.21
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.
- checksums.yaml +4 -4
- data/Gemfile +36 -31
- data/LICENSE +201 -201
- data/Rakefile +73 -68
- data/bin/chef-zero +111 -111
- data/chef-zero.gemspec +34 -33
- data/lib/chef_zero/chef_data/acl_path.rb +140 -140
- data/lib/chef_zero/chef_data/cookbook_data.rb +237 -237
- data/lib/chef_zero/chef_data/data_normalizer.rb +276 -276
- data/lib/chef_zero/chef_data/default_creator.rb +476 -476
- data/lib/chef_zero/data_store/data_already_exists_error.rb +29 -29
- data/lib/chef_zero/data_store/data_error.rb +32 -32
- data/lib/chef_zero/data_store/data_not_found_error.rb +29 -29
- data/lib/chef_zero/data_store/default_facade.rb +143 -147
- data/lib/chef_zero/data_store/interface_v1.rb +67 -67
- data/lib/chef_zero/data_store/interface_v2.rb +18 -18
- data/lib/chef_zero/data_store/memory_store.rb +33 -33
- data/lib/chef_zero/data_store/memory_store_v2.rb +159 -159
- data/lib/chef_zero/data_store/raw_file_store.rb +143 -143
- data/lib/chef_zero/data_store/v1_to_v2_adapter.rb +150 -150
- data/lib/chef_zero/data_store/v2_to_v1_adapter.rb +105 -105
- data/lib/chef_zero/dist.rb +9 -9
- data/lib/chef_zero/endpoints/acl_endpoint.rb +39 -39
- data/lib/chef_zero/endpoints/acls_endpoint.rb +41 -41
- data/lib/chef_zero/endpoints/actor_default_key_endpoint.rb +78 -78
- data/lib/chef_zero/endpoints/actor_endpoint.rb +184 -184
- data/lib/chef_zero/endpoints/actor_key_endpoint.rb +62 -62
- data/lib/chef_zero/endpoints/actor_keys_endpoint.rb +129 -129
- data/lib/chef_zero/endpoints/actors_endpoint.rb +104 -104
- data/lib/chef_zero/endpoints/authenticate_user_endpoint.rb +32 -32
- data/lib/chef_zero/endpoints/container_endpoint.rb +22 -22
- data/lib/chef_zero/endpoints/containers_endpoint.rb +25 -25
- data/lib/chef_zero/endpoints/controls_endpoint.rb +16 -16
- data/lib/chef_zero/endpoints/cookbook_artifact_endpoint.rb +24 -24
- data/lib/chef_zero/endpoints/cookbook_artifact_identifier_endpoint.rb +68 -68
- data/lib/chef_zero/endpoints/cookbook_artifacts_endpoint.rb +34 -34
- data/lib/chef_zero/endpoints/cookbook_endpoint.rb +39 -39
- data/lib/chef_zero/endpoints/cookbook_version_endpoint.rb +136 -136
- data/lib/chef_zero/endpoints/cookbooks_base.rb +80 -80
- data/lib/chef_zero/endpoints/cookbooks_endpoint.rb +19 -19
- data/lib/chef_zero/endpoints/data_bag_endpoint.rb +45 -45
- data/lib/chef_zero/endpoints/data_bag_item_endpoint.rb +25 -25
- data/lib/chef_zero/endpoints/data_bags_endpoint.rb +23 -23
- data/lib/chef_zero/endpoints/dummy_endpoint.rb +29 -29
- data/lib/chef_zero/endpoints/environment_cookbook_endpoint.rb +24 -24
- data/lib/chef_zero/endpoints/environment_cookbook_versions_endpoint.rb +126 -126
- data/lib/chef_zero/endpoints/environment_cookbooks_endpoint.rb +22 -22
- data/lib/chef_zero/endpoints/environment_endpoint.rb +33 -33
- data/lib/chef_zero/endpoints/environment_nodes_endpoint.rb +23 -23
- data/lib/chef_zero/endpoints/environment_recipes_endpoint.rb +22 -22
- data/lib/chef_zero/endpoints/environment_role_endpoint.rb +36 -36
- data/lib/chef_zero/endpoints/file_store_file_endpoint.rb +22 -22
- data/lib/chef_zero/endpoints/group_endpoint.rb +20 -20
- data/lib/chef_zero/endpoints/groups_endpoint.rb +13 -13
- data/lib/chef_zero/endpoints/license_endpoint.rb +25 -25
- data/lib/chef_zero/endpoints/node_endpoint.rb +34 -34
- data/lib/chef_zero/endpoints/node_identifiers_endpoint.rb +22 -22
- data/lib/chef_zero/endpoints/nodes_endpoint.rb +34 -34
- data/lib/chef_zero/endpoints/not_found_endpoint.rb +11 -11
- data/lib/chef_zero/endpoints/organization_association_request_endpoint.rb +22 -22
- data/lib/chef_zero/endpoints/organization_association_requests_endpoint.rb +30 -30
- data/lib/chef_zero/endpoints/organization_authenticate_user_endpoint.rb +26 -26
- data/lib/chef_zero/endpoints/organization_endpoint.rb +47 -47
- data/lib/chef_zero/endpoints/organization_user_base.rb +15 -15
- data/lib/chef_zero/endpoints/organization_user_default_key_endpoint.rb +16 -16
- data/lib/chef_zero/endpoints/organization_user_endpoint.rb +26 -26
- data/lib/chef_zero/endpoints/organization_user_key_endpoint.rb +17 -17
- data/lib/chef_zero/endpoints/organization_user_keys_endpoint.rb +17 -17
- data/lib/chef_zero/endpoints/organization_users_endpoint.rb +43 -43
- data/lib/chef_zero/endpoints/organization_validator_key_endpoint.rb +20 -20
- data/lib/chef_zero/endpoints/organizations_endpoint.rb +61 -61
- data/lib/chef_zero/endpoints/policies_endpoint.rb +26 -26
- data/lib/chef_zero/endpoints/policy_endpoint.rb +24 -24
- data/lib/chef_zero/endpoints/policy_group_endpoint.rb +46 -46
- data/lib/chef_zero/endpoints/policy_group_policy_endpoint.rb +83 -83
- data/lib/chef_zero/endpoints/policy_groups_endpoint.rb +38 -38
- data/lib/chef_zero/endpoints/policy_revision_endpoint.rb +66 -66
- data/lib/chef_zero/endpoints/policy_revisions_endpoint.rb +15 -15
- data/lib/chef_zero/endpoints/principal_endpoint.rb +55 -55
- data/lib/chef_zero/endpoints/rest_list_endpoint.rb +42 -42
- data/lib/chef_zero/endpoints/rest_object_endpoint.rb +78 -78
- data/lib/chef_zero/endpoints/role_endpoint.rb +16 -16
- data/lib/chef_zero/endpoints/role_environments_endpoint.rb +14 -14
- data/lib/chef_zero/endpoints/sandbox_endpoint.rb +27 -27
- data/lib/chef_zero/endpoints/sandboxes_endpoint.rb +51 -51
- data/lib/chef_zero/endpoints/search_endpoint.rb +208 -208
- data/lib/chef_zero/endpoints/searches_endpoint.rb +18 -18
- data/lib/chef_zero/endpoints/server_api_version_endpoint.rb +14 -14
- data/lib/chef_zero/endpoints/system_recovery_endpoint.rb +30 -30
- data/lib/chef_zero/endpoints/universe_endpoint.rb +15 -15
- data/lib/chef_zero/endpoints/user_association_request_endpoint.rb +41 -41
- data/lib/chef_zero/endpoints/user_association_requests_count_endpoint.rb +19 -19
- data/lib/chef_zero/endpoints/user_association_requests_endpoint.rb +19 -19
- data/lib/chef_zero/endpoints/user_organizations_endpoint.rb +22 -22
- data/lib/chef_zero/endpoints/version_endpoint.rb +13 -13
- data/lib/chef_zero/log.rb +7 -7
- data/lib/chef_zero/rest_base.rb +332 -332
- data/lib/chef_zero/rest_error_response.rb +11 -11
- data/lib/chef_zero/rest_request.rb +84 -88
- data/lib/chef_zero/rest_router.rb +72 -72
- data/lib/chef_zero/rspec.rb +355 -355
- data/lib/chef_zero/server.rb +730 -730
- data/lib/chef_zero/socketless_server_map.rb +92 -93
- data/lib/chef_zero/solr/query/binary_operator.rb +52 -52
- data/lib/chef_zero/solr/query/phrase.rb +23 -23
- data/lib/chef_zero/solr/query/range_query.rb +46 -46
- data/lib/chef_zero/solr/query/regexpable_query.rb +30 -30
- data/lib/chef_zero/solr/query/subquery.rb +37 -37
- data/lib/chef_zero/solr/query/term.rb +45 -45
- data/lib/chef_zero/solr/query/unary_operator.rb +41 -41
- data/lib/chef_zero/solr/solr_doc.rb +53 -53
- data/lib/chef_zero/solr/solr_parser.rb +208 -208
- data/lib/chef_zero/version.rb +3 -3
- data/lib/chef_zero.rb +10 -10
- data/spec/run_oc_pedant.rb +226 -226
- data/spec/search_spec.rb +36 -36
- data/spec/server_spec.rb +96 -96
- data/spec/socketless_server_map_spec.rb +74 -74
- data/spec/support/oc_pedant.rb +149 -149
- data/spec/support/secrets.json +6 -6
- data/spec/support/stickywicket.pem +27 -27
- metadata +35 -18
data/lib/chef_zero/rest_base.rb
CHANGED
@@ -1,332 +1,332 @@
|
|
1
|
-
require_relative "rest_request"
|
2
|
-
require_relative "rest_error_response"
|
3
|
-
require_relative "data_store/data_not_found_error"
|
4
|
-
require_relative "chef_data/acl_path"
|
5
|
-
|
6
|
-
module ChefZero
|
7
|
-
class RestBase
|
8
|
-
DEFAULT_REQUEST_VERSION = 0
|
9
|
-
DEFAULT_RESPONSE_VERSION = 0
|
10
|
-
|
11
|
-
def initialize(server)
|
12
|
-
@server = server
|
13
|
-
end
|
14
|
-
|
15
|
-
attr_reader :server
|
16
|
-
|
17
|
-
def data_store
|
18
|
-
server.data_store
|
19
|
-
end
|
20
|
-
|
21
|
-
def check_api_version(request)
|
22
|
-
version = request.api_version
|
23
|
-
|
24
|
-
if version > MAX_API_VERSION || version < MIN_API_VERSION
|
25
|
-
response = {
|
26
|
-
"error" => "invalid-x-ops-server-api-version",
|
27
|
-
"message" => "Specified version #{version} not supported",
|
28
|
-
"min_api_version" => MIN_API_VERSION,
|
29
|
-
"max_api_version" => MAX_API_VERSION,
|
30
|
-
}
|
31
|
-
|
32
|
-
|
33
|
-
response,
|
34
|
-
request_version: version, response_version: -1)
|
35
|
-
end
|
36
|
-
rescue ArgumentError
|
37
|
-
json_response(406,
|
38
|
-
{ "username" => request.requestor },
|
39
|
-
request_version: -1, response_version: -1)
|
40
|
-
end
|
41
|
-
|
42
|
-
def call(request)
|
43
|
-
response = check_api_version(request)
|
44
|
-
return response unless response.nil?
|
45
|
-
|
46
|
-
method = request.method.downcase.to_sym
|
47
|
-
unless respond_to?(method)
|
48
|
-
accept_methods = %i{get put post delete}.select { |m| respond_to?(m) }
|
49
|
-
accept_methods_str = accept_methods.map { |m| m.to_s.upcase }.join(", ")
|
50
|
-
return [405, { "Content-Type" => "text/plain", "Allow" => accept_methods_str }, "Bad request method for '#{request.env["REQUEST_PATH"]}': #{request.env["REQUEST_METHOD"]}"]
|
51
|
-
end
|
52
|
-
if json_only && !accepts?(request, "application", "json")
|
53
|
-
return [406, { "Content-Type" => "text/plain" }, "Must accept application/json"]
|
54
|
-
end
|
55
|
-
|
56
|
-
# Dispatch to get()/post()/put()/delete()
|
57
|
-
begin
|
58
|
-
send(method, request)
|
59
|
-
rescue RestErrorResponse => e
|
60
|
-
ChefZero::Log.debug("#{e.inspect}\n#{e.backtrace.join("\n")}")
|
61
|
-
error(e.response_code, e.error)
|
62
|
-
end
|
63
|
-
end
|
64
|
-
|
65
|
-
def json_only
|
66
|
-
true
|
67
|
-
end
|
68
|
-
|
69
|
-
def accepts?(request, category, type)
|
70
|
-
# If HTTP_ACCEPT is not sent at all, assume it accepts anything
|
71
|
-
# This parses as per https://datatracker.ietf.org/doc/html/rfc7231#section-5.3
|
72
|
-
return true unless request.env["HTTP_ACCEPT"]
|
73
|
-
|
74
|
-
accepts = request.env["HTTP_ACCEPT"].split(/,\s*/).map { |x| x.split(";", 2)[0].strip }
|
75
|
-
accepts.include?("#{category}/#{type}") || accepts.include?("#{category}/*") || accepts.include?("*/*")
|
76
|
-
end
|
77
|
-
|
78
|
-
def get_data(request, rest_path = nil, *options)
|
79
|
-
rest_path ||= request.rest_path
|
80
|
-
rest_path = rest_path.map { |v| self.class.rfc2396_parser.unescape(v) }
|
81
|
-
begin
|
82
|
-
data_store.get(rest_path, request)
|
83
|
-
rescue DataStore::DataNotFoundError
|
84
|
-
if options.include?(:nil)
|
85
|
-
nil
|
86
|
-
elsif options.include?(:data_store_exceptions)
|
87
|
-
raise
|
88
|
-
else
|
89
|
-
raise RestErrorResponse.new(404, "Object not found: #{build_uri(request.base_uri, rest_path)}")
|
90
|
-
end
|
91
|
-
end
|
92
|
-
end
|
93
|
-
|
94
|
-
def list_data(request, rest_path = nil, *options)
|
95
|
-
rest_path ||= request.rest_path
|
96
|
-
begin
|
97
|
-
data_store.list(rest_path)
|
98
|
-
rescue DataStore::DataNotFoundError
|
99
|
-
if options.include?(:data_store_exceptions)
|
100
|
-
raise
|
101
|
-
else
|
102
|
-
raise RestErrorResponse.new(404, "Object not found: #{build_uri(request.base_uri, rest_path)}")
|
103
|
-
end
|
104
|
-
end
|
105
|
-
end
|
106
|
-
|
107
|
-
def delete_data(request, rest_path = nil, *options)
|
108
|
-
rest_path ||= request.rest_path
|
109
|
-
begin
|
110
|
-
data_store.delete(rest_path, *options)
|
111
|
-
rescue DataStore::DataNotFoundError
|
112
|
-
if options.include?(:data_store_exceptions)
|
113
|
-
raise
|
114
|
-
else
|
115
|
-
raise RestErrorResponse.new(404, "Object not found: #{build_uri(request.base_uri, rest_path)}")
|
116
|
-
end
|
117
|
-
end
|
118
|
-
|
119
|
-
begin
|
120
|
-
acl_path = ChefData::AclPath.get_acl_data_path(rest_path)
|
121
|
-
data_store.delete(acl_path) if acl_path
|
122
|
-
rescue DataStore::DataNotFoundError
|
123
|
-
end
|
124
|
-
end
|
125
|
-
|
126
|
-
def delete_data_dir(request, rest_path, *options)
|
127
|
-
rest_path ||= request.rest_path
|
128
|
-
begin
|
129
|
-
data_store.delete_dir(rest_path, *options)
|
130
|
-
rescue DataStore::DataNotFoundError
|
131
|
-
if options.include?(:data_store_exceptions)
|
132
|
-
raise
|
133
|
-
else
|
134
|
-
raise RestErrorResponse.new(404, "Object not found: #{build_uri(request.base_uri, rest_path)}")
|
135
|
-
end
|
136
|
-
end
|
137
|
-
|
138
|
-
begin
|
139
|
-
acl_path = ChefData::AclPath.get_acl_data_path(rest_path)
|
140
|
-
data_store.delete(acl_path) if acl_path
|
141
|
-
rescue DataStore::DataNotFoundError
|
142
|
-
end
|
143
|
-
end
|
144
|
-
|
145
|
-
def set_data(request, rest_path, data, *options)
|
146
|
-
rest_path ||= request.rest_path
|
147
|
-
begin
|
148
|
-
data_store.set(rest_path, data, *options, requestor: request.requestor)
|
149
|
-
rescue DataStore::DataNotFoundError
|
150
|
-
if options.include?(:data_store_exceptions)
|
151
|
-
raise
|
152
|
-
else
|
153
|
-
raise RestErrorResponse.new(404, "Object not found: #{build_uri(request.base_uri, rest_path)}")
|
154
|
-
end
|
155
|
-
end
|
156
|
-
end
|
157
|
-
|
158
|
-
def create_data_dir(request, rest_path, name, *options)
|
159
|
-
rest_path ||= request.rest_path
|
160
|
-
begin
|
161
|
-
data_store.create_dir(rest_path, name, *options, requestor: request.requestor)
|
162
|
-
rescue DataStore::DataNotFoundError
|
163
|
-
if options.include?(:data_store_exceptions)
|
164
|
-
raise
|
165
|
-
else
|
166
|
-
raise RestErrorResponse.new(404, "Parent not found: #{build_uri(request.base_uri, rest_path)}")
|
167
|
-
end
|
168
|
-
rescue DataStore::DataAlreadyExistsError
|
169
|
-
if options.include?(:data_store_exceptions)
|
170
|
-
raise
|
171
|
-
else
|
172
|
-
raise RestErrorResponse.new(409, "Object already exists: #{build_uri(request.base_uri, rest_path + [name])}")
|
173
|
-
end
|
174
|
-
end
|
175
|
-
end
|
176
|
-
|
177
|
-
def create_data(request, rest_path, name, data, *options)
|
178
|
-
rest_path ||= request.rest_path
|
179
|
-
begin
|
180
|
-
data_store.create(rest_path, name, data, *options, requestor: request.requestor)
|
181
|
-
rescue DataStore::DataNotFoundError
|
182
|
-
if options.include?(:data_store_exceptions)
|
183
|
-
raise
|
184
|
-
else
|
185
|
-
raise RestErrorResponse.new(404, "Parent not found: #{build_uri(request.base_uri, rest_path)}")
|
186
|
-
end
|
187
|
-
rescue DataStore::DataAlreadyExistsError
|
188
|
-
if options.include?(:data_store_exceptions)
|
189
|
-
raise
|
190
|
-
else
|
191
|
-
raise RestErrorResponse.new(409, "Object already exists: #{build_uri(request.base_uri, rest_path + [name])}")
|
192
|
-
end
|
193
|
-
end
|
194
|
-
end
|
195
|
-
|
196
|
-
def exists_data?(request, rest_path = nil)
|
197
|
-
rest_path ||= request.rest_path
|
198
|
-
data_store.exists?(rest_path)
|
199
|
-
end
|
200
|
-
|
201
|
-
def exists_data_dir?(request, rest_path = nil)
|
202
|
-
rest_path ||= request.rest_path
|
203
|
-
data_store.exists_dir?(rest_path)
|
204
|
-
end
|
205
|
-
|
206
|
-
def error(response_code, error, opts = {})
|
207
|
-
json_response(response_code, { "error" => [ error ] }, opts)
|
208
|
-
end
|
209
|
-
|
210
|
-
# Serializes `data` to JSON and returns an Array with the
|
211
|
-
# response code, HTTP headers and JSON body.
|
212
|
-
#
|
213
|
-
# @param [Fixnum] response_code HTTP response code
|
214
|
-
# @param [Hash] data The data for the response body as a Hash
|
215
|
-
# @param [Hash] options
|
216
|
-
# @option options [Hash] :headers (see #already_json_response)
|
217
|
-
# @option options [Boolean] :pretty (true) Pretty-format the JSON
|
218
|
-
# @option options [Fixnum] :request_version (see #already_json_response)
|
219
|
-
# @option options [Fixnum] :response_version (see #already_json_response)
|
220
|
-
#
|
221
|
-
# @return (see #already_json_response)
|
222
|
-
#
|
223
|
-
def json_response(response_code, data, options = {})
|
224
|
-
options = { pretty: true }.merge(options)
|
225
|
-
do_pretty_json = !!options.delete(:pretty) # make sure we have a proper Boolean.
|
226
|
-
json = FFI_Yajl::Encoder.encode(data, pretty: do_pretty_json)
|
227
|
-
already_json_response(response_code, json, options)
|
228
|
-
end
|
229
|
-
|
230
|
-
def text_response(response_code, text)
|
231
|
-
[response_code, { "Content-Type" => "text/plain" }, text]
|
232
|
-
end
|
233
|
-
|
234
|
-
# rfc090 returns 404 error or 200 with an emtpy body
|
235
|
-
# @param [ChefZero::RestRequest] request The HTTP request object
|
236
|
-
#
|
237
|
-
# @return (see #json_response)
|
238
|
-
#
|
239
|
-
def head_request(request)
|
240
|
-
get_data(request) # will raise 404 if non-existant
|
241
|
-
json_response(200, nil)
|
242
|
-
end
|
243
|
-
|
244
|
-
# Returns an Array with the response code, HTTP headers, and JSON body.
|
245
|
-
#
|
246
|
-
# @param [Fixnum] response_code The HTTP response code
|
247
|
-
# @param [String] json_text The JSON body for the response
|
248
|
-
# @param [Hash] options
|
249
|
-
# @option options [Hash] :headers ({}) HTTP headers (may override default headers)
|
250
|
-
# @option options [Fixnum] :request_version (0) Request API version
|
251
|
-
# @option options [Fixnum] :response_version (0) Response API version
|
252
|
-
#
|
253
|
-
# @return [Array(Fixnum, Hash{String => String}, String)]
|
254
|
-
#
|
255
|
-
def already_json_response(response_code, json_text, options = {})
|
256
|
-
version_header = FFI_Yajl::Encoder.encode(
|
257
|
-
"min_version" => MIN_API_VERSION.to_s,
|
258
|
-
"max_version" => MAX_API_VERSION.to_s,
|
259
|
-
"request_version" => options[:request_version] || DEFAULT_REQUEST_VERSION.to_s,
|
260
|
-
"response_version" => options[:response_version] || DEFAULT_RESPONSE_VERSION.to_s
|
261
|
-
)
|
262
|
-
|
263
|
-
headers = {
|
264
|
-
"Content-Type" => "application/json",
|
265
|
-
"X-Ops-Server-API-Version" => version_header,
|
266
|
-
}
|
267
|
-
headers.merge!(options[:headers]) if options[:headers]
|
268
|
-
|
269
|
-
[ response_code, headers, json_text ]
|
270
|
-
end
|
271
|
-
|
272
|
-
# To be called from inside rest endpoints
|
273
|
-
def build_uri(base_uri, rest_path)
|
274
|
-
if server.options[:single_org]
|
275
|
-
# Strip off /organizations/chef if we are in single org mode
|
276
|
-
if rest_path[0..1] != [ "organizations", server.options[:single_org] ]
|
277
|
-
raise "Unexpected URL #{rest_path[0..1]} passed to build_uri in single org mode"
|
278
|
-
end
|
279
|
-
|
280
|
-
return self.class.build_uri(base_uri, rest_path[2..-1])
|
281
|
-
end
|
282
|
-
|
283
|
-
self.class.build_uri(base_uri, rest_path)
|
284
|
-
end
|
285
|
-
|
286
|
-
def self.build_uri(base_uri, rest_path)
|
287
|
-
"#{base_uri}/#{rest_path.map { |v| rfc2396_parser.escape(v) }.join("/")}"
|
288
|
-
end
|
289
|
-
|
290
|
-
def populate_defaults(request, response)
|
291
|
-
response
|
292
|
-
end
|
293
|
-
|
294
|
-
def parse_json(json)
|
295
|
-
FFI_Yajl::Parser.parse(json)
|
296
|
-
end
|
297
|
-
|
298
|
-
def to_json(data)
|
299
|
-
FFI_Yajl::Encoder.encode(data, pretty: true)
|
300
|
-
end
|
301
|
-
|
302
|
-
def get_data_or_else(request, path, or_else_value)
|
303
|
-
if exists_data?(request, path)
|
304
|
-
parse_json(get_data(request, path))
|
305
|
-
else
|
306
|
-
or_else_value
|
307
|
-
end
|
308
|
-
end
|
309
|
-
|
310
|
-
def list_data_or_else(request, path, or_else_value)
|
311
|
-
if exists_data_dir?(request, path)
|
312
|
-
list_data(request, path)
|
313
|
-
else
|
314
|
-
or_else_value
|
315
|
-
end
|
316
|
-
end
|
317
|
-
|
318
|
-
def hashify_list(list)
|
319
|
-
list.reduce({}) { |acc, obj| acc.merge( obj => {} ) }
|
320
|
-
end
|
321
|
-
|
322
|
-
def policy_name_invalid?(name)
|
323
|
-
!name.is_a?(String) ||
|
324
|
-
name.size > 255 ||
|
325
|
-
name =~ /[+ !]/
|
326
|
-
end
|
327
|
-
|
328
|
-
def self.rfc2396_parser
|
329
|
-
@parser ||= URI::RFC2396_Parser.new
|
330
|
-
end
|
331
|
-
end
|
332
|
-
end
|
1
|
+
require_relative "rest_request"
|
2
|
+
require_relative "rest_error_response"
|
3
|
+
require_relative "data_store/data_not_found_error"
|
4
|
+
require_relative "chef_data/acl_path"
|
5
|
+
|
6
|
+
module ChefZero
|
7
|
+
class RestBase
|
8
|
+
DEFAULT_REQUEST_VERSION = 0
|
9
|
+
DEFAULT_RESPONSE_VERSION = 0
|
10
|
+
|
11
|
+
def initialize(server)
|
12
|
+
@server = server
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_reader :server
|
16
|
+
|
17
|
+
def data_store
|
18
|
+
server.data_store
|
19
|
+
end
|
20
|
+
|
21
|
+
def check_api_version(request)
|
22
|
+
version = request.api_version
|
23
|
+
|
24
|
+
if version > MAX_API_VERSION || version < MIN_API_VERSION
|
25
|
+
response = {
|
26
|
+
"error" => "invalid-x-ops-server-api-version",
|
27
|
+
"message" => "Specified version #{version} not supported",
|
28
|
+
"min_api_version" => MIN_API_VERSION,
|
29
|
+
"max_api_version" => MAX_API_VERSION,
|
30
|
+
}
|
31
|
+
|
32
|
+
json_response(406,
|
33
|
+
response,
|
34
|
+
request_version: version, response_version: -1)
|
35
|
+
end
|
36
|
+
rescue ArgumentError
|
37
|
+
json_response(406,
|
38
|
+
{ "username" => request.requestor },
|
39
|
+
request_version: -1, response_version: -1)
|
40
|
+
end
|
41
|
+
|
42
|
+
def call(request)
|
43
|
+
response = check_api_version(request)
|
44
|
+
return response unless response.nil?
|
45
|
+
|
46
|
+
method = request.method.downcase.to_sym
|
47
|
+
unless respond_to?(method)
|
48
|
+
accept_methods = %i{get put post delete}.select { |m| respond_to?(m) }
|
49
|
+
accept_methods_str = accept_methods.map { |m| m.to_s.upcase }.join(", ")
|
50
|
+
return [405, { "Content-Type" => "text/plain", "Allow" => accept_methods_str }, "Bad request method for '#{request.env["REQUEST_PATH"]}': #{request.env["REQUEST_METHOD"]}"]
|
51
|
+
end
|
52
|
+
if json_only && !accepts?(request, "application", "json")
|
53
|
+
return [406, { "Content-Type" => "text/plain" }, "Must accept application/json"]
|
54
|
+
end
|
55
|
+
|
56
|
+
# Dispatch to get()/post()/put()/delete()
|
57
|
+
begin
|
58
|
+
send(method, request)
|
59
|
+
rescue RestErrorResponse => e
|
60
|
+
ChefZero::Log.debug("#{e.inspect}\n#{e.backtrace.join("\n")}")
|
61
|
+
error(e.response_code, e.error)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def json_only
|
66
|
+
true
|
67
|
+
end
|
68
|
+
|
69
|
+
def accepts?(request, category, type)
|
70
|
+
# If HTTP_ACCEPT is not sent at all, assume it accepts anything
|
71
|
+
# This parses as per https://datatracker.ietf.org/doc/html/rfc7231#section-5.3
|
72
|
+
return true unless request.env["HTTP_ACCEPT"]
|
73
|
+
|
74
|
+
accepts = request.env["HTTP_ACCEPT"].split(/,\s*/).map { |x| x.split(";", 2)[0].strip }
|
75
|
+
accepts.include?("#{category}/#{type}") || accepts.include?("#{category}/*") || accepts.include?("*/*")
|
76
|
+
end
|
77
|
+
|
78
|
+
def get_data(request, rest_path = nil, *options)
|
79
|
+
rest_path ||= request.rest_path
|
80
|
+
rest_path = rest_path.map { |v| self.class.rfc2396_parser.unescape(v) }
|
81
|
+
begin
|
82
|
+
data_store.get(rest_path, request)
|
83
|
+
rescue DataStore::DataNotFoundError
|
84
|
+
if options.include?(:nil)
|
85
|
+
nil
|
86
|
+
elsif options.include?(:data_store_exceptions)
|
87
|
+
raise
|
88
|
+
else
|
89
|
+
raise RestErrorResponse.new(404, "Object not found: #{build_uri(request.base_uri, rest_path)}")
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def list_data(request, rest_path = nil, *options)
|
95
|
+
rest_path ||= request.rest_path
|
96
|
+
begin
|
97
|
+
data_store.list(rest_path)
|
98
|
+
rescue DataStore::DataNotFoundError
|
99
|
+
if options.include?(:data_store_exceptions)
|
100
|
+
raise
|
101
|
+
else
|
102
|
+
raise RestErrorResponse.new(404, "Object not found: #{build_uri(request.base_uri, rest_path)}")
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def delete_data(request, rest_path = nil, *options)
|
108
|
+
rest_path ||= request.rest_path
|
109
|
+
begin
|
110
|
+
data_store.delete(rest_path, *options)
|
111
|
+
rescue DataStore::DataNotFoundError
|
112
|
+
if options.include?(:data_store_exceptions)
|
113
|
+
raise
|
114
|
+
else
|
115
|
+
raise RestErrorResponse.new(404, "Object not found: #{build_uri(request.base_uri, rest_path)}")
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
begin
|
120
|
+
acl_path = ChefData::AclPath.get_acl_data_path(rest_path)
|
121
|
+
data_store.delete(acl_path) if acl_path
|
122
|
+
rescue DataStore::DataNotFoundError
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def delete_data_dir(request, rest_path, *options)
|
127
|
+
rest_path ||= request.rest_path
|
128
|
+
begin
|
129
|
+
data_store.delete_dir(rest_path, *options)
|
130
|
+
rescue DataStore::DataNotFoundError
|
131
|
+
if options.include?(:data_store_exceptions)
|
132
|
+
raise
|
133
|
+
else
|
134
|
+
raise RestErrorResponse.new(404, "Object not found: #{build_uri(request.base_uri, rest_path)}")
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
begin
|
139
|
+
acl_path = ChefData::AclPath.get_acl_data_path(rest_path)
|
140
|
+
data_store.delete(acl_path) if acl_path
|
141
|
+
rescue DataStore::DataNotFoundError
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def set_data(request, rest_path, data, *options)
|
146
|
+
rest_path ||= request.rest_path
|
147
|
+
begin
|
148
|
+
data_store.set(rest_path, data, *options, requestor: request.requestor)
|
149
|
+
rescue DataStore::DataNotFoundError
|
150
|
+
if options.include?(:data_store_exceptions)
|
151
|
+
raise
|
152
|
+
else
|
153
|
+
raise RestErrorResponse.new(404, "Object not found: #{build_uri(request.base_uri, rest_path)}")
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def create_data_dir(request, rest_path, name, *options)
|
159
|
+
rest_path ||= request.rest_path
|
160
|
+
begin
|
161
|
+
data_store.create_dir(rest_path, name, *options, requestor: request.requestor)
|
162
|
+
rescue DataStore::DataNotFoundError
|
163
|
+
if options.include?(:data_store_exceptions)
|
164
|
+
raise
|
165
|
+
else
|
166
|
+
raise RestErrorResponse.new(404, "Parent not found: #{build_uri(request.base_uri, rest_path)}")
|
167
|
+
end
|
168
|
+
rescue DataStore::DataAlreadyExistsError
|
169
|
+
if options.include?(:data_store_exceptions)
|
170
|
+
raise
|
171
|
+
else
|
172
|
+
raise RestErrorResponse.new(409, "Object already exists: #{build_uri(request.base_uri, rest_path + [name])}")
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
def create_data(request, rest_path, name, data, *options)
|
178
|
+
rest_path ||= request.rest_path
|
179
|
+
begin
|
180
|
+
data_store.create(rest_path, name, data, *options, requestor: request.requestor)
|
181
|
+
rescue DataStore::DataNotFoundError
|
182
|
+
if options.include?(:data_store_exceptions)
|
183
|
+
raise
|
184
|
+
else
|
185
|
+
raise RestErrorResponse.new(404, "Parent not found: #{build_uri(request.base_uri, rest_path)}")
|
186
|
+
end
|
187
|
+
rescue DataStore::DataAlreadyExistsError
|
188
|
+
if options.include?(:data_store_exceptions)
|
189
|
+
raise
|
190
|
+
else
|
191
|
+
raise RestErrorResponse.new(409, "Object already exists: #{build_uri(request.base_uri, rest_path + [name])}")
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
def exists_data?(request, rest_path = nil)
|
197
|
+
rest_path ||= request.rest_path
|
198
|
+
data_store.exists?(rest_path)
|
199
|
+
end
|
200
|
+
|
201
|
+
def exists_data_dir?(request, rest_path = nil)
|
202
|
+
rest_path ||= request.rest_path
|
203
|
+
data_store.exists_dir?(rest_path)
|
204
|
+
end
|
205
|
+
|
206
|
+
def error(response_code, error, opts = {})
|
207
|
+
json_response(response_code, { "error" => [ error ] }, opts)
|
208
|
+
end
|
209
|
+
|
210
|
+
# Serializes `data` to JSON and returns an Array with the
|
211
|
+
# response code, HTTP headers and JSON body.
|
212
|
+
#
|
213
|
+
# @param [Fixnum] response_code HTTP response code
|
214
|
+
# @param [Hash] data The data for the response body as a Hash
|
215
|
+
# @param [Hash] options
|
216
|
+
# @option options [Hash] :headers (see #already_json_response)
|
217
|
+
# @option options [Boolean] :pretty (true) Pretty-format the JSON
|
218
|
+
# @option options [Fixnum] :request_version (see #already_json_response)
|
219
|
+
# @option options [Fixnum] :response_version (see #already_json_response)
|
220
|
+
#
|
221
|
+
# @return (see #already_json_response)
|
222
|
+
#
|
223
|
+
def json_response(response_code, data, options = {})
|
224
|
+
options = { pretty: true }.merge(options)
|
225
|
+
do_pretty_json = !!options.delete(:pretty) # make sure we have a proper Boolean.
|
226
|
+
json = FFI_Yajl::Encoder.encode(data, pretty: do_pretty_json)
|
227
|
+
already_json_response(response_code, json, options)
|
228
|
+
end
|
229
|
+
|
230
|
+
def text_response(response_code, text)
|
231
|
+
[response_code, { "Content-Type" => "text/plain" }, text]
|
232
|
+
end
|
233
|
+
|
234
|
+
# rfc090 returns 404 error or 200 with an emtpy body
|
235
|
+
# @param [ChefZero::RestRequest] request The HTTP request object
|
236
|
+
#
|
237
|
+
# @return (see #json_response)
|
238
|
+
#
|
239
|
+
def head_request(request)
|
240
|
+
get_data(request) # will raise 404 if non-existant
|
241
|
+
json_response(200, nil)
|
242
|
+
end
|
243
|
+
|
244
|
+
# Returns an Array with the response code, HTTP headers, and JSON body.
|
245
|
+
#
|
246
|
+
# @param [Fixnum] response_code The HTTP response code
|
247
|
+
# @param [String] json_text The JSON body for the response
|
248
|
+
# @param [Hash] options
|
249
|
+
# @option options [Hash] :headers ({}) HTTP headers (may override default headers)
|
250
|
+
# @option options [Fixnum] :request_version (0) Request API version
|
251
|
+
# @option options [Fixnum] :response_version (0) Response API version
|
252
|
+
#
|
253
|
+
# @return [Array(Fixnum, Hash{String => String}, String)]
|
254
|
+
#
|
255
|
+
def already_json_response(response_code, json_text, options = {})
|
256
|
+
version_header = FFI_Yajl::Encoder.encode(
|
257
|
+
"min_version" => MIN_API_VERSION.to_s,
|
258
|
+
"max_version" => MAX_API_VERSION.to_s,
|
259
|
+
"request_version" => options[:request_version] || DEFAULT_REQUEST_VERSION.to_s,
|
260
|
+
"response_version" => options[:response_version] || DEFAULT_RESPONSE_VERSION.to_s
|
261
|
+
)
|
262
|
+
|
263
|
+
headers = {
|
264
|
+
"Content-Type" => "application/json",
|
265
|
+
"X-Ops-Server-API-Version" => version_header,
|
266
|
+
}
|
267
|
+
headers.merge!(options[:headers]) if options[:headers]
|
268
|
+
|
269
|
+
[ response_code, headers, json_text ]
|
270
|
+
end
|
271
|
+
|
272
|
+
# To be called from inside rest endpoints
|
273
|
+
def build_uri(base_uri, rest_path)
|
274
|
+
if server.options[:single_org]
|
275
|
+
# Strip off /organizations/chef if we are in single org mode
|
276
|
+
if rest_path[0..1] != [ "organizations", server.options[:single_org] ]
|
277
|
+
raise "Unexpected URL #{rest_path[0..1]} passed to build_uri in single org mode"
|
278
|
+
end
|
279
|
+
|
280
|
+
return self.class.build_uri(base_uri, rest_path[2..-1])
|
281
|
+
end
|
282
|
+
|
283
|
+
self.class.build_uri(base_uri, rest_path)
|
284
|
+
end
|
285
|
+
|
286
|
+
def self.build_uri(base_uri, rest_path)
|
287
|
+
"#{base_uri}/#{rest_path.map { |v| rfc2396_parser.escape(v) }.join("/")}"
|
288
|
+
end
|
289
|
+
|
290
|
+
def populate_defaults(request, response)
|
291
|
+
response
|
292
|
+
end
|
293
|
+
|
294
|
+
def parse_json(json)
|
295
|
+
FFI_Yajl::Parser.parse(json)
|
296
|
+
end
|
297
|
+
|
298
|
+
def to_json(data)
|
299
|
+
FFI_Yajl::Encoder.encode(data, pretty: true)
|
300
|
+
end
|
301
|
+
|
302
|
+
def get_data_or_else(request, path, or_else_value)
|
303
|
+
if exists_data?(request, path)
|
304
|
+
parse_json(get_data(request, path))
|
305
|
+
else
|
306
|
+
or_else_value
|
307
|
+
end
|
308
|
+
end
|
309
|
+
|
310
|
+
def list_data_or_else(request, path, or_else_value)
|
311
|
+
if exists_data_dir?(request, path)
|
312
|
+
list_data(request, path)
|
313
|
+
else
|
314
|
+
or_else_value
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
def hashify_list(list)
|
319
|
+
list.reduce({}) { |acc, obj| acc.merge( obj => {} ) }
|
320
|
+
end
|
321
|
+
|
322
|
+
def policy_name_invalid?(name)
|
323
|
+
!name.is_a?(String) ||
|
324
|
+
name.size > 255 ||
|
325
|
+
name =~ /[+ !]/
|
326
|
+
end
|
327
|
+
|
328
|
+
def self.rfc2396_parser
|
329
|
+
@parser ||= URI::RFC2396_Parser.new
|
330
|
+
end
|
331
|
+
end
|
332
|
+
end
|