chef-zero 4.5.0 → 4.6.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,62 @@
1
+ require 'chef_zero/rest_base'
2
+
3
+ module ChefZero
4
+ module Endpoints
5
+ # ActorKeyEndpoint
6
+ #
7
+ # This class handles DELETE/GET/PUT requests for all client/user keys
8
+ # **except** default public keys, i.e. requests with identity key
9
+ # "default". Those are handled by ActorDefaultKeyEndpoint. See that class
10
+ # for more information.
11
+ #
12
+ # /users/USER/keys/NAME
13
+ # /organizations/ORG/clients/CLIENT/keys/NAME
14
+ class ActorKeyEndpoint < RestBase
15
+ def get(request)
16
+ validate_actor!(request)
17
+ key_path = data_path(request)
18
+ already_json_response(200, get_data(request, key_path))
19
+ end
20
+
21
+ def delete(request)
22
+ validate_actor!(request) # 404 if actor doesn't exist
23
+
24
+ key_path = data_path(request)
25
+ data = get_data(request, key_path)
26
+ delete_data(request, key_path)
27
+
28
+ already_json_response(200, data)
29
+ end
30
+
31
+ def put(request)
32
+ validate_actor!(request) # 404 if actor doesn't exist
33
+ set_data(request, data_path(request), request.body)
34
+ end
35
+
36
+ private
37
+
38
+ # Returns the keys data store path, which is the same as
39
+ # `request.rest_path` except with "client_keys" instead of "clients" or
40
+ # "user_keys" instead of "users."
41
+ def data_path(request)
42
+ request.rest_path.dup.tap do |path|
43
+ if client?(request)
44
+ path[2] = "client_keys"
45
+ else
46
+ path[0] = "user_keys"
47
+ end
48
+ end
49
+ end
50
+
51
+ # Raises RestErrorResponse (404) if actor doesn't exist
52
+ def validate_actor!(request)
53
+ actor_path = request.rest_path[ client?(request) ? 0..3 : 0..1 ]
54
+ get_data(request, actor_path)
55
+ end
56
+
57
+ def client?(request)
58
+ request.rest_path[2] == "clients"
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,129 @@
1
+ require 'chef_zero/rest_base'
2
+
3
+ module ChefZero
4
+ module Endpoints
5
+ # /users/USER/keys
6
+ # /organizations/ORG/clients/CLIENT/keys
7
+ class ActorKeysEndpoint < RestBase
8
+ DEFAULT_PUBLIC_KEY_NAME = "default"
9
+ DATE_FORMAT = "%FT%TZ" # e.g. 2015-12-24T21:00:00Z
10
+
11
+ def get(request, alt_uri_root=nil)
12
+ path = data_path(request)
13
+
14
+ # Get actor or 404 if it doesn't exist
15
+ actor_json = get_data(request, actor_path(request))
16
+
17
+ key_names = list_data_or_else(request, path, [])
18
+ key_names.unshift(DEFAULT_PUBLIC_KEY_NAME) if actor_has_default_public_key?(actor_json)
19
+
20
+ result = key_names.map do |key_name|
21
+ list_key(request, [ *path, key_name ], alt_uri_root)
22
+ end
23
+
24
+ json_response(200, result)
25
+ end
26
+
27
+ def post(request)
28
+ request_body = parse_json(request.body)
29
+
30
+ # Try loading the client or user so a 404 is returned if it doesn't exist
31
+ actor_json = get_data(request, actor_path(request))
32
+
33
+ generate_keys = request_body["public_key"].nil?
34
+
35
+ if generate_keys
36
+ private_key, public_key = server.gen_key_pair
37
+ else
38
+ public_key = request_body['public_key']
39
+ end
40
+
41
+ key_name = request_body["name"]
42
+
43
+ if key_name == DEFAULT_PUBLIC_KEY_NAME
44
+ store_actor_default_public_key!(request, actor_json, public_key)
45
+ else
46
+ store_actor_public_key!(request, key_name, public_key, request_body["expiration_date"])
47
+ end
48
+
49
+ response_body = { "uri" => key_uri(request, key_name) }
50
+ response_body["private_key"] = private_key if generate_keys
51
+
52
+ json_response(201, response_body,
53
+ headers: { "Location" => response_body["uri"] })
54
+ end
55
+
56
+ private
57
+
58
+ def store_actor_public_key!(request, name, public_key, expiration_date)
59
+ data = to_json(
60
+ "name" => name,
61
+ "public_key" => public_key,
62
+ "expiration_date" => expiration_date
63
+ )
64
+
65
+ create_data(request, data_path(request), name, data, :create_dir)
66
+ end
67
+
68
+ def store_actor_default_public_key!(request, actor_json, public_key)
69
+ actor_data = parse_json(actor_json)
70
+
71
+ if actor_data["public_key"]
72
+ raise RestErrorResponse.new(409, "Object already exists: #{key_uri(request, DEFAULT_PUBLIC_KEY_NAME)}")
73
+ end
74
+
75
+ actor_data["public_key"] = public_key
76
+ set_data(request, actor_path(request), to_json(actor_data))
77
+
78
+ end
79
+
80
+ # Returns the keys data store path, which is the same as
81
+ # `request.rest_path` except with "user_keys" instead of "users" or
82
+ # "client_keys" instead of "clients."
83
+ def data_path(request)
84
+ request.rest_path.dup.tap do |path|
85
+ if client?(request)
86
+ path[2] = "client_keys"
87
+ else
88
+ path[0] = "user_keys"
89
+ end
90
+ end
91
+ end
92
+
93
+ def list_key(request, data_path, alt_uri_root=nil)
94
+ key_name, expiration_date =
95
+ if data_path[-1] == DEFAULT_PUBLIC_KEY_NAME
96
+ [ DEFAULT_PUBLIC_KEY_NAME, "infinity" ]
97
+ else
98
+ parse_json(get_data(request, data_path))
99
+ .values_at("name", "expiration_date")
100
+ end
101
+
102
+ expired = expiration_date != "infinity" &&
103
+ DateTime.now > DateTime.strptime(expiration_date, DATE_FORMAT)
104
+
105
+ { "name" => key_name,
106
+ "uri" => key_uri(request, key_name, alt_uri_root),
107
+ "expired" => expired }
108
+ end
109
+
110
+ def client?(request)
111
+ request.rest_path[2] == "clients"
112
+ end
113
+
114
+ def key_uri(request, key_name, alt_uri_root=nil)
115
+ uri_root = alt_uri_root.nil? ? request.rest_path : alt_uri_root
116
+ build_uri(request.base_uri, [ *uri_root, key_name ])
117
+ end
118
+
119
+ def actor_path(request)
120
+ return request.rest_path[0..3] if client?(request)
121
+ request.rest_path[0..1]
122
+ end
123
+
124
+ def actor_has_default_public_key?(actor_json)
125
+ !!parse_json(actor_json)["public_key"]
126
+ end
127
+ end
128
+ end
129
+ end
@@ -9,29 +9,29 @@ module ChefZero
9
9
  response = super(request)
10
10
 
11
11
  if request.query_params['email']
12
- results = FFI_Yajl::Parser.parse(response[2], :create_additions => false)
12
+ results = parse_json(response[2])
13
13
  new_results = {}
14
14
  results.each do |name, url|
15
15
  record = get_data(request, request.rest_path + [ name ], :nil)
16
16
  if record
17
- record = FFI_Yajl::Parser.parse(record, :create_additions => false)
17
+ record = parse_json(record)
18
18
  new_results[name] = url if record['email'] == request.query_params['email']
19
19
  end
20
20
  end
21
- response[2] = FFI_Yajl::Encoder.encode(new_results, :pretty => true)
21
+ response[2] = to_json(new_results)
22
22
  end
23
23
 
24
24
  if request.query_params['verbose']
25
- results = FFI_Yajl::Parser.parse(response[2], :create_additions => false)
25
+ results = parse_json(response[2])
26
26
  results.each do |name, url|
27
27
  record = get_data(request, request.rest_path + [ name ], :nil)
28
28
  if record
29
- record = FFI_Yajl::Parser.parse(record, :create_additions => false)
29
+ record = parse_json(record)
30
30
  record = ChefData::DataNormalizer.normalize_user(record, name, identity_keys, server.options[:osc_compat])
31
31
  results[name] = record
32
32
  end
33
33
  end
34
- response[2] = FFI_Yajl::Encoder.encode(results, :pretty => true)
34
+ response[2] = to_json(results)
35
35
  end
36
36
  response
37
37
  end
@@ -39,21 +39,47 @@ module ChefZero
39
39
  def post(request)
40
40
  # First, find out if the user actually posted a public key. If not, make
41
41
  # one.
42
- request_body = FFI_Yajl::Parser.parse(request.body, :create_additions => false)
42
+ request_body = parse_json(request.body)
43
43
  public_key = request_body['public_key']
44
- if !public_key
44
+
45
+ skip_key_create = !request.api_v0? && !request_body["create_key"]
46
+
47
+ if !public_key && !skip_key_create
45
48
  private_key, public_key = server.gen_key_pair
46
49
  request_body['public_key'] = public_key
47
- request.body = FFI_Yajl::Encoder.encode(request_body, :pretty => true)
50
+ request.body = to_json(request_body)
51
+ elsif skip_key_create
52
+ request_body['public_key'] = nil
53
+ request.body = to_json(request_body)
48
54
  end
49
55
 
50
56
  result = super(request)
51
57
 
52
58
  if result[0] == 201
53
59
  # If we generated a key, stuff it in the response.
54
- response = FFI_Yajl::Parser.parse(result[2], :create_additions => false)
55
- response['private_key'] = private_key if private_key
56
- response['public_key'] = public_key unless request.rest_path[0] == 'users'
60
+ user_data = parse_json(result[2])
61
+
62
+ key_data = {}
63
+ key_data['private_key'] = private_key if private_key
64
+ key_data['public_key'] = public_key unless request.rest_path[0] == 'users'
65
+
66
+ response =
67
+ if request.api_v0?
68
+ user_data.merge(key_data)
69
+ elsif skip_key_create && !public_key
70
+ user_data
71
+ else
72
+ actor_name = request_body["name"] || request_body["username"] || request_body["clientname"]
73
+
74
+ relpath_to_default_key = [ actor_name, "keys", "default" ]
75
+ key_data["uri"] = build_uri(request.base_uri, request.rest_path + relpath_to_default_key)
76
+ key_data["public_key"] = public_key
77
+ key_data["name"] = "default"
78
+ key_data["expiration_date"] = "infinity"
79
+ user_data["chef_key"] = key_data
80
+ user_data
81
+ end
82
+
57
83
  json_response(201, response)
58
84
  else
59
85
  result
@@ -0,0 +1,16 @@
1
+ require 'chef_zero/rest_base'
2
+
3
+ module ChefZero
4
+ module Endpoints
5
+ # GET /organizations/ORG/users/USER/keys/default
6
+ class OrganizationUserDefaultKeyEndpoint < RestBase
7
+ def get(request)
8
+ # 404 if it doesn't exist
9
+ get_data(request, request.rest_path[0..3])
10
+ # Just use the /users/USER/keys/default endpoint
11
+ request.rest_path = request.rest_path[2..-1]
12
+ ActorDefaultKeyEndpoint.new(server).get(request)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,17 @@
1
+ require 'chef_zero/rest_base'
2
+ require 'chef_zero/endpoints/actor_keys_endpoint'
3
+
4
+ module ChefZero
5
+ module Endpoints
6
+ # GET /organizations/ORG/users/USER/keys/NAME
7
+ class OrganizationUserKeyEndpoint < RestBase
8
+ def get(request)
9
+ # 404 if not a member of the org
10
+ get_data(request, request.rest_path[0..3])
11
+ # Just use the /users/USER/keys endpoint
12
+ request.rest_path = request.rest_path[2..-1]
13
+ ActorKeyEndpoint.new(server).get(request)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ require 'chef_zero/rest_base'
2
+
3
+ module ChefZero
4
+ module Endpoints
5
+ # GET /organizations/ORG/users/USER/keys
6
+ class OrganizationUserKeysEndpoint < RestBase
7
+ def get(request)
8
+ # 404 if it doesn't exist
9
+ get_data(request, request.rest_path[0..3])
10
+ # Just use the /users/USER/keys/key endpoint
11
+ original_path = request.rest_path
12
+ request.rest_path = request.rest_path[2..-1]
13
+ ActorKeysEndpoint.new(server).get(request, original_path)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -30,13 +30,22 @@ module ChefZero
30
30
  end
31
31
  end
32
32
  if json
33
- json_response(200, {
33
+ principal_data = {
34
34
  'name' => name,
35
35
  'type' => type,
36
36
  'public_key' => FFI_Yajl::Parser.parse(json)['public_key'] || PUBLIC_KEY,
37
37
  'authz_id' => '0'*32,
38
38
  'org_member' => org_member
39
- })
39
+ }
40
+
41
+ response_data =
42
+ if request.api_v0?
43
+ principal_data
44
+ else
45
+ { "principals" => [ principal_data ] }
46
+ end
47
+
48
+ json_response(200, response_data)
40
49
  else
41
50
  error(404, 'Principal not found')
42
51
  end
@@ -21,12 +21,11 @@ module ChefZero
21
21
  def put(request)
22
22
  # We grab the old body to trigger a 404 if it doesn't exist
23
23
  old_body = get_data(request)
24
- request_json = FFI_Yajl::Parser.parse(request.body, :create_additions => false)
25
- key = identity_keys.map { |k| request_json[k] }.select { |v| v }.first
26
- key ||= request.rest_path[-1]
24
+
27
25
  # If it's a rename, check for conflict and delete the old value
28
- rename = key != request.rest_path[-1]
29
- if rename
26
+ if is_rename?(request)
27
+ key = identity_key_value(request)
28
+
30
29
  begin
31
30
  create_data(request, request.rest_path[0..-2], key, request.body, :data_store_exceptions)
32
31
  rescue DataStore::DataAlreadyExistsError
@@ -56,8 +55,23 @@ module ChefZero
56
55
  return FFI_Yajl::Encoder.encode(merged_json, :pretty => true)
57
56
  end
58
57
  end
58
+
59
59
  request.body
60
60
  end
61
+
62
+ private
63
+
64
+ # Get the value of the (first existing) identity key from the request body or nil
65
+ def identity_key_value(request)
66
+ request_json = parse_json(request.body)
67
+ identity_keys.map { |k| request_json[k] }.compact.first
68
+ end
69
+
70
+ # Does this request change the value of the identity key?
71
+ def is_rename?(request)
72
+ return false unless key = identity_key_value(request)
73
+ key != request.rest_path[-1]
74
+ end
61
75
  end
62
76
  end
63
77
  end
@@ -7,7 +7,7 @@ module ChefZero
7
7
  API_VERSION = 1
8
8
  def get(request)
9
9
  json_response(200, {"min_api_version"=>MIN_API_VERSION, "max_api_version"=>MAX_API_VERSION},
10
- request.api_version, API_VERSION)
10
+ request_version: request.api_version, response_version: API_VERSION)
11
11
  end
12
12
  end
13
13
  end
@@ -5,6 +5,9 @@ require 'chef_zero/chef_data/acl_path'
5
5
 
6
6
  module ChefZero
7
7
  class RestBase
8
+ DEFAULT_REQUEST_VERSION = 0
9
+ DEFAULT_RESPONSE_VERSION = 0
10
+
8
11
  def initialize(server)
9
12
  @server = server
10
13
  end
@@ -16,21 +19,28 @@ module ChefZero
16
19
  end
17
20
 
18
21
  def check_api_version(request)
19
- version = request.api_version
20
- return nil if version.nil? # Not present in headers
22
+ return if request.api_version.nil? # Not present in headers
23
+ version = request.api_version.to_i
24
+
25
+ unless version.to_s == request.api_version.to_s # Version is not an Integer
26
+ return json_response(406,
27
+ { "username" => request.requestor },
28
+ request_version: -1, response_version: -1
29
+ )
30
+ end
21
31
 
22
- if version.to_i.to_s != version.to_s # Version is not an Integer
23
- return json_response(406, { "username" => request.requestor }, -1, -1)
24
- elsif version.to_i > MAX_API_VERSION or version.to_i < MIN_API_VERSION
32
+ if version > MAX_API_VERSION || version < MIN_API_VERSION
25
33
  response = {
26
34
  "error" => "invalid-x-ops-server-api-version",
27
35
  "message" => "Specified version #{version} not supported",
28
36
  "min_api_version" => MIN_API_VERSION,
29
37
  "max_api_version" => MAX_API_VERSION
30
38
  }
31
- return json_response(406, response, version, -1)
32
- else
33
- return nil
39
+
40
+ return json_response(406,
41
+ response,
42
+ request_version: version, response_version: -1
43
+ )
34
44
  end
35
45
  end
36
46
 
@@ -51,7 +61,7 @@ module ChefZero
51
61
  begin
52
62
  self.send(method, request)
53
63
  rescue RestErrorResponse => e
54
- ChefZero::Log.debug("#{e.inspect}\n#{e.backtrace.join("\n")}")
64
+ ChefZero::Log.info("#{e.inspect}\n#{e.backtrace.join("\n")}")
55
65
  error(e.response_code, e.error)
56
66
  end
57
67
  end
@@ -104,7 +114,7 @@ module ChefZero
104
114
  if options.include?(:data_store_exceptions)
105
115
  raise
106
116
  else
107
- raise RestErrorResponse.new(404, "Object not found: #{build_uri(request.base_uri, request.rest_path)}")
117
+ raise RestErrorResponse.new(404, "Object not found: #{build_uri(request.base_uri, rest_path)}")
108
118
  end
109
119
  end
110
120
 
@@ -123,7 +133,7 @@ module ChefZero
123
133
  if options.include?(:data_store_exceptions)
124
134
  raise
125
135
  else
126
- raise RestErrorResponse.new(404, "Object not found: #{build_uri(request.base_uri, request.rest_path)}")
136
+ raise RestErrorResponse.new(404, "Object not found: #{build_uri(request.base_uri, rest_path)}")
127
137
  end
128
138
  end
129
139
 
@@ -142,7 +152,7 @@ module ChefZero
142
152
  if options.include?(:data_store_exceptions)
143
153
  raise
144
154
  else
145
- raise RestErrorResponse.new(404, "Object not found: #{build_uri(request.base_uri, request.rest_path)}")
155
+ raise RestErrorResponse.new(404, "Object not found: #{build_uri(request.base_uri, rest_path)}")
146
156
  end
147
157
  end
148
158
  end
@@ -155,13 +165,13 @@ module ChefZero
155
165
  if options.include?(:data_store_exceptions)
156
166
  raise
157
167
  else
158
- raise RestErrorResponse.new(404, "Parent not found: #{build_uri(request.base_uri, request.rest_path)}")
168
+ raise RestErrorResponse.new(404, "Parent not found: #{build_uri(request.base_uri, rest_path)}")
159
169
  end
160
170
  rescue DataStore::DataAlreadyExistsError
161
171
  if options.include?(:data_store_exceptions)
162
172
  raise
163
173
  else
164
- raise RestErrorResponse.new(409, "Object already exists: #{build_uri(request.base_uri, request.rest_path + [name])}")
174
+ raise RestErrorResponse.new(409, "Object already exists: #{build_uri(request.base_uri, rest_path + [name])}")
165
175
  end
166
176
  end
167
177
  end
@@ -174,13 +184,13 @@ module ChefZero
174
184
  if options.include?(:data_store_exceptions)
175
185
  raise
176
186
  else
177
- raise RestErrorResponse.new(404, "Parent not found: #{build_uri(request.base_uri, request.rest_path)}")
187
+ raise RestErrorResponse.new(404, "Parent not found: #{build_uri(request.base_uri, rest_path)}")
178
188
  end
179
189
  rescue DataStore::DataAlreadyExistsError
180
190
  if options.include?(:data_store_exceptions)
181
191
  raise
182
192
  else
183
- raise RestErrorResponse.new(409, "Object already exists: #{build_uri(request.base_uri, request.rest_path + [name])}")
193
+ raise RestErrorResponse.new(409, "Object already exists: #{build_uri(request.base_uri, rest_path + [name])}")
184
194
  end
185
195
  end
186
196
  end
@@ -196,26 +206,59 @@ module ChefZero
196
206
  end
197
207
 
198
208
  def error(response_code, error, opts={})
199
- json_response(response_code, {"error" => [error]}, 0, 0, opts)
209
+ json_response(response_code, { "error" => [ error ] }, opts)
200
210
  end
201
211
 
202
- def json_response(response_code, json, request_version=0, response_version=0, opts={pretty: true})
203
- do_pretty_json = !!opts[:pretty] # make sure we have a proper Boolean.
204
- already_json_response(response_code, FFI_Yajl::Encoder.encode(json, :pretty => do_pretty_json), request_version, response_version)
212
+ # Serializes `data` to JSON and returns an Array with the
213
+ # response code, HTTP headers and JSON body.
214
+ #
215
+ # @param [Fixnum] response_code HTTP response code
216
+ # @param [Hash] data The data for the response body as a Hash
217
+ # @param [Hash] options
218
+ # @option options [Hash] :headers (see #already_json_response)
219
+ # @option options [Boolean] :pretty (true) Pretty-format the JSON
220
+ # @option options [Fixnum] :request_version (see #already_json_response)
221
+ # @option options [Fixnum] :response_version (see #already_json_response)
222
+ #
223
+ # @return (see #already_json_response)
224
+ #
225
+ def json_response(response_code, data, options={})
226
+ options = { pretty: true }.merge(options)
227
+ do_pretty_json = !!options.delete(:pretty) # make sure we have a proper Boolean.
228
+ json = FFI_Yajl::Encoder.encode(data, pretty: do_pretty_json)
229
+ already_json_response(response_code, json, options)
205
230
  end
206
231
 
207
232
  def text_response(response_code, text)
208
233
  [response_code, {"Content-Type" => "text/plain"}, text]
209
234
  end
210
235
 
211
- def already_json_response(response_code, json_text, request_version=0, response_version=0)
212
- header = { "min_version" => MIN_API_VERSION.to_s, "max_version" => MAX_API_VERSION.to_s,
213
- "request_version" => request_version.to_s,
214
- "response_version" => response_version.to_s }
215
- [ response_code,
216
- { "Content-Type" => "application/json",
217
- "X-Ops-Server-API-Version" => FFI_Yajl::Encoder.encode(header) },
218
- json_text ]
236
+ # Returns an Array with the response code, HTTP headers, and JSON body.
237
+ #
238
+ # @param [Fixnum] response_code The HTTP response code
239
+ # @param [String] json_text The JSON body for the response
240
+ # @param [Hash] options
241
+ # @option options [Hash] :headers ({}) HTTP headers (may override default headers)
242
+ # @option options [Fixnum] :request_version (0) Request API version
243
+ # @option options [Fixnum] :response_version (0) Response API version
244
+ #
245
+ # @return [Array(Fixnum, Hash{String => String}, String)]
246
+ #
247
+ def already_json_response(response_code, json_text, options={})
248
+ version_header = FFI_Yajl::Encoder.encode(
249
+ "min_version" => MIN_API_VERSION.to_s,
250
+ "max_version" => MAX_API_VERSION.to_s,
251
+ "request_version" => options[:request_version] || DEFAULT_REQUEST_VERSION.to_s,
252
+ "response_version" => options[:response_version] || DEFAULT_RESPONSE_VERSION.to_s
253
+ )
254
+
255
+ headers = {
256
+ "Content-Type" => "application/json",
257
+ "X-Ops-Server-API-Version" => version_header
258
+ }
259
+ headers.merge!(options[:headers]) if options[:headers]
260
+
261
+ [ response_code, headers, json_text ]
219
262
  end
220
263
 
221
264
  # To be called from inside rest endpoints
@@ -224,12 +267,12 @@ module ChefZero
224
267
  # Strip off /organizations/chef if we are in single org mode
225
268
  if rest_path[0..1] != [ 'organizations', server.options[:single_org] ]
226
269
  raise "Unexpected URL #{rest_path[0..1]} passed to build_uri in single org mode"
227
- else
228
- "#{base_uri}/#{rest_path[2..-1].join('/')}"
229
270
  end
230
- else
231
- "#{base_uri}/#{rest_path.join('/')}"
271
+
272
+ return self.class.build_uri(base_uri, rest_path[2..-1])
232
273
  end
274
+
275
+ self.class.build_uri(base_uri, rest_path)
233
276
  end
234
277
 
235
278
  def self.build_uri(base_uri, rest_path)