cloudkit-jruby 0.11.2

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 (64) hide show
  1. data/CHANGES +47 -0
  2. data/COPYING +20 -0
  3. data/README +84 -0
  4. data/Rakefile +42 -0
  5. data/TODO +21 -0
  6. data/cloudkit.gemspec +89 -0
  7. data/doc/curl.html +388 -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 +90 -0
  13. data/doc/main.css +151 -0
  14. data/doc/rest-api.html +467 -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 +9 -0
  20. data/examples/6.ru +11 -0
  21. data/examples/TOC +17 -0
  22. data/lib/cloudkit.rb +92 -0
  23. data/lib/cloudkit/constants.rb +34 -0
  24. data/lib/cloudkit/exceptions.rb +10 -0
  25. data/lib/cloudkit/flash_session.rb +20 -0
  26. data/lib/cloudkit/oauth_filter.rb +266 -0
  27. data/lib/cloudkit/oauth_store.rb +48 -0
  28. data/lib/cloudkit/openid_filter.rb +236 -0
  29. data/lib/cloudkit/openid_store.rb +100 -0
  30. data/lib/cloudkit/rack/builder.rb +120 -0
  31. data/lib/cloudkit/rack/router.rb +20 -0
  32. data/lib/cloudkit/request.rb +177 -0
  33. data/lib/cloudkit/service.rb +162 -0
  34. data/lib/cloudkit/store.rb +349 -0
  35. data/lib/cloudkit/store/memory_table.rb +99 -0
  36. data/lib/cloudkit/store/resource.rb +269 -0
  37. data/lib/cloudkit/store/response.rb +52 -0
  38. data/lib/cloudkit/store/response_helpers.rb +84 -0
  39. data/lib/cloudkit/templates/authorize_request_token.erb +19 -0
  40. data/lib/cloudkit/templates/oauth_descriptor.erb +43 -0
  41. data/lib/cloudkit/templates/oauth_meta.erb +8 -0
  42. data/lib/cloudkit/templates/openid_login.erb +31 -0
  43. data/lib/cloudkit/templates/request_authorization.erb +23 -0
  44. data/lib/cloudkit/templates/request_token_denied.erb +18 -0
  45. data/lib/cloudkit/uri.rb +88 -0
  46. data/lib/cloudkit/user_store.rb +37 -0
  47. data/lib/cloudkit/util.rb +25 -0
  48. data/spec/ext_spec.rb +76 -0
  49. data/spec/flash_session_spec.rb +20 -0
  50. data/spec/memory_table_spec.rb +86 -0
  51. data/spec/oauth_filter_spec.rb +326 -0
  52. data/spec/oauth_store_spec.rb +10 -0
  53. data/spec/openid_filter_spec.rb +81 -0
  54. data/spec/openid_store_spec.rb +101 -0
  55. data/spec/rack_builder_spec.rb +39 -0
  56. data/spec/request_spec.rb +191 -0
  57. data/spec/resource_spec.rb +310 -0
  58. data/spec/service_spec.rb +1039 -0
  59. data/spec/spec_helper.rb +32 -0
  60. data/spec/store_spec.rb +10 -0
  61. data/spec/uri_spec.rb +93 -0
  62. data/spec/user_store_spec.rb +10 -0
  63. data/spec/util_spec.rb +11 -0
  64. metadata +180 -0
@@ -0,0 +1,48 @@
1
+ module CloudKit
2
+
3
+ # An OAuthStore is a thin abstraction around CloudKit::Store, providing
4
+ # consistent collection names, and allowing automatic upgrades 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
12
+ @@store = Store.new(
13
+ :collections => [
14
+ :cloudkit_oauth_nonces,
15
+ :cloudkit_oauth_tokens,
16
+ :cloudkit_oauth_request_tokens,
17
+ :cloudkit_oauth_consumers]
18
+ ) unless @@store
19
+ load_static_consumer
20
+ end
21
+
22
+ def get(uri, options={}) #:nodoc:
23
+ @@store.get(CloudKit::URI.new(uri), options)
24
+ end
25
+
26
+ def post(uri, options={}) #:nodoc:
27
+ @@store.post(CloudKit::URI.new(uri), options)
28
+ end
29
+
30
+ def put(uri, options={}) #:nodoc:
31
+ @@store.put(CloudKit::URI.new(uri), options)
32
+ end
33
+
34
+ def delete(uri, options={}) #:nodoc:
35
+ @@store.delete(CloudKit::URI.new(uri), options)
36
+ end
37
+
38
+ # Return the version number for this store.
39
+ def version; 1; end
40
+
41
+ # Load the static consumer entity if it does not already exist.
42
+ # See the OAuth Discovery spec for more info on static consumers.
43
+ def load_static_consumer
44
+ json = JSON.generate(:secret => '')
45
+ @@store.put(CloudKit::URI.new('/cloudkit_oauth_consumers/cloudkitconsumer'), :json => json)
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,236 @@
1
+ module CloudKit
2
+
3
+ # An OpenIDFilter provides OpenID authentication, listening for upstream
4
+ # OAuth authentication and bypassing if already authorized.
5
+ #
6
+ # The root URI, "/", is always bypassed. More URIs can also be bypassed using
7
+ # the :allow option:
8
+ #
9
+ # use Rack::Session::Pool
10
+ # use OpenIDFilter, :allow => ['/foo', '/bar']
11
+ #
12
+ # In addition to the :allow option, a block can also be used for more complex
13
+ # decisions:
14
+ #
15
+ # use Rack::Session::Pool
16
+ # use OpenIDFilter, :allow => ['/foo'] do |url|
17
+ # bar(url) # some method returning true or false
18
+ # end
19
+ #
20
+ # Responds to the following URIs:
21
+ # /login
22
+ # /logout
23
+ # /openid_complete
24
+ #
25
+ class OpenIDFilter
26
+ include Util
27
+
28
+ @@lock = Mutex.new
29
+ @@store = nil
30
+
31
+ def initialize(app, options={}, &bypass_route_callback)
32
+ @app = app
33
+ @options = options
34
+ @bypass_route_callback = bypass_route_callback || Proc.new {|url| url == '/'}
35
+ end
36
+
37
+ def call(env)
38
+ @@lock.synchronize do
39
+ @@store = OpenIDStore.new
40
+ @users = UserStore.new
41
+ end unless @@store
42
+
43
+ request = Request.new(env)
44
+ request.announce_auth(CLOUDKIT_OPENID_FILTER_KEY)
45
+
46
+ case request
47
+ when r(:get, request.login_url); request_login(request)
48
+ when r(:post, request.login_url); begin_openid_login(request)
49
+ when r(:get, '/openid_complete'); complete_openid_login(request)
50
+ when r(:post, request.logout_url); logout(request)
51
+ else
52
+ if bypass?(request)
53
+ @app.call(env)
54
+ else
55
+ if request.env[CLOUDKIT_AUTH_CHALLENGE]
56
+ store_location(request)
57
+ erb(
58
+ request,
59
+ :openid_login,
60
+ request.env[CLOUDKIT_AUTH_CHALLENGE].merge('Content-Type' => 'text/html'),
61
+ 401)
62
+ elsif !request.via.include?(CLOUDKIT_OAUTH_FILTER_KEY)
63
+ store_location(request)
64
+ login_redirect(request)
65
+ else
66
+ Rack::Response.new('server misconfigured', 500).finish
67
+ end
68
+ end
69
+ end
70
+ end
71
+
72
+ def logout(request)
73
+ user_uri = request.session.delete('user_uri')
74
+ result = @users.get(user_uri)
75
+ user = result.parsed_content
76
+ user.delete('remember_me_token')
77
+ user.delete('remember_me_expiration')
78
+ json = JSON.generate(user)
79
+ @users.put(user_uri, :etag => result.etag, :json => json)
80
+
81
+ request.env[CLOUDKIT_AUTH_KEY] = nil
82
+ request.flash['info'] = 'You have been logged out.'
83
+ response = Rack::Response.new(
84
+ [],
85
+ 302,
86
+ {'Location' => request.login_url, 'Content-Type' => 'text/html'})
87
+ response.delete_cookie('remember_me')
88
+ response.finish
89
+ end
90
+
91
+ def request_login(request)
92
+ erb(request, :openid_login)
93
+ end
94
+
95
+ def begin_openid_login(request)
96
+ begin
97
+ response = openid_consumer(request).begin(request[:openid_url])
98
+ rescue => e
99
+ request.flash[:error] = e
100
+ return login_redirect(request)
101
+ end
102
+
103
+ redirect_url = response.redirect_url(base_url(request), full_url(request))
104
+ Rack::Response.new([], 302, {'Location' => redirect_url}).finish
105
+ end
106
+
107
+ def complete_openid_login(request)
108
+ begin
109
+ idp_response = openid_consumer(request).complete(request.params, full_url(request))
110
+ rescue => e
111
+ request.flash[:error] = e
112
+ return login_redirect(request)
113
+ end
114
+
115
+ if idp_response.is_a?(OpenID::Consumer::FailureResponse)
116
+ request.flash[:error] = idp_response.message
117
+ return login_redirect(request)
118
+ end
119
+
120
+ result = @users.get(
121
+ '/cloudkit_users',
122
+ # '/cloudkit_login_view',
123
+ :identity_url => idp_response.endpoint.claimed_id)
124
+ user_uris = result.parsed_content['uris']
125
+
126
+ if user_uris.empty?
127
+ json = JSON.generate(:identity_url => idp_response.endpoint.claimed_id)
128
+ result = @users.post('/cloudkit_users', :json => json)
129
+ user_uri = result.parsed_content['uri']
130
+ else
131
+ user_uri = user_uris.first
132
+ end
133
+ user_result = @users.resolve_uris([user_uri]).first
134
+ user = user_result.parsed_content
135
+
136
+ if request.session['user_uri'] = user_uri
137
+ request.current_user = user_uri
138
+ user['remember_me_expiration'] = two_weeks_from_now
139
+ user['remember_me_token'] = Base64.encode64(
140
+ OpenSSL::Random.random_bytes(32)).gsub(/\W/,'')
141
+ url = request.session.delete('return_to')
142
+ response = Rack::Response.new(
143
+ [],
144
+ 302,
145
+ {'Location' => (url || '/'), 'Content-Type' => 'text/html'})
146
+ response.set_cookie(
147
+ 'remember_me', {
148
+ :value => user['remember_me_token'],
149
+ :expires => Time.at(user['remember_me_expiration']).utc})
150
+ json = JSON.generate(user)
151
+ @users.put(user_uri, :etag => user_result.etag, :json => json)
152
+ request.flash[:notice] = 'You have been logged in.'
153
+ response.finish
154
+ else
155
+ request.flash[:error] = 'Could not log on with your OpenID.'
156
+ login_redirect(request)
157
+ end
158
+ end
159
+
160
+ def login_redirect(request)
161
+ Rack::Response.new([], 302, {'Location' => request.login_url}).finish
162
+ end
163
+
164
+ def base_url(request)
165
+ "#{request.scheme}://#{request.env['HTTP_HOST']}/"
166
+ end
167
+
168
+ def full_url(request)
169
+ base_url(request) + 'openid_complete'
170
+ end
171
+
172
+ def logged_in?(request)
173
+ logged_in = user_in_session?(request) || valid_remember_me_token?(request)
174
+ request.current_user = request.session['user_uri'] if logged_in
175
+ logged_in
176
+ end
177
+
178
+ def user_in_session?(request)
179
+ request.session['user_uri'] != nil
180
+ end
181
+
182
+ def store_location(request)
183
+ request.session['return_to'] = request.url
184
+ end
185
+
186
+ def root_request?(request)
187
+ request.path_info == '/' || request.path_info == '/favicon.ico'
188
+ end
189
+
190
+ def valid_auth_key?(request)
191
+ request.env[CLOUDKIT_AUTH_KEY] && request.env[CLOUDKIT_AUTH_KEY] != ''
192
+ end
193
+
194
+ def openid_consumer(request)
195
+ @openid_consumer ||= OpenID::Consumer.new(
196
+ request.session, OpenIDStore.new)
197
+ end
198
+
199
+ def valid_remember_me_token?(request)
200
+ return false unless token = request.cookies['remember_me']
201
+
202
+ # result = @users.get('/cloudkit_login_view', :remember_me_token => token)
203
+ result = @users.get('/cloudkit_users', :remember_me_token => token)
204
+ return false unless result.status == 200
205
+
206
+ user_uris = result.parsed_content['uris']
207
+ return false unless user_uris.try(:size) == 1
208
+
209
+ user_uri = user_uris.first
210
+ user_result = @users.resolve_uris([user_uri]).first
211
+ user = user_result.parsed_content
212
+ return false unless Time.now.to_i < user['remember_me_expiration']
213
+
214
+ user['remember_me_expiration'] = two_weeks_from_now
215
+ json = JSON.generate(user)
216
+ @users.put(user_uri, :etag => user_result.etag, :json => json)
217
+ request.session['user_uri'] = user_uri
218
+ true
219
+ end
220
+
221
+ def two_weeks_from_now
222
+ Time.now.to_i+1209600
223
+ end
224
+
225
+ def allow?(uri)
226
+ @bypass_route_callback.call(uri) ||
227
+ @options[:allow] && @options[:allow].include?(uri)
228
+ end
229
+
230
+ def bypass?(request)
231
+ allow?(request.path_info) ||
232
+ valid_auth_key?(request) ||
233
+ logged_in?(request)
234
+ end
235
+ end
236
+ end
@@ -0,0 +1,100 @@
1
+ require 'openid/store/interface'
2
+ module CloudKit
3
+
4
+ # An OpenIDStore provides the interface expected by the ruby-openid gem,
5
+ # mapping it to a CloudKit::Store instance.
6
+ class OpenIDStore < OpenID::Store::Interface
7
+ @@store = nil
8
+
9
+ # Initialize an OpenIDStore.
10
+ def initialize
11
+ unless @@store
12
+ @@store = Store.new(
13
+ :collections => [:cloudkit_openid_associations, :cloudkit_openid_nonces])
14
+ end
15
+ end
16
+
17
+ def get_association(server_url, handle=nil) #:nodoc:
18
+ options = {:server_url => server_url}
19
+ options.merge!(:handle => Base64.encode64(handle)) if (handle && handle != '')
20
+ result = @@store.get(CloudKit::URI.new('/cloudkit_openid_associations'), options)
21
+ return nil unless result.status == 200
22
+ return nil if result.parsed_content['total'] == 0
23
+
24
+ ignore, associations = resolve_associations(result.parsed_content)
25
+ return nil if associations.empty?
26
+
27
+ associations.sort_by{|a| a['issued']}
28
+ a = associations[-1]
29
+ OpenID::Association.new(
30
+ Base64.decode64(a['handle']),
31
+ Base64.decode64(a['secret']),
32
+ Time.at(a['issued']),
33
+ a['lifetime'],
34
+ a['assoc_type'])
35
+ end
36
+
37
+ def remove_association(server_url, handle) #:nodoc:
38
+ result = @@store.get(
39
+ CloudKit::URI.new('/cloudkit_openid_associations'),
40
+ :server_url => server_url,
41
+ :handle => Base64.encode64(handle))
42
+ return nil unless result.status == 200
43
+
44
+ responses, associations = resolve_associations(result.parsed_content)
45
+ return nil if associations.empty?
46
+
47
+ uris = result.parsed_content['uris']
48
+ responses.each_with_index do |r, index|
49
+ @@store.delete(CloudKit::URI.new(uris[index]), :etag => r.etag)
50
+ end
51
+ end
52
+
53
+ def store_association(server_url, association) #:nodoc:
54
+ remove_association(server_url, association.handle)
55
+ json = JSON.generate(
56
+ :server_url => server_url,
57
+ :handle => Base64.encode64(association.handle),
58
+ :secret => Base64.encode64(association.secret),
59
+ :issued => association.issued.to_i,
60
+ :lifetime => association.lifetime,
61
+ :assoc_type => association.assoc_type)
62
+ result = @@store.post(CloudKit::URI.new('/cloudkit_openid_associations'), :json => json)
63
+ return (result.status == 201)
64
+ end
65
+
66
+ def use_nonce(server_url, timestamp, salt) #:nodoc:
67
+ return false if (timestamp - Time.now.to_i).abs > OpenID::Nonce.skew
68
+
69
+ fragment = ::URI.escape(
70
+ [server_url, timestamp, salt].join('-'),
71
+ Regexp.union(::URI::REGEXP::UNSAFE, '/', ':'))
72
+ uri = "/cloudkit_openid_nonces/#{fragment}"
73
+ result = @@store.put(CloudKit::URI.new(uri), :json => '{}')
74
+ return (result.status == 201)
75
+ end
76
+
77
+ def self.cleanup #:nodoc:
78
+ # TODO
79
+ end
80
+
81
+ def self.cleanup_associations #:nodoc:
82
+ # TODO
83
+ end
84
+
85
+ def self.cleanup_nonces #:nodoc:
86
+ # TODO
87
+ end
88
+
89
+ # Return the version number for this store.
90
+ def version; 1; end
91
+
92
+ protected
93
+
94
+ def resolve_associations(parsed_content) #:nodoc:
95
+ uri_list = parsed_content['uris'].map! { |u| CloudKit::URI.new(u) }
96
+ association_responses = @@store.resolve_uris(uri_list)
97
+ return association_responses, association_responses.map{|a| a.parsed_content}
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,120 @@
1
+ module Rack #:nodoc:
2
+ class Builder
3
+ alias_method :cloudkit_to_app, :to_app
4
+
5
+ # Extends Rack::Builder's to_app method to detect if the last piece of
6
+ # middleware in the stack is a CloudKit shortcut (contain or expose), adding
7
+ # a default developer page at the root and a 404 everywhere else.
8
+ def to_app
9
+ default_app = lambda do |env|
10
+ if (env['PATH_INFO'] == '/')
11
+ Rack::Response.new(welcome).finish
12
+ else
13
+ Rack::Response.new('not found', 404).finish
14
+ end
15
+ end
16
+ @ins << default_app if @last_cloudkit_id == @ins.last.object_id
17
+ cloudkit_to_app
18
+ end
19
+
20
+ # Setup resource collections hosted behind OAuth and OpenID auth filters.
21
+ #
22
+ # ===Example
23
+ # contain :notes, :projects
24
+ #
25
+ def contain(*args)
26
+ @ins << lambda do |app|
27
+ Rack::Session::Pool.new(
28
+ CloudKit::OAuthFilter.new(
29
+ CloudKit::OpenIDFilter.new(
30
+ CloudKit::Service.new(app, :collections => args.to_a))))
31
+ end
32
+ @last_cloudkit_id = @ins.last.object_id
33
+ end
34
+
35
+ # Setup resource collections without authentication.
36
+ #
37
+ # ===Example
38
+ # expose :notes, :projects
39
+ #
40
+ def expose(*args)
41
+ @ins << lambda do |app|
42
+ CloudKit::Service.new(app, :collections => args.to_a)
43
+ end
44
+ @last_cloudkit_id = @ins.last.object_id
45
+ end
46
+
47
+ def welcome #:nodoc:
48
+ doc = <<HTML
49
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
50
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
51
+ <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
52
+ <head>
53
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
54
+ <title>CloudKit</title>
55
+ <style type="text/css">
56
+ body {
57
+ font-family: 'Helvetica', 'Arial', san-serif;
58
+ font-size: 15px;
59
+ margin: 0;
60
+ padding: 0;
61
+ color: #222222;
62
+ }
63
+ h1 {
64
+ font-family: 'Helvetica Neue', 'Helvetica', 'Arial', san-serif;
65
+ font-size: 73px;
66
+ font-weight: bold;
67
+ line-height: 28px;
68
+ margin: 20px 0px 20px 0px;
69
+ }
70
+ .wrapper {
71
+ width: 500px;
72
+ margin: 0 auto;
73
+ clear: both;
74
+ }
75
+ p {
76
+ margin-top: 0px;
77
+ line-height: 1.5em;
78
+ }
79
+ #header {
80
+ background-color: #ffffcc;
81
+ display: block;
82
+ padding: 2px 0;
83
+ margin: 35px 0px 10px 0px;
84
+ border-top: 1px solid #ffcc66;
85
+ border-bottom: 1px solid #ffcc66;
86
+ }
87
+ a {
88
+ color: #6b8df2;
89
+ text-decoration: none;
90
+ }
91
+ .meta {
92
+ padding: 7px 7px 7px 7px;
93
+ background-color: #ffccff;
94
+ border-top: 1px solid #cc99ff;
95
+ border-bottom: 1px solid #cc99ff;
96
+ font-size: 14px;
97
+ display: block;
98
+ margin: 10px 0px 10px 0px;
99
+ }
100
+ </style>
101
+ </head>
102
+ <body>
103
+ <div id="header">
104
+ <div class="wrapper">
105
+ <h1>CloudKit</h1>
106
+ </div>
107
+ </div>
108
+ <div class="meta">
109
+ <p class="wrapper">
110
+ This page is appearing because you have not set up a default app in your
111
+ rackup file. To learn more about CloudKit, check out
112
+ <a href="http://getcloudkit.com">the site</a>.
113
+ </p>
114
+ </div>
115
+ </body>
116
+ </html>
117
+ HTML
118
+ end
119
+ end
120
+ end