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/examples/1.ru ADDED
@@ -0,0 +1,3 @@
1
+ $:.unshift File.expand_path(File.dirname(__FILE__)) + '/../lib'
2
+ require 'cloudkit'
3
+ expose :notes
data/examples/2.ru ADDED
@@ -0,0 +1,3 @@
1
+ $:.unshift File.expand_path(File.dirname(__FILE__)) + '/../lib'
2
+ require 'cloudkit'
3
+ contain :notes
data/examples/3.ru ADDED
@@ -0,0 +1,6 @@
1
+ $:.unshift File.expand_path(File.dirname(__FILE__)) + '/../lib'
2
+ require 'cloudkit'
3
+ use Rack::Session::Pool
4
+ use CloudKit::OpenIDFilter
5
+ use CloudKit::Service, :collections => [:notes]
6
+ run lambda{|env| [200, {}, ['HELLO']]}
data/examples/4.ru ADDED
@@ -0,0 +1,5 @@
1
+ $:.unshift File.expand_path(File.dirname(__FILE__)) + '/../lib'
2
+ require 'cloudkit'
3
+ use CloudKit::OAuthFilter
4
+ use CloudKit::Service, :collections => [:notes]
5
+ run lambda{|env| [200, {}, ['HELLO']]}
data/examples/5.ru ADDED
@@ -0,0 +1,10 @@
1
+ $:.unshift File.expand_path(File.dirname(__FILE__)) + '/../lib'
2
+ require 'cloudkit'
3
+ use Rack::Config do |env|
4
+ env['cloudkit.storage.uri'] = 'sqlite://example.db'
5
+ end
6
+ use Rack::Session::Pool
7
+ use CloudKit::OAuthFilter
8
+ use CloudKit::OpenIDFilter
9
+ use CloudKit::Service, :collections => [:notes]
10
+ run lambda{|env| [200, {}, ['HELLO']]}
data/examples/6.ru ADDED
@@ -0,0 +1,10 @@
1
+ $:.unshift File.expand_path(File.dirname(__FILE__)) + '/../lib'
2
+ require 'cloudkit'
3
+ use Rack::Config do |env|
4
+ env['cloudkit.storage.uri'] = 'mysql://root:@localhost/cloudkit_example'
5
+ end
6
+ use Rack::Session::Pool
7
+ use CloudKit::OAuthFilter
8
+ use CloudKit::OpenIDFilter
9
+ use CloudKit::Service, :collections => [:notes]
10
+ run lambda{|env| [200, {}, ['HELLO']]}
data/examples/TOC ADDED
@@ -0,0 +1,17 @@
1
+ Index of Examples
2
+ -----------------
3
+
4
+ When using the gem version of CloudKit, the first line of each example can be
5
+ removed.
6
+
7
+ 1. Expose Notes - Mount a JSON "notes" API. Uses in-memory SQLite.
8
+
9
+ 2. Contain Notes - Same as #1, adding OpenID and OAuth.
10
+
11
+ 3. Notes with OpenID - Same as #1 using only OpenID.
12
+
13
+ 4. Notes with OAuth - Same as #1 using only OAuth.
14
+
15
+ 5. SQLite Notes - Same as #2 with a SQLite file store.
16
+
17
+ 6. MySQL Notes - Same as #2 with MySQL store.
data/lib/cloudkit.rb ADDED
@@ -0,0 +1,74 @@
1
+ require 'rubygems'
2
+ require 'erb'
3
+ require 'json'
4
+ require 'md5'
5
+ require 'openid'
6
+ gem 'sequel', '=2.6.0'
7
+ require 'sequel'
8
+ require 'time'
9
+ require 'uuid'
10
+ require 'rack'
11
+ require 'rack/config'
12
+ require 'oauth'
13
+ require 'oauth/consumer'
14
+ require 'oauth/request_proxy/rack_request'
15
+ require 'oauth/server'
16
+ require 'oauth/signature'
17
+ require 'cloudkit/util'
18
+ require 'cloudkit/store/adapter'
19
+ require 'cloudkit/store/extraction_view'
20
+ require 'cloudkit/store/response'
21
+ require 'cloudkit/store/response_helpers'
22
+ require 'cloudkit/store/sql_adapter'
23
+ require 'cloudkit/store'
24
+ require 'cloudkit/flash_session'
25
+ require 'cloudkit/oauth_filter'
26
+ require 'cloudkit/oauth_store'
27
+ require 'cloudkit/openid_filter'
28
+ require 'cloudkit/openid_store'
29
+ require 'cloudkit/rack/builder'
30
+ require 'cloudkit/rack/router'
31
+ require 'cloudkit/request'
32
+ require 'cloudkit/service'
33
+ require 'cloudkit/user_store'
34
+
35
+ class Object
36
+
37
+ # Execute a method if it exists.
38
+ def try(method) # via defunkt
39
+ send method if respond_to? method
40
+ end
41
+ end
42
+
43
+ class Hash
44
+
45
+ # For each key in 'other' that has a non-nil value, merge it into the current
46
+ # Hash.
47
+ def filter_merge!(other={})
48
+ other.each_pair{|k,v| self.merge!(k => v) if v}
49
+ self
50
+ end
51
+
52
+ # Change the key 'oldkey' to 'newkey'
53
+ def rekey!(oldkey, newkey)
54
+ if self[oldkey]
55
+ self[newkey] = self.delete(oldkey)
56
+ end
57
+ end
58
+
59
+ # Return a new Hash, excluding the specified list of keys.
60
+ def excluding(*keys)
61
+ trimmed = self.dup
62
+ keys.each{|k| trimmed.delete(k)}
63
+ trimmed
64
+ end
65
+ end
66
+
67
+ class Array
68
+
69
+ # Return a new Array, excluding the specified list of values.
70
+ def excluding(*keys)
71
+ trimmed = self.dup
72
+ trimmed.reject{|v| keys.include?(v)}
73
+ end
74
+ end
@@ -0,0 +1,22 @@
1
+ module CloudKit
2
+
3
+ # FlashSessions are hashes that forget their contents after the first access.
4
+ # Useful for session-based messaging.
5
+ class FlashSession
6
+ def initialize
7
+ @values = {}
8
+ end
9
+
10
+ # Set the value for a key.
11
+ def []=(k, v)
12
+ @values[k] = v
13
+ end
14
+
15
+ # Access a value, then forget it.
16
+ def [](k)
17
+ v = @values[k]
18
+ @values[k] = nil
19
+ v
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,273 @@
1
+ module CloudKit
2
+
3
+ # An OAuthFilter provides both OAuth 1.0 support, plus OAuth Discovery.
4
+ #
5
+ # Responds to the following URIs as part of the OAuth 1.0 "dance":
6
+ #
7
+ # /oauth/request_tokens
8
+ # /oauth/authorization
9
+ # /oauth/authorized_request_tokens/{id}
10
+ # /oauth/access_tokens
11
+ #
12
+ # Responds to the following URIs are part of OAuth Discovery:
13
+ # /oauth
14
+ # /oauth/meta
15
+ #
16
+ # See also:
17
+ # - {OAuth Core 1.0}[http://oauth.net/core/1.0]
18
+ # - {OAuth Discovery}[http://oauth.net/discovery]
19
+ # - Thread[http://groups.google.com/group/oauth/browse_thread/thread/29a1b550396f63cf] covering /oauth and /oauth/meta URIs.
20
+ #
21
+ class OAuthFilter
22
+ include Util
23
+
24
+ @@lock = Mutex.new
25
+ @@store = nil
26
+
27
+ def initialize(app, options={})
28
+ @app = app
29
+ @options = options
30
+ end
31
+
32
+ def call(env)
33
+ @@lock.synchronize do
34
+ @@store = OAuthStore.new(env[storage_uri_key])
35
+ end unless @@store
36
+
37
+ request = Request.new(env)
38
+ request.announce_auth(oauth_filter_key)
39
+ return xrds_location(request) if oauth_disco_draft2_xrds?(request)
40
+ return @app.call(env) if request.path_info == '/'
41
+
42
+ load_user_from_session(request)
43
+
44
+ begin
45
+ case request
46
+ when r(:get, '/oauth/meta')
47
+ get_meta(request)
48
+ when r(:post, '/oauth/request_tokens', ['oauth_consumer_key'])
49
+ create_request_token(request)
50
+ when r(:get, '/oauth/authorization', ['oauth_token'])
51
+ request_authorization(request)
52
+ when r(:put, '/oauth/authorized_request_tokens/:id', ['submit' => 'Approve'])
53
+ # Temporarily relying on a button value until pluggable templates are
54
+ # introduced in 1.0.
55
+ authorize_request_token(request)
56
+ when r(:put, '/oauth/authorized_request_tokens/:id', ['submit' => 'Deny'])
57
+ # See previous comment.
58
+ deny_request_token(request)
59
+ when r(:post, '/oauth/authorized_request_tokens/:id', [{'_method' => 'PUT'}])
60
+ authorize_request_token(request)
61
+ when r(:post, '/oauth/access_tokens')
62
+ create_access_token(request)
63
+ when r(:get, '/oauth')
64
+ get_descriptor(request)
65
+ else
66
+ inject_user_or_challenge(request)
67
+ @app.call(env)
68
+ end
69
+ rescue OAuth::Signature::UnknownSignatureMethod
70
+ # The OAuth spec suggests a 400 status, but serving a 401 with the
71
+ # meta/challenge info seems more appropriate as the OAuth metadata
72
+ # specifies the supported signature methods, giving the user agent an
73
+ # opportunity to fix the error.
74
+ return challenge(request, 'unknown signature method')
75
+ end
76
+ end
77
+
78
+ def store; @@store; end
79
+
80
+ protected
81
+
82
+ def create_request_token(request)
83
+ return challenge(request, 'invalid nonce') unless valid_nonce?(request)
84
+
85
+ consumer_result = @@store.get(
86
+ "/cloudkit_oauth_consumers/#{request[:oauth_consumer_key]}")
87
+ unless consumer_result.status == 200
88
+ return challenge(request, 'invalid consumer')
89
+ end
90
+
91
+ consumer = consumer_result.parsed_content
92
+ signature = OAuth::Signature.build(request) { [nil, consumer['secret']] }
93
+ return challenge(request, 'invalid signature') unless signature.verify
94
+
95
+ token_id, secret = OAuth::Server.new(request.host).generate_credentials
96
+ request_token = JSON.generate(
97
+ :secret => secret,
98
+ :consumer_key => request[:oauth_consumer_key])
99
+ @@store.put(
100
+ "/cloudkit_oauth_request_tokens/#{token_id}",
101
+ :json => request_token)
102
+ [201, {}, ["oauth_token=#{token_id}&oauth_token_secret=#{secret}"]]
103
+ end
104
+
105
+ def request_authorization(request)
106
+ return login_redirect(request) unless request.current_user
107
+
108
+ request_token_result = @@store.get(
109
+ "/cloudkit_oauth_request_tokens/#{request[:oauth_token]}")
110
+ unless request_token_result.status == 200
111
+ return challenge(request, 'invalid request token')
112
+ end
113
+
114
+ request_token = request_token_result.parsed_content
115
+ erb(request, :request_authorization)
116
+ end
117
+
118
+ def authorize_request_token(request)
119
+ return login_redirect(request) unless request.current_user
120
+
121
+ request_token_response = @@store.get(
122
+ "/cloudkit_oauth_request_tokens/#{request.last_path_element}")
123
+ request_token = request_token_response.parsed_content
124
+ if request_token['authorized_at']
125
+ return challenge(request, 'invalid request token')
126
+ end
127
+
128
+ request_token['user_id'] = request.current_user
129
+ request_token['authorized_at'] = Time.now.httpdate
130
+ json = JSON.generate(request_token)
131
+ @@store.put(
132
+ "/cloudkit_oauth_request_tokens/#{request.last_path_element}",
133
+ :etag => request_token_response.etag,
134
+ :json => json)
135
+ erb(request, :authorize_request_token)
136
+ end
137
+
138
+ def deny_request_token(request)
139
+ return login_redirect(request) unless request.current_user
140
+
141
+ request_token_response = @@store.get(
142
+ "/cloudkit_oauth_request_tokens/#{request.last_path_element}")
143
+ @@store.delete(
144
+ "/cloudkit_oauth_request_tokens/#{request.last_path_element}",
145
+ :etag => request_token_response.etag)
146
+ erb(request, :request_token_denied)
147
+ end
148
+
149
+ def create_access_token(request)
150
+ return challenge(request, 'invalid nonce') unless valid_nonce?(request)
151
+
152
+ consumer_response = @@store.get(
153
+ "/cloudkit_oauth_consumers/#{request[:oauth_consumer_key]}")
154
+ unless consumer_response.status == 200
155
+ return challenge(request, 'invalid consumer')
156
+ end
157
+
158
+ consumer = consumer_response.parsed_content
159
+ request_token_response = @@store.get(
160
+ "/cloudkit_oauth_request_tokens/#{request[:oauth_token]}")
161
+ unless request_token_response.status == 200
162
+ return challenge(request, 'invalid request token')
163
+ end
164
+
165
+ request_token = request_token_response.parsed_content
166
+ unless request_token['consumer_key'] == request[:oauth_consumer_key]
167
+ return challenge(request, 'invalid consumer')
168
+ end
169
+
170
+ signature = OAuth::Signature.build(request) do
171
+ [request_token['secret'], consumer['secret']]
172
+ end
173
+ unless signature.verify
174
+ return challenge(request, 'invalid signature')
175
+ end
176
+
177
+ token_id, secret = OAuth::Server.new(request.host).generate_credentials
178
+ token_data = JSON.generate(
179
+ :secret => secret,
180
+ :consumer_key => request[:oauth_consumer_key],
181
+ :consumer_secret => consumer['secret'],
182
+ :user_id => request_token['user_id'])
183
+ @@store.put(
184
+ "/cloudkit_oauth_tokens/#{token_id}",
185
+ :json => token_data)
186
+ @@store.delete(
187
+ "/cloudkit_oauth_request_tokens/#{request[:oauth_token]}",
188
+ :etag => request_token_response.etag)
189
+ [201, {}, ["oauth_token=#{token_id}&oauth_token_secret=#{secret}"]]
190
+ end
191
+
192
+ def inject_user_or_challenge(request)
193
+ unless valid_nonce?(request)
194
+ request.current_user = ''
195
+ inject_challenge(request)
196
+ return
197
+ end
198
+
199
+ result = @@store.get("/cloudkit_oauth_tokens/#{request[:oauth_token]}")
200
+ access_token = result.parsed_content
201
+ signature = OAuth::Signature.build(request) do
202
+ [access_token['secret'], access_token['consumer_secret']]
203
+ end
204
+ if signature.verify
205
+ request.current_user = access_token['user_id']
206
+ else
207
+ request.current_user = ''
208
+ inject_challenge(request)
209
+ end
210
+ end
211
+
212
+ def valid_nonce?(request)
213
+ timestamp = request[:oauth_timestamp]
214
+ nonce = request[:oauth_nonce]
215
+ return false unless (timestamp && nonce)
216
+
217
+ uri = "/cloudkit_oauth_nonces/#{timestamp},#{nonce}"
218
+ result = @@store.put(uri, :json => '{}')
219
+ return false unless result.status == 201
220
+
221
+ true
222
+ end
223
+
224
+ def inject_challenge(request)
225
+ request.env[challenge_key] = challenge_headers(request)
226
+ end
227
+
228
+ def challenge(request, message)
229
+ [401, challenge_headers(request), [message || '']]
230
+ end
231
+
232
+ def challenge_headers(request)
233
+ {
234
+ 'WWW-Authenticate' => "OAuth realm=\"http://#{request.env['HTTP_HOST']}\"",
235
+ 'Link' => discovery_link(request)
236
+ }
237
+ end
238
+
239
+ def discovery_link(request)
240
+ "<#{request.scheme}://#{request.env['HTTP_HOST']}/oauth/meta>; rel=\"http://oauth.net/discovery/1.0/rel/provider\""
241
+ end
242
+
243
+ def login_redirect(request)
244
+ request.session['return_to'] = request.url if request.session
245
+ [302, {'Location' => request.login_url}, []]
246
+ end
247
+
248
+ def load_user_from_session(request)
249
+ request.current_user = request.session['user_uri'] if request.session
250
+ end
251
+
252
+ def get_meta(request)
253
+ # Expected in next OAuth Discovery Draft
254
+ erb(request, :oauth_meta)
255
+ end
256
+
257
+ def oauth_disco_draft2_xrds?(request)
258
+ # Current OAuth Discovery Draft 2 / XRDS-Simple 1.0, Section 5.1.2
259
+ request.get? &&
260
+ request.env['HTTP_ACCEPT'] &&
261
+ request.env['HTTP_ACCEPT'].match(/application\/xrds\+xml/)
262
+ end
263
+
264
+ def xrds_location(request)
265
+ # Current OAuth Discovery Draft 2 / XRDS-Simple 1.0, Section 5.1.2
266
+ [200, {'X-XRDS-Location' => "#{request.scheme}://#{request.env['HTTP_HOST']}/oauth"}, []]
267
+ end
268
+
269
+ def get_descriptor(request)
270
+ erb(request, :oauth_descriptor, {'Content-Type' => 'application/xrds+xml'})
271
+ end
272
+ end
273
+ end
@@ -0,0 +1,56 @@
1
+ module CloudKit
2
+
3
+ # An OAuthStore is a thin abstraction around CloudKit::Store, providing
4
+ # consistent collection names, and allowing automatic migrations in later
5
+ # releases if needed.
6
+ class OAuthStore
7
+ @@store = nil
8
+
9
+ # Initialize a Store for use with OAuth middleware. Load the static consumer
10
+ # resource if it does not exist.
11
+ def initialize(uri=nil)
12
+ @@store = Store.new(
13
+ :collections => [
14
+ :cloudkit_oauth_nonces,
15
+ :cloudkit_oauth_tokens,
16
+ :cloudkit_oauth_request_tokens,
17
+ :cloudkit_oauth_consumers],
18
+ :adapter => SQLAdapter.new(uri)) unless @@store
19
+ load_static_consumer
20
+ end
21
+
22
+ def get(uri, options={}) #:nodoc:
23
+ @@store.get(uri, options)
24
+ end
25
+
26
+ def post(uri, options={}) #:nodoc:
27
+ @@store.post(uri, options)
28
+ end
29
+
30
+ def put(uri, options={}) #:nodoc:
31
+ @@store.put(uri, options)
32
+ end
33
+
34
+ def delete(uri, options={}) #:nodoc:
35
+ @@store.delete(uri, options)
36
+ end
37
+
38
+ def resolve_uris(uris) #:nodoc:
39
+ @@store.resolve_uris(uris)
40
+ end
41
+
42
+ def reset! #:nodoc:
43
+ @@store.reset!
44
+ end
45
+
46
+ # Return the version number for this store.
47
+ def version; 1; end
48
+
49
+ # Load the static consumer entity if it does not already exist.
50
+ # See the OAuth Discovery spec for more info on static consumers.
51
+ def load_static_consumer
52
+ json = JSON.generate(:secret => '')
53
+ @@store.put('/cloudkit_oauth_consumers/cloudkitconsumer', :json => json)
54
+ end
55
+ end
56
+ end