cloudkit 0.9.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 (59) hide show
  1. data/CHANGES +2 -0
  2. data/COPYING +20 -0
  3. data/README +55 -0
  4. data/Rakefile +35 -0
  5. data/TODO +22 -0
  6. data/cloudkit.gemspec +82 -0
  7. data/doc/curl.html +329 -0
  8. data/doc/images/example-code.gif +0 -0
  9. data/doc/images/json-title.gif +0 -0
  10. data/doc/images/oauth-discovery-logo.gif +0 -0
  11. data/doc/images/openid-logo.gif +0 -0
  12. data/doc/index.html +87 -0
  13. data/doc/main.css +151 -0
  14. data/doc/rest-api.html +358 -0
  15. data/examples/1.ru +3 -0
  16. data/examples/2.ru +3 -0
  17. data/examples/3.ru +6 -0
  18. data/examples/4.ru +5 -0
  19. data/examples/5.ru +10 -0
  20. data/examples/6.ru +10 -0
  21. data/examples/TOC +17 -0
  22. data/lib/cloudkit.rb +74 -0
  23. data/lib/cloudkit/flash_session.rb +22 -0
  24. data/lib/cloudkit/oauth_filter.rb +273 -0
  25. data/lib/cloudkit/oauth_store.rb +56 -0
  26. data/lib/cloudkit/openid_filter.rb +198 -0
  27. data/lib/cloudkit/openid_store.rb +101 -0
  28. data/lib/cloudkit/rack/builder.rb +120 -0
  29. data/lib/cloudkit/rack/router.rb +20 -0
  30. data/lib/cloudkit/request.rb +159 -0
  31. data/lib/cloudkit/service.rb +135 -0
  32. data/lib/cloudkit/store.rb +459 -0
  33. data/lib/cloudkit/store/adapter.rb +9 -0
  34. data/lib/cloudkit/store/extraction_view.rb +57 -0
  35. data/lib/cloudkit/store/response.rb +51 -0
  36. data/lib/cloudkit/store/response_helpers.rb +72 -0
  37. data/lib/cloudkit/store/sql_adapter.rb +36 -0
  38. data/lib/cloudkit/templates/authorize_request_token.erb +19 -0
  39. data/lib/cloudkit/templates/oauth_descriptor.erb +43 -0
  40. data/lib/cloudkit/templates/oauth_meta.erb +8 -0
  41. data/lib/cloudkit/templates/openid_login.erb +31 -0
  42. data/lib/cloudkit/templates/request_authorization.erb +23 -0
  43. data/lib/cloudkit/templates/request_token_denied.erb +18 -0
  44. data/lib/cloudkit/user_store.rb +44 -0
  45. data/lib/cloudkit/util.rb +60 -0
  46. data/test/ext_test.rb +57 -0
  47. data/test/flash_session_test.rb +22 -0
  48. data/test/helper.rb +50 -0
  49. data/test/oauth_filter_test.rb +331 -0
  50. data/test/oauth_store_test.rb +12 -0
  51. data/test/openid_filter_test.rb +54 -0
  52. data/test/openid_store_test.rb +12 -0
  53. data/test/rack_builder_test.rb +41 -0
  54. data/test/request_test.rb +197 -0
  55. data/test/service_test.rb +718 -0
  56. data/test/store_test.rb +99 -0
  57. data/test/user_store_test.rb +12 -0
  58. data/test/util_test.rb +13 -0
  59. metadata +190 -0
data/test/ext_test.rb ADDED
@@ -0,0 +1,57 @@
1
+ require 'helper'
2
+ class ExtTest < Test::Unit::TestCase
3
+
4
+ context "A Hash" do
5
+
6
+ should "re-key an entry if it exists" do
7
+ x = {:a => 1, :b => 2}
8
+ x.rekey!(:b, :c)
9
+ assert x == {:a => 1, :c => 2}
10
+ x.rekey!(:d, :b)
11
+ assert x == {:a => 1, :c => 2}
12
+ end
13
+
14
+ should "merge conditionally" do
15
+ x = {:a => 1}
16
+ y = {:b => 2}
17
+ x.filter_merge!(:c => y[:c])
18
+ assert x == {:a => 1}
19
+ x.filter_merge!(:c => y[:b])
20
+ assert x == {:a => 1, :c => 2}
21
+ x = {}.filter_merge!(:a => 1)
22
+ assert x == {:a => 1}
23
+ end
24
+
25
+ should "exclude pairs using a single key" do
26
+ x = {:a => 1, :b => 2}
27
+ y = x.excluding(:b)
28
+ assert y == {:a => 1}
29
+ end
30
+
31
+ should "exclude pairs using a list of keys" do
32
+ x = {:a => 1, :b => 2, :c => 3}
33
+ y = x.excluding(:b, :c)
34
+ assert y == {:a => 1}
35
+ end
36
+ end
37
+
38
+ context "An Array" do
39
+
40
+ should "exclude elements" do
41
+ x = [0, 1, 2, 3]
42
+ y = x.excluding(1, 3)
43
+ assert_equal [0, 2], y
44
+ end
45
+ end
46
+
47
+ context "An Object" do
48
+
49
+ should "try" do
50
+ x = {:a => 'a'}
51
+ result = x[:a].try(:upcase)
52
+ assert_equal 'A', result
53
+ assert_nothing_raised {x[:b].try(:upcase)}
54
+ end
55
+ end
56
+
57
+ end
@@ -0,0 +1,22 @@
1
+ require 'helper'
2
+ class FlashSessionTest < Test::Unit::TestCase
3
+
4
+ context "A FlashSession" do
5
+
6
+ setup do
7
+ @flash = CloudKit::FlashSession.new
8
+ end
9
+
10
+ should "accept a value for a key" do
11
+ @flash['greeting'] = 'hello'
12
+ assert_equal 'hello', @flash['greeting']
13
+ end
14
+
15
+ should "erase a key/value pair after access" do
16
+ @flash['greeting'] = 'hello'
17
+ x = @flash['greeting']
18
+ assert_nil @flash['greeting']
19
+ end
20
+
21
+ end
22
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,50 @@
1
+ $:.unshift File.expand_path(File.dirname(__FILE__)) + '/../lib'
2
+ require 'cloudkit'
3
+ require 'test/unit'
4
+ require 'shoulda'
5
+ require 'rexml/document'
6
+
7
+ def auth_key; "cloudkit.user"; end
8
+ def remote_user; "/cloudkit_users/abcdef"; end
9
+
10
+ def echo_text(text)
11
+ lambda {|env| [200, {'Content-Type' => 'text/html'}, [text]]}
12
+ end
13
+
14
+ def echo_env(key)
15
+ lambda {|env| [200, {'Content-Type' => 'text/html'}, [env[key] || '']]}
16
+ end
17
+
18
+ def auth
19
+ {auth_key => remote_user}
20
+ end
21
+
22
+ def plain_service
23
+ Rack::Builder.new do
24
+ use Rack::Config do |env|
25
+ env['cloudkit.storage.uri'] = 'sqlite://service.db'
26
+ end
27
+ use CloudKit::Service, :collections => [:items, :things]
28
+ run echo_text('martino')
29
+ end
30
+ end
31
+
32
+ def authed_service
33
+ Rack::Builder.new do
34
+ use Rack::Config do |env|
35
+ env['cloudkit.storage.uri'] = 'sqlite://service.db'
36
+ r = CloudKit::Request.new(env)
37
+ r.announce_auth('cloudkit.filter.oauth') # mock
38
+ end
39
+ use CloudKit::Service, :collections => [:items, :things]
40
+ run echo_text('martino')
41
+ end
42
+ end
43
+
44
+ def openid_app
45
+ Rack::Builder.new do
46
+ use Rack::Session::Pool
47
+ use CloudKit::OpenIDFilter
48
+ run echo_env(auth_key)
49
+ end
50
+ end
@@ -0,0 +1,331 @@
1
+ require 'helper'
2
+ class OAuthFilterTest < Test::Unit::TestCase
3
+
4
+ context "An OAuthFilter" do
5
+
6
+ setup do
7
+ @oauth_filtered_app = CloudKit::OAuthFilter.new(echo_env(auth_key))
8
+ token = JSON.generate(
9
+ :secret => 'pfkkdhi9sl3r4s00',
10
+ :consumer_key => 'dpf43f3p2l4k3l03',
11
+ :consumer_secret => 'kd94hf93k423kf44',
12
+ :user_id => 'martino')
13
+ Rack::MockRequest.new(@oauth_filtered_app).get('/') # prime the storage
14
+ @store = @oauth_filtered_app.store
15
+ result = @store.put('/cloudkit_oauth_tokens/nnch734d00sl2jdk', :json => token)
16
+ @token_etag = result.parsed_content['etag']
17
+ end
18
+
19
+ teardown do
20
+ @store.reset!
21
+ @store.load_static_consumer
22
+ end
23
+
24
+ should "verify signatures" do
25
+ response = do_get
26
+ assert_equal 200, response.status
27
+ assert_equal 'martino', response.body
28
+ end
29
+
30
+ should "notify downstream nodes of its presence" do
31
+ app = CloudKit::OAuthFilter.new(echo_env('cloudkit.via'))
32
+ response = Rack::MockRequest.new(app).get('/')
33
+ assert_equal 'cloudkit.filter.oauth', response.body
34
+ end
35
+
36
+ should "not allow a nonce/timestamp combination to appear twice" do
37
+ do_get
38
+ response = do_get
39
+ assert_equal '', response.body
40
+ get_request_token
41
+ response = get_request_token
42
+ assert_equal 401, response.status
43
+ end
44
+
45
+ should "add the remote user to the rack environment for verified requests" do
46
+ response = do_get
47
+ assert_equal 'martino', response.body
48
+ end
49
+
50
+ should "allow requests for / to pass through" do
51
+ response = Rack::MockRequest.new(@oauth_filtered_app).get('/')
52
+ assert_equal 200, response.status
53
+ end
54
+
55
+ should "reject unauthorized requests" do
56
+ response = Rack::MockRequest.new(@oauth_filtered_app).get(
57
+ 'http://photos.example.net/photos?file=vacation.jpg&size=original' +
58
+ '&oauth_version=1.0' +
59
+ '&oauth_consumer_key=dpf43f3p2l4k3l03' +
60
+ '&oauth_token=nnch734d00sl2jdk' +
61
+ '&oauth_timestamp=1191242096' +
62
+ '&oauth_nonce=kllo9940pd9333jh' +
63
+ '&oauth_signature=fail'+
64
+ '&oauth_signature_method=HMAC-SHA1', 'X-Remote-User' => 'intruder') # TODO rework
65
+ assert_equal '', response.body
66
+ end
67
+
68
+ context "supporting OAuth Discovery" do
69
+
70
+ should "set the auth challenge for unauthorized requests" do
71
+ app = CloudKit::OAuthFilter.new(
72
+ lambda {|env| [200, {}, [env['cloudkit.challenge']['WWW-Authenticate'] || '']]})
73
+ response = Rack::MockRequest.new(app).get(
74
+ '/items', 'HTTP_HOST' => 'example.org')
75
+ assert_equal 'OAuth realm="http://example.org"', response.body
76
+ app = CloudKit::OAuthFilter.new(
77
+ lambda {|env| [200, {}, [env['cloudkit.challenge']['Link'] || '']]})
78
+ response = Rack::MockRequest.new(app).get(
79
+ '/items', 'HTTP_HOST' => 'example.org')
80
+ assert_equal '<http://example.org/oauth/meta>; rel="http://oauth.net/discovery/1.0/rel/provider"',
81
+ response.body
82
+ end
83
+
84
+ should "provide XRD metadata on GET /oauth/meta" do
85
+ response = Rack::MockRequest.new(@oauth_filtered_app).get(
86
+ '/oauth/meta', 'HTTP_HOST' => 'example.org')
87
+ assert_equal 200, response.status
88
+ doc = REXML::Document.new(response.body)
89
+ assert REXML::XPath.first(doc, '//XRD/Type')
90
+ assert_equal 'http://oauth.net/discovery/1.0',
91
+ REXML::XPath.first(doc, '//XRD/Type').children[0].to_s
92
+ assert REXML::XPath.first(doc, '//XRD/Service/Type')
93
+ assert_equal 'http://oauth.net/discovery/1.0/rel/provider',
94
+ REXML::XPath.first(doc, '//XRD/Service/Type').children[0].to_s
95
+ assert REXML::XPath.first(doc, '//XRD/Service/URI')
96
+ assert_equal 'http://example.org/oauth',
97
+ REXML::XPath.first(doc, '//XRD/Service/URI').children[0].to_s
98
+ end
99
+
100
+ should "respond to OAuth Discovery Draft 2 / XRDS-Simple Discovery" do
101
+ response = Rack::MockRequest.new(@oauth_filtered_app).get(
102
+ '/anything',
103
+ 'HTTP_HOST' => 'example.org',
104
+ 'HTTP_ACCEPT' => 'application/xrds+xml')
105
+ assert 200, response.status
106
+ assert_equal 'http://example.org/oauth', response['X-XRDS-Location']
107
+ end
108
+
109
+ should "provide a descriptor document on GET /oauth" do
110
+ response = Rack::MockRequest.new(@oauth_filtered_app).get(
111
+ '/oauth', 'HTTP_HOST' => 'example.org')
112
+ assert_equal 200, response.status
113
+ assert_equal 'application/xrds+xml', response['Content-Type']
114
+ end
115
+
116
+ should "populate the static consumer on startup" do
117
+ response = @store.get('/cloudkit_oauth_consumers/cloudkitconsumer')
118
+ assert_equal 200, response.status
119
+ end
120
+
121
+ end
122
+
123
+ context "supporting authorization" do
124
+
125
+ should "generate request tokens" do
126
+ response = get_request_token
127
+ assert_equal 201, response.status
128
+ token, secret = response.body.split('&')
129
+ token_parts = token.split('=')
130
+ secret_parts = secret.split('=')
131
+ assert_equal 'oauth_token', token_parts.first
132
+ assert_equal 'oauth_token_secret', secret_parts.first
133
+ assert token_parts.last
134
+ assert !token_parts.last.empty?
135
+ assert secret_parts.last
136
+ assert !secret_parts.last.empty?
137
+ end
138
+
139
+ should "not generate request tokens for invalid consumers" do
140
+ # this does not mean consumers must register, only that they
141
+ # should use the static value provided in the xrd document
142
+ # or one that is specified in the consumer database
143
+ pre_sign = Rack::Request.new(Rack::MockRequest.env_for(
144
+ 'http://photos.example.net/oauth/request_tokens',
145
+ 'Authorization' => 'OAuth realm="http://photos.example.net", ' +
146
+ 'oauth_version="1.0", ' +
147
+ 'oauth_consumer_key="mysteryconsumer", ' +
148
+ 'oauth_timestamp="1191242096", ' +
149
+ 'oauth_nonce="AAAAAAAAAAAAAAAAA", ' +
150
+ 'oauth_signature_method="HMAC-SHA1"',
151
+ :method => "POST"))
152
+ signature = OAuth::Signature.build(pre_sign) do |token, consumer_key|
153
+ [nil, '']
154
+ end
155
+ response = Rack::MockRequest.new(@oauth_filtered_app).post(
156
+ 'http://photos.example.net/oauth/request_tokens?' +
157
+ 'oauth_version=1.0' +
158
+ '&oauth_consumer_key=mysteryconsumer' +
159
+ '&oauth_timestamp=1191242096' +
160
+ '&oauth_nonce=AAAAAAAAAAAAAAAAA' +
161
+ '&oauth_signature=' + CGI.escape(signature.signature) +
162
+ '&oauth_signature_method=HMAC-SHA1')
163
+ assert_equal 401, response.status
164
+ end
165
+
166
+ should "store request tokens for authorizaton" do
167
+ response = get_request_token
168
+ assert_equal 201, response.status
169
+ token, secret = extract_token(response)
170
+ request_token = @store.get("/cloudkit_oauth_request_tokens/#{token}").parsed_content
171
+ assert request_token
172
+ assert_equal secret, request_token['secret']
173
+ assert !request_token['authorized_at']
174
+ end
175
+
176
+ should "redirect to login before allowing GET requests for request token authorization" do
177
+ response = get_request_token
178
+ token, secret = extract_token(response)
179
+ response = Rack::MockRequest.new(@oauth_filtered_app).get(
180
+ "/oauth/authorization?oauth_token=#{token}")
181
+ assert_equal 302, response.status
182
+ assert_equal '/login', response['Location']
183
+ end
184
+
185
+ should "respond successfully to authorization GET requests for logged-in users with a valid request token" do
186
+ response = get_request_token
187
+ token, secret = extract_token(response)
188
+ response = Rack::MockRequest.new(@oauth_filtered_app).get(
189
+ "/oauth/authorization?oauth_token=#{token}", auth)
190
+ assert_equal 200, response.status
191
+ end
192
+
193
+ should "reject authorization GET requests with invalid tokens" do
194
+ response = get_request_token
195
+ token, secret = extract_token(response)
196
+ response = Rack::MockRequest.new(@oauth_filtered_app).get(
197
+ "/oauth/authorization?oauth_token=fail", auth)
198
+ assert_equal 401, response.status
199
+ end
200
+
201
+ should "authorize request tokens for verified requests" do
202
+ response = get_request_token
203
+ token, secret = extract_token(response)
204
+ response = Rack::MockRequest.new(@oauth_filtered_app).put(
205
+ "/oauth/authorized_request_tokens/#{token}?submit=Approve", auth)
206
+ assert_equal 200, response.status
207
+ request_token = @store.get("/cloudkit_oauth_request_tokens/#{token}").parsed_content
208
+ assert request_token['authorized_at']
209
+ assert request_token['user_id']
210
+ end
211
+
212
+ should "removed denied request tokens" do
213
+ response = get_request_token
214
+ token, secret = extract_token(response)
215
+ response = Rack::MockRequest.new(@oauth_filtered_app).put(
216
+ "/oauth/authorized_request_tokens/#{token}?submit=Deny", auth)
217
+ assert_equal 200, response.status
218
+ request_token = @store.get("/cloudkit_oauth_request_tokens/#{token}").parsed_content
219
+ assert 410, response.status
220
+ end
221
+
222
+ should "redirect to login for authorization PUT requests unless logged-in" do
223
+ response = get_request_token
224
+ token, secret = extract_token(response)
225
+ response = Rack::MockRequest.new(@oauth_filtered_app).put(
226
+ "/oauth/authorized_request_tokens/#{token}?submit=Approve")
227
+ assert_equal 302, response.status
228
+ assert_equal '/login', response['Location']
229
+ end
230
+
231
+ should "not create access tokens for request tokens that have already been authorized" do
232
+ response = get_request_token
233
+ token, secret = extract_token(response)
234
+ response = Rack::MockRequest.new(@oauth_filtered_app).put(
235
+ "/oauth/authorized_request_tokens/#{token}?submit=Approve", auth)
236
+ assert_equal 200, response.status
237
+ response = Rack::MockRequest.new(@oauth_filtered_app).put(
238
+ "/oauth/authorized_request_tokens/#{token}?submit=Approve", auth)
239
+ assert_equal 401, response.status
240
+ end
241
+
242
+ should "provide access tokens in exchange for authorized request tokens" do
243
+ response = get_access_token
244
+ assert_equal 201, response.status
245
+ token, secret = extract_token(response)
246
+ assert !token.empty?
247
+ assert !secret.empty?
248
+ end
249
+
250
+ should "remove request tokens after creating access tokens" do
251
+ response = get_access_token
252
+ assert_equal 201, response.status
253
+ request_tokens = @store.get('/cloudkit_oauth_request_tokens').parsed_content
254
+ assert_equal 0, request_tokens['uris'].size
255
+ end
256
+
257
+ end
258
+ end
259
+
260
+ def do_get
261
+ Rack::MockRequest.new(@oauth_filtered_app).get(
262
+ 'http://photos.example.net/photos?file=vacation.jpg&size=original' +
263
+ '&oauth_version=1.0' +
264
+ '&oauth_consumer_key=dpf43f3p2l4k3l03' +
265
+ '&oauth_token=nnch734d00sl2jdk' +
266
+ '&oauth_timestamp=1191242096' +
267
+ '&oauth_nonce=kllo9940pd9333jh' +
268
+ '&oauth_signature=tR3%2BTy81lMeYAr%2FFid0kMTYa%2FWM%3D' +
269
+ '&oauth_signature_method=HMAC-SHA1')
270
+ end
271
+
272
+ def get_request_token
273
+ pre_sign = Rack::Request.new(Rack::MockRequest.env_for(
274
+ 'http://photos.example.net/oauth/request_tokens',
275
+ 'Authorization' => 'OAuth realm="http://photos.example.net", ' +
276
+ 'oauth_version="1.0", ' +
277
+ 'oauth_consumer_key="cloudkitconsumer", ' +
278
+ 'oauth_timestamp="1191242096", ' +
279
+ 'oauth_nonce="AAAAAAAAAAAAAAAAA", ' +
280
+ 'oauth_signature_method="HMAC-SHA1"',
281
+ :method => "POST"))
282
+ signature = OAuth::Signature.build(pre_sign) do |token, consumer_key|
283
+ [nil, '']
284
+ end
285
+ Rack::MockRequest.new(@oauth_filtered_app).post(
286
+ 'http://photos.example.net/oauth/request_tokens?' +
287
+ 'oauth_version=1.0' +
288
+ '&oauth_consumer_key=cloudkitconsumer' +
289
+ '&oauth_timestamp=1191242096' +
290
+ '&oauth_nonce=AAAAAAAAAAAAAAAAA' +
291
+ '&oauth_signature=' + CGI.escape(signature.signature) +
292
+ '&oauth_signature_method=HMAC-SHA1')
293
+ end
294
+
295
+ def get_access_token
296
+ response = get_request_token
297
+ token, secret = extract_token(response)
298
+ response = Rack::MockRequest.new(@oauth_filtered_app).put(
299
+ "/oauth/authorized_request_tokens/#{token}", auth)
300
+ assert_equal 200, response.status
301
+ pre_sign = Rack::Request.new(Rack::MockRequest.env_for(
302
+ 'http://photos.example.net/oauth/access_tokens',
303
+ 'Authorization' => 'OAuth realm="http://photos.example.net", ' +
304
+ 'oauth_version="1.0", ' +
305
+ 'oauth_consumer_key="cloudkitconsumer", ' +
306
+ 'oauth_token="' + token + '", ' +
307
+ 'oauth_timestamp="1191242097", ' +
308
+ 'oauth_nonce="AAAAAAAAAAAAAAAAA", ' +
309
+ 'oauth_signature_method="HMAC-SHA1"',
310
+ :method => "POST"))
311
+ signature = OAuth::Signature.build(pre_sign) do |token, consumer_key|
312
+ [secret, '']
313
+ end
314
+ Rack::MockRequest.new(@oauth_filtered_app).post(
315
+ 'http://photos.example.net/oauth/access_tokens?' +
316
+ 'oauth_version=1.0' +
317
+ '&oauth_consumer_key=cloudkitconsumer' +
318
+ '&oauth_token=' + token +
319
+ '&oauth_timestamp=1191242097' +
320
+ '&oauth_nonce=AAAAAAAAAAAAAAAAA' +
321
+ '&oauth_signature=' + CGI.escape(signature.signature) +
322
+ '&oauth_signature_method=HMAC-SHA1')
323
+ end
324
+
325
+ def extract_token(response)
326
+ token, secret = response.body.split('&')
327
+ token_parts = token.split('=')
328
+ secret_parts = secret.split('=')
329
+ return token_parts.last, secret_parts.last
330
+ end
331
+ end