cfoundry 0.4.21 → 0.5.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 (50) hide show
  1. data/Rakefile +47 -24
  2. data/lib/cfoundry/auth_token.rb +48 -0
  3. data/lib/cfoundry/baseclient.rb +96 -277
  4. data/lib/cfoundry/client.rb +2 -0
  5. data/lib/cfoundry/concerns/login_helpers.rb +13 -0
  6. data/lib/cfoundry/errors.rb +21 -13
  7. data/lib/cfoundry/rest_client.rb +290 -0
  8. data/lib/cfoundry/test_support.rb +3 -0
  9. data/lib/cfoundry/trace_helpers.rb +9 -9
  10. data/lib/cfoundry/uaaclient.rb +66 -74
  11. data/lib/cfoundry/upload_helpers.rb +2 -0
  12. data/lib/cfoundry/v1/app.rb +2 -2
  13. data/lib/cfoundry/v1/base.rb +4 -51
  14. data/lib/cfoundry/v1/client.rb +7 -30
  15. data/lib/cfoundry/v1/model.rb +22 -5
  16. data/lib/cfoundry/v1/model_magic.rb +30 -15
  17. data/lib/cfoundry/v2/app.rb +2 -5
  18. data/lib/cfoundry/v2/base.rb +10 -74
  19. data/lib/cfoundry/v2/client.rb +19 -30
  20. data/lib/cfoundry/v2/domain.rb +1 -4
  21. data/lib/cfoundry/v2/model.rb +1 -3
  22. data/lib/cfoundry/v2/model_magic.rb +13 -23
  23. data/lib/cfoundry/version.rb +1 -1
  24. data/lib/cfoundry/zip.rb +1 -1
  25. data/spec/cfoundry/auth_token_spec.rb +77 -0
  26. data/spec/cfoundry/baseclient_spec.rb +54 -30
  27. data/spec/cfoundry/errors_spec.rb +10 -13
  28. data/spec/cfoundry/rest_client_spec.rb +238 -0
  29. data/spec/cfoundry/trace_helpers_spec.rb +10 -5
  30. data/spec/cfoundry/uaaclient_spec.rb +141 -114
  31. data/spec/cfoundry/upload_helpers_spec.rb +129 -0
  32. data/spec/cfoundry/v1/base_spec.rb +2 -2
  33. data/spec/cfoundry/v1/client_spec.rb +17 -0
  34. data/spec/cfoundry/v1/model_magic_spec.rb +43 -0
  35. data/spec/cfoundry/v2/base_spec.rb +256 -33
  36. data/spec/cfoundry/v2/client_spec.rb +68 -0
  37. data/spec/cfoundry/v2/model_magic_spec.rb +49 -0
  38. data/spec/fixtures/apps/with_vmcignore/ignored_dir/file_in_ignored_dir.txt +1 -0
  39. data/spec/fixtures/apps/with_vmcignore/ignored_file.txt +1 -0
  40. data/spec/fixtures/apps/with_vmcignore/non_ignored_dir/file_in_non_ignored_dir.txt +1 -0
  41. data/spec/fixtures/apps/with_vmcignore/non_ignored_dir/ignored_file.txt +1 -0
  42. data/spec/fixtures/apps/with_vmcignore/non_ignored_file.txt +1 -0
  43. data/spec/fixtures/empty_file +0 -0
  44. data/spec/spec_helper.rb +4 -4
  45. data/spec/support/randoms.rb +3 -0
  46. data/spec/support/shared_examples/client_login_examples.rb +46 -0
  47. data/spec/support/{summaries.rb → shared_examples/model_summary_examples.rb} +0 -0
  48. data/spec/support/v1_fake_helper.rb +144 -0
  49. metadata +101 -37
  50. data/lib/cfoundry/spec_helper.rb +0 -1
@@ -1,4 +1,6 @@
1
1
  require "cfoundry/baseclient"
2
+ require "cfoundry/rest_client"
3
+ require "cfoundry/auth_token"
2
4
 
3
5
  require "cfoundry/v1/app"
4
6
  require "cfoundry/v1/framework"
@@ -0,0 +1,13 @@
1
+ require "base64"
2
+
3
+ module CFoundry
4
+ module LoginHelpers
5
+ def login_prompts
6
+ @base.uaa.prompts
7
+ end
8
+
9
+ def login(username, password)
10
+ @base.token = AuthToken.from_uaa_token_info(@base.uaa.authorize(username, password))
11
+ end
12
+ end
13
+ end
@@ -3,9 +3,11 @@ require "multi_json"
3
3
 
4
4
  module CFoundry
5
5
  # Base class for CFoundry errors (not from the server).
6
- class Error < RuntimeError; end
6
+ class Error < RuntimeError;
7
+ end
7
8
 
8
- class Deprecated < Error; end
9
+ class Deprecated < Error;
10
+ end
9
11
 
10
12
  class Mismatch < Error
11
13
  def initialize(expected, got)
@@ -44,7 +46,7 @@ module CFoundry
44
46
  end
45
47
 
46
48
  def to_s
47
- "#{method::METHOD} #{uri} timed out"
49
+ "#{method} #{uri} timed out"
48
50
  end
49
51
  end
50
52
 
@@ -61,10 +63,10 @@ module CFoundry
61
63
  attr_reader :error_code, :description, :request, :response
62
64
 
63
65
  # Create an APIError with a given request and response.
64
- def initialize(request, response, description = nil, error_code = nil)
66
+ def initialize(description = nil, error_code = nil, request = nil, response = nil)
65
67
  @response = response
66
68
  @request = request
67
- @error_code = error_code || response.code
69
+ @error_code = error_code || (response ? response[:status] : nil)
68
70
  @description = description || parse_description
69
71
  end
70
72
 
@@ -82,12 +84,13 @@ module CFoundry
82
84
  end
83
85
 
84
86
  private
87
+
85
88
  def parse_description
86
- begin
87
- parse_json(response.body)[:description]
88
- rescue MultiJson::DecodeError
89
- response.body
90
- end
89
+ return unless response
90
+
91
+ parse_json(response[:body])[:description]
92
+ rescue MultiJson::DecodeError
93
+ response[:body]
91
94
  end
92
95
 
93
96
  def parse_json(x)
@@ -99,12 +102,17 @@ module CFoundry
99
102
  end
100
103
  end
101
104
 
102
- class NotFound < APIError; end
105
+ class NotFound < APIError
106
+ end
103
107
 
104
- class Denied < APIError; end
108
+ class Denied < APIError
109
+ end
105
110
 
106
- class BadResponse < APIError; end
111
+ class BadResponse < APIError
112
+ end
107
113
 
114
+ class UAAError < APIError
115
+ end
108
116
 
109
117
  def self.define_error(class_name, *codes)
110
118
  base =
@@ -0,0 +1,290 @@
1
+ require "cfoundry/trace_helpers"
2
+ require "net/https"
3
+ require "net/http/post/multipart"
4
+ require "multi_json"
5
+ require "fileutils"
6
+
7
+ module CFoundry
8
+ class RestClient
9
+ include CFoundry::TraceHelpers
10
+
11
+ LOG_LENGTH = 10
12
+
13
+ HTTP_METHODS = {
14
+ "GET" => Net::HTTP::Get,
15
+ "PUT" => Net::HTTP::Put,
16
+ "POST" => Net::HTTP::Post,
17
+ "DELETE" => Net::HTTP::Delete,
18
+ "HEAD" => Net::HTTP::Head,
19
+ }
20
+
21
+ DEFAULT_OPTIONS = {
22
+ :follow_redirects => true
23
+ }
24
+
25
+ attr_reader :target
26
+
27
+ attr_accessor :trace, :backtrace, :log, :request_id, :token, :target, :proxy
28
+
29
+ def initialize(target, token = nil)
30
+ @target = target
31
+ @token = token
32
+ @trace = false
33
+ @backtrace = false
34
+ @log = false
35
+ end
36
+
37
+ def request(method, path, options = {})
38
+ request_uri(method, construct_url(path), DEFAULT_OPTIONS.merge(options))
39
+ end
40
+
41
+ def generate_headers(payload, options)
42
+ headers = {}
43
+
44
+ if payload.is_a?(String)
45
+ headers["Content-Length"] = payload.size
46
+ elsif !payload
47
+ headers["Content-Length"] = 0
48
+ end
49
+
50
+ headers["X-Request-Id"] = @request_id if @request_id
51
+ headers["Authorization"] = @token.auth_header if @token
52
+ headers["Proxy-User"] = @proxy if @proxy
53
+
54
+ if accept_type = mimetype(options[:accept])
55
+ headers["Accept"] = accept_type
56
+ end
57
+
58
+ if content_type = mimetype(options[:content])
59
+ headers["Content-Type"] = content_type
60
+ end
61
+
62
+ headers.merge!(options[:headers]) if options[:headers]
63
+ headers
64
+ end
65
+
66
+ private
67
+
68
+ def request_uri(method, uri, options = {})
69
+ uri = URI.parse(uri)
70
+
71
+ # keep original options in case there's a redirect to follow
72
+ original_options = options.dup
73
+ payload = options[:payload]
74
+
75
+ if params = options[:params]
76
+ if uri.query
77
+ uri.query += "&" + encode_params(params)
78
+ else
79
+ uri.query = encode_params(params)
80
+ end
81
+ end
82
+
83
+ unless payload.is_a?(String)
84
+ case options[:content]
85
+ when :json
86
+ payload = MultiJson.dump(payload)
87
+ when :form
88
+ payload = encode_params(payload)
89
+ end
90
+ end
91
+
92
+ method_class = get_method_class(method)
93
+ if payload.is_a?(Hash)
94
+ multipart = method_class.const_get(:Multipart)
95
+ request = multipart.new(uri.request_uri, payload)
96
+ else
97
+ request = method_class.new(uri.request_uri)
98
+ request.body = payload if payload
99
+ end
100
+
101
+ headers = generate_headers(payload, options)
102
+
103
+ request_hash = {
104
+ :url => uri.to_s,
105
+ :method => method,
106
+ :headers => headers,
107
+ :body => payload
108
+ }
109
+
110
+ print_request(request_hash) if @trace
111
+
112
+ add_headers(request, headers)
113
+
114
+ # TODO: test http proxies
115
+ http = Net::HTTP.new(uri.host, uri.port)
116
+
117
+ # TODO remove this when staging returns streaming responses
118
+ http.read_timeout = 300
119
+
120
+ if uri.is_a?(URI::HTTPS)
121
+ http.use_ssl = true
122
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
123
+ end
124
+
125
+ before = Time.now
126
+ http.start do
127
+ response = http.request(request)
128
+ time = Time.now - before
129
+
130
+ response_hash = {
131
+ :headers => sane_headers(response),
132
+ :status => response.code,
133
+ :body => response.body
134
+ }
135
+
136
+ print_response(response_hash) if @trace
137
+ print_backtrace(caller) if @trace
138
+
139
+ log_request(time, request, response)
140
+
141
+ if response.is_a?(Net::HTTPRedirection) && options[:follow_redirects]
142
+ request_uri("GET", response["location"], original_options)
143
+ else
144
+ return request_hash, response_hash
145
+ end
146
+ end
147
+ rescue ::Timeout::Error => e
148
+ raise Timeout.new(method, uri, e)
149
+ rescue SocketError, Errno::ECONNREFUSED => e
150
+ raise TargetRefused, e.message
151
+ end
152
+
153
+ def construct_url(path)
154
+ path = "/#{path}" unless path[0] == ?\/
155
+ target + path
156
+ end
157
+
158
+ def get_method_class(method_string)
159
+ HTTP_METHODS[method_string.upcase]
160
+ end
161
+
162
+ def add_headers(request, headers)
163
+ headers.each { |key, value| request[key] = value }
164
+ end
165
+
166
+ def mimetype(content)
167
+ case content
168
+ when String
169
+ content
170
+ when :json
171
+ "application/json"
172
+ when :form
173
+ "application/x-www-form-urlencoded"
174
+ when nil
175
+ nil
176
+ # return request headers (not really Accept)
177
+ else
178
+ raise CFoundry::Error, "Unknown mimetype '#{content.inspect}'"
179
+ end
180
+ end
181
+
182
+ def encode_params(hash, escape = true)
183
+ hash.keys.map do |k|
184
+ v = hash[k]
185
+ v = MultiJson.dump(v) if v.is_a?(Hash)
186
+ v = URI.escape(v.to_s, /[^#{URI::PATTERN::UNRESERVED}]/) if escape
187
+ "#{k}=#{v}"
188
+ end.join("&")
189
+ end
190
+
191
+ def log_data(time, request, response)
192
+ { :time => time,
193
+ :request => {
194
+ :method => request.method,
195
+ :url => request.path,
196
+ :headers => sane_headers(request)
197
+ },
198
+ :response => {
199
+ :code => response.code,
200
+ :headers => sane_headers(response)
201
+ }
202
+ }
203
+ end
204
+
205
+ def log_line(io, data)
206
+ io.printf(
207
+ "[%s] %0.3fs %6s -> %d %s\n",
208
+ Time.now.strftime("%F %T"),
209
+ data[:time],
210
+ data[:request][:method].to_s.upcase,
211
+ data[:response][:code],
212
+ data[:request][:url])
213
+ end
214
+
215
+ def log_request(time, request, response)
216
+ return unless @log
217
+
218
+ data = log_data(time, request, response)
219
+
220
+ case @log
221
+ when IO
222
+ log_line(@log, data)
223
+ return
224
+ when String
225
+ if File.exists?(@log)
226
+ log = File.readlines(@log).last(LOG_LENGTH - 1)
227
+ elsif !File.exists?(File.dirname(@log))
228
+ FileUtils.mkdir_p(File.dirname(@log))
229
+ end
230
+
231
+ File.open(@log, "w") do |io|
232
+ log.each { |l| io.print l } if log
233
+ log_line(io, data)
234
+ end
235
+
236
+ return
237
+ end
238
+
239
+ if @log.respond_to?(:call)
240
+ @log.call(data)
241
+ return
242
+ end
243
+
244
+ if @log.respond_to?(:<<)
245
+ @log << data
246
+ return
247
+ end
248
+ end
249
+
250
+ def print_request(request)
251
+ $stderr.puts ">>>"
252
+ $stderr.puts request_trace(request)
253
+ end
254
+
255
+ def print_response(response)
256
+ $stderr.puts response_trace(response)
257
+ $stderr.puts "<<<"
258
+ end
259
+
260
+ def print_backtrace(locs)
261
+ return unless @backtrace
262
+
263
+ interesting_locs = locs.drop_while { |loc|
264
+ loc =~ /\/(cfoundry\/|restclient\/|net\/http)/
265
+ }
266
+
267
+ $stderr.puts "--- backtrace:"
268
+
269
+ $stderr.puts "... (boring)" unless locs == interesting_locs
270
+
271
+ trimmed_locs = interesting_locs[0..5]
272
+
273
+ trimmed_locs.each do |loc|
274
+ $stderr.puts "=== #{loc}"
275
+ end
276
+
277
+ $stderr.puts "... (trimmed)" unless trimmed_locs == interesting_locs
278
+ end
279
+
280
+ def sane_headers(obj)
281
+ hds = {}
282
+
283
+ obj.each_header do |k, v|
284
+ hds[k] = v
285
+ end
286
+
287
+ hds
288
+ end
289
+ end
290
+ end
@@ -0,0 +1,3 @@
1
+ Dir[File.expand_path('../../../spec/{support,fakes}/**/*.rb', __FILE__)].each do |file|
2
+ require file
3
+ end
@@ -6,25 +6,25 @@ module CFoundry
6
6
 
7
7
  def request_trace(request)
8
8
  return nil unless request
9
- info = ["REQUEST: #{request.method} #{request.path}"]
9
+ info = ["REQUEST: #{request[:method]} #{request[:url]}"]
10
10
  info << "REQUEST_HEADERS:"
11
- info << header_trace(request)
12
- info << "REQUEST_BODY: #{request.body}" if request.body
11
+ info << header_trace(request[:headers])
12
+ info << "REQUEST_BODY: #{request[:body]}" if request[:body]
13
13
  info.join("\n")
14
14
  end
15
15
 
16
16
 
17
17
  def response_trace(response)
18
18
  return nil unless response
19
- info = ["RESPONSE: [#{response.code}]"]
19
+ info = ["RESPONSE: [#{response[:status]}]"]
20
20
  info << "RESPONSE_HEADERS:"
21
- info << header_trace(response)
21
+ info << header_trace(response[:headers])
22
22
  info << "RESPONSE_BODY:"
23
23
  begin
24
- parsed_body = MultiJson.load(response.body)
24
+ parsed_body = MultiJson.load(response[:body])
25
25
  info << MultiJson.dump(parsed_body, :pretty => true)
26
26
  rescue
27
- info << "#{response.body}"
27
+ info << "#{response[:body]}"
28
28
  end
29
29
  info.join("\n")
30
30
  end
@@ -32,8 +32,8 @@ module CFoundry
32
32
  private
33
33
 
34
34
  def header_trace(headers)
35
- headers.to_hash.sort.map do |key, value|
36
- " #{key} : #{value.join(", ")}"
35
+ headers.sort.map do |key, value|
36
+ " #{key} : #{value}"
37
37
  end
38
38
  end
39
39
  end
@@ -1,108 +1,100 @@
1
1
  require "cfoundry/baseclient"
2
+ require 'uaa'
2
3
 
3
4
  module CFoundry
4
5
  class UAAClient < BaseClient
5
- attr_accessor :target, :client_id, :redirect_uri, :token, :trace
6
+ attr_accessor :target, :client_id, :token, :trace
6
7
 
7
- def initialize(
8
- target = "https://uaa.cloudfoundry.com",
9
- client_id = "vmc")
8
+ def initialize(target = "https://uaa.cloudfoundry.com", client_id = "vmc")
10
9
  @target = target
11
10
  @client_id = client_id
12
- @redirect_uri = "https://uaa.cloudfoundry.com/redirect/vmc"
11
+ CF::UAA::Misc.symbolize_keys = true
13
12
  end
14
13
 
15
14
  def prompts
16
- get("login", :accept => :json)[:prompts]
15
+ wrap_uaa_errors do
16
+ CF::UAA::Misc.server(target)[:prompts]
17
+ end
17
18
  end
18
19
 
19
- def authorize(credentials)
20
- query = {
21
- :client_id => @client_id,
22
- :response_type => "token",
23
- :redirect_uri => @redirect_uri
24
- }
25
-
26
- auth =
27
- post(
28
- { :credentials => credentials },
29
- "oauth", "authorize",
30
- :return_response => true,
31
- :content => :form,
32
- :accept => :json,
33
- :params => query)
34
-
35
- if auth.is_a? Net::HTTPRedirection
36
- extract_token(auth["location"])
37
- else
38
- json = parse_json(auth.body)
39
- raise CFoundry::Denied.new(nil, nil, json[:error_description], auth.code)
20
+ def authorize(username, password)
21
+ wrap_uaa_errors do
22
+ begin
23
+ creds = { :username => username, :password => password }
24
+ token_issuer.implicit_grant_with_creds(creds)
25
+ rescue CF::UAA::BadResponse => e
26
+ status_code = e.message[/\d+/] || 400
27
+ raise CFoundry::Denied.new("Authorization failed", status_code)
28
+ end
40
29
  end
41
-
42
30
  end
43
31
 
44
32
  def users
45
- get("Users", :accept => :json)
33
+ wrap_uaa_errors do
34
+ scim.query(:user)
35
+ end
46
36
  end
47
37
 
48
38
  def change_password(guid, new, old)
49
- put(
50
- { :password => new, :oldPassword => old },
51
- "Users", guid, "password",
52
- :accept => :json,
53
- :content => :json)
39
+ wrap_uaa_errors do
40
+ scim.change_password(guid, new, old)
41
+ end
54
42
  end
55
43
 
56
44
  def password_score(password)
57
- response = post(
58
- { :password => password },
59
- "password", "score",
60
- :content => :form,
61
- :accept => :json
62
- )
63
- required_score = response[:requiredScore] || 0
64
- case (response[:score] || 0)
65
- when 10 then :strong
66
- when required_score..9 then :good
67
- else :weak
45
+ wrap_uaa_errors do
46
+ response = CF::UAA::Misc.password_strength(target, password)
47
+
48
+ required_score = response[:requiredScore] || 0
49
+ case (response[:score] || 0)
50
+ when 10 then
51
+ :strong
52
+ when required_score..9 then
53
+ :good
54
+ else
55
+ :weak
56
+ end
68
57
  end
69
58
  end
70
59
 
71
- private
72
-
73
- def handle_response(response, accept, request)
74
- case response
75
- when Net::HTTPSuccess, Net::HTTPRedirection
76
- accept == :json ? parse_json(response.body) : response.body
77
- when Net::HTTPBadRequest, Net::HTTPUnauthorized, Net::HTTPForbidden
78
- info = parse_json(response.body)
79
- raise Denied.new(request, response, info[:error_description])
80
-
81
- when Net::HTTPNotFound
82
- raise CFoundry::NotFound.new(request, response)
83
-
84
- when Net::HTTPConflict
85
- info = parse_json(response.body)
86
- raise CFoundry::Denied.new(request, response, info[:message])
87
-
88
- else
89
- raise BadResponse.new(request, response)
60
+ def add_user(email, password)
61
+ wrap_uaa_errors do
62
+ scim.add(
63
+ :user,
64
+ {:userName => email,
65
+ :emails => [{:value => email}],
66
+ :password => password,
67
+ :name => {:givenName => email, :familyName => email}
68
+ }
69
+ )
90
70
  end
91
71
  end
92
72
 
93
- def extract_token(url)
94
- _, params = url.split('#')
95
- return unless params
73
+ private
96
74
 
97
- values = {}
98
- params.split("&").each do |pair|
99
- key, val = pair.split("=")
100
- values[key] = val
101
- end
75
+ def token_issuer
76
+ @token_issuer ||= CF::UAA::TokenIssuer.new(target, client_id, nil, :symbolize_keys => true)
77
+ @token_issuer.logger.level = @trace ? 0 : 1
78
+ @token_issuer
79
+ end
102
80
 
103
- return unless values["access_token"] && values["token_type"]
81
+ def scim
82
+ auth_header = token && token.auth_header
83
+ scim = CF::UAA::Scim.new(target, auth_header)
84
+ scim.logger.level = @trace ? 0 : 1
85
+ scim
86
+ end
104
87
 
105
- "#{values["token_type"]} #{values["access_token"]}"
88
+ def wrap_uaa_errors
89
+ yield
90
+ rescue CF::UAA::BadResponse
91
+ raise CFoundry::BadResponse
92
+ rescue CF::UAA::NotFound
93
+ raise CFoundry::NotFound
94
+ rescue CF::UAA::InvalidToken
95
+ raise CFoundry::Denied
96
+ rescue CF::UAA::TargetError => e
97
+ raise CFoundry::UAAError.new(e.info["error_description"], e.info["error"])
106
98
  end
107
99
  end
108
100
  end