cfoundry 0.4.21 → 0.5.0

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