cloudkit 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,9 @@
1
+ module CloudKit
2
+
3
+ # A common interface for pluggable storage adapters
4
+ class Adapter
5
+ include Util
6
+
7
+ # TODO extract from SQLAdapter
8
+ end
9
+ end
@@ -0,0 +1,57 @@
1
+ module CloudKit
2
+
3
+ # An ExtractionView observes a resource collection and extracts specified
4
+ # elements for querying.
5
+ class ExtractionView
6
+ attr_accessor :name, :observe, :extract
7
+
8
+ # Initialize an ExtractionView.
9
+ #
10
+ # ==Examples
11
+ #
12
+ # Observe a "notes" collection and extract the titles and colors.
13
+ # view = ExtractionView.new(
14
+ # :name => :note_features,
15
+ # :observe => :notes,
16
+ # :extract => [:title, :color])
17
+ #
18
+ def initialize(name, options)
19
+ @name = name
20
+ @observe = options[:observe]
21
+ @extract = options[:extract]
22
+ end
23
+
24
+ # Map the provided data into a collection for querying.
25
+ def map(db, collection, uri, data)
26
+ if @observe == collection
27
+ elements = @extract.inject({}) do |e, field|
28
+ e.merge(field.to_s => data[field.to_s])
29
+ end
30
+ elements.merge!('uri' => uri)
31
+ db.transaction do
32
+ db[@name].filter(:uri => uri).delete
33
+ db[@name].insert(elements)
34
+ end
35
+ end
36
+ end
37
+
38
+ # Remove the data with the specified URI from the view
39
+ def unmap(db, type, uri)
40
+ if @observe == type
41
+ db[@name].filter(:uri => uri).delete
42
+ end
43
+ end
44
+
45
+ # Initialize the storage for this view
46
+ def initialize_storage(db)
47
+ extractions = @extract
48
+ db.create_table @name do
49
+ extractions.each do |field|
50
+ text field
51
+ end
52
+ primary_key :id
53
+ varchar :uri
54
+ end unless db.table_exists?(@name)
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,51 @@
1
+ module CloudKit
2
+
3
+ # A response wrapper for CloudKit::Store
4
+ class Response
5
+ include Util
6
+
7
+ attr_reader :status, :meta, :content
8
+
9
+ # Create an instance of a Response.
10
+ def initialize(status, meta, content='')
11
+ @status = status; @meta = meta; @content = content
12
+ end
13
+
14
+ # Return the header value specified by key.
15
+ def [](key)
16
+ meta[key]
17
+ end
18
+
19
+ # Set the header specified by key to value.
20
+ def []=(key, value)
21
+ meta[key] = value
22
+ end
23
+
24
+ # Translate to the standard Rack representation: [status, headers, content]
25
+ def to_rack
26
+ [status, meta, [content]]
27
+ end
28
+
29
+ # Parse and return the JSON content
30
+ def parsed_content
31
+ JSON.parse(content)
32
+ end
33
+
34
+ # Clear only the content of the response. Useful for HEAD requests.
35
+ def clear_content
36
+ @content = ''
37
+ end
38
+
39
+ # Return a response suitable for HEAD requests.
40
+ def head
41
+ response = self.dup
42
+ response.clear_content
43
+ response
44
+ end
45
+
46
+ # Return the ETag for this response without the surrounding quotes.
47
+ def etag
48
+ unquote(meta['ETag'])
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,72 @@
1
+ # A set of mixins for building CloudKit::Response objects.
2
+ module CloudKit::ResponseHelpers
3
+ def status_404
4
+ json_error_response(404, 'not found')
5
+ end
6
+
7
+ def status_405(methods)
8
+ response = json_error_response(405, 'method not allowed')
9
+ response['Allow'] = methods.join(', ')
10
+ response
11
+ end
12
+
13
+ def status_410
14
+ json_error_response(410, 'entity previously deleted')
15
+ end
16
+
17
+ def status_412
18
+ json_error_response(412, 'precondition failed')
19
+ end
20
+
21
+ def status_422
22
+ json_error_response(422, 'unprocessable entity')
23
+ end
24
+
25
+ def internal_server_error
26
+ json_error_response(500, 'unknown server error')
27
+ end
28
+
29
+ def data_required
30
+ json_error_response(400, 'data required')
31
+ end
32
+
33
+ def invalid_entity_type
34
+ json_error_response(400, 'valid entity type required')
35
+ end
36
+
37
+ def etag_required
38
+ json_error_response(400, 'etag required')
39
+ end
40
+
41
+ def allow(methods)
42
+ CloudKit::Response.new(200, {'Allow' => methods.join(', ')})
43
+ end
44
+
45
+ def response(status, content='', etag=nil, last_modified=nil, options={})
46
+ cache_control = options[:cache] == false ? 'no-cache' : 'proxy-revalidate'
47
+ headers = {
48
+ 'Content-Type' => 'application/json',
49
+ 'Cache-Control' => cache_control}
50
+ headers.merge!('ETag' => "\"#{etag}\"") if etag
51
+ headers.merge!('Last-Modified' => last_modified) if last_modified
52
+ CloudKit::Response.new(status, headers, content)
53
+ end
54
+
55
+ def json_meta_response(status, uri, etag, last_modified)
56
+ json = JSON.generate(
57
+ :ok => true,
58
+ :uri => uri,
59
+ :etag => etag,
60
+ :last_modified => last_modified)
61
+ response(status, json, nil, nil, :cache => false)
62
+ end
63
+
64
+ def json_error(message)
65
+ "{\"error\":\"#{message}\"}"
66
+ end
67
+
68
+ def json_error_response(status, message)
69
+ "trying to throw a json error message for #{status} #{message}"
70
+ response(status, json_error(message), nil, nil, :cache => false)
71
+ end
72
+ end
@@ -0,0 +1,36 @@
1
+ module CloudKit
2
+
3
+ # Adapts a CloudKit::Store to a SQL backend.
4
+ class SQLAdapter < Adapter
5
+
6
+ # Initialize a new SQL backend. Defaults to in-memory SQLite.
7
+ def initialize(uri=nil, options={})
8
+ @db = uri ? Sequel.connect(uri, options) : Sequel.sqlite
9
+ # TODO accept views as part of a store, then initialize them here
10
+ initialize_storage
11
+ end
12
+
13
+ # method_missing is a placeholder for future interface extraction into
14
+ # CloudKit::Adapter.
15
+ def method_missing(method, *args, &block)
16
+ @db.send(method, *args, &block)
17
+ end
18
+
19
+ protected
20
+
21
+ # Initialize the HTTP-oriented storage if it does not exist.
22
+ def initialize_storage
23
+ @db.create_table store_key do
24
+ primary_key :id
25
+ varchar :uri, :unique => true
26
+ varchar :etag
27
+ varchar :collection_reference
28
+ varchar :resource_reference
29
+ varchar :last_modified
30
+ varchar :remote_user
31
+ text :content
32
+ boolean :deleted, :default => false
33
+ end unless @db.table_exists?(store_key)
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,19 @@
1
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
2
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
3
+ <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
4
+ <head>
5
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
6
+ <title>OAuth Request Token Authorized</title>
7
+ <style type="text/css">
8
+ body {
9
+ font-family: 'Helvetica', 'Arial', san-serif;
10
+ }
11
+ </style>
12
+ </head>
13
+ <body>
14
+ <p>
15
+ Your OAuth Request Token has been authorized. Please return to the application
16
+ that initiated this request to complete the setup.
17
+ </p>
18
+ </body>
19
+ </html>
@@ -0,0 +1,43 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <XRDS xmlns="xri://$xrds">
3
+ <XRD xml:id="oauth" xmlns:simple="http://xrds-simple.net/core/1.0" xmlns="xri://$XRD*($v*2.0)" version="2.0">
4
+ <Type>xri://$xrds*simple</Type>
5
+ <Expires><%= Time.at(Time.now.to_i + 2592000).utc.xmlschema %></Expires>
6
+ <Service priority="10">
7
+ <Type>http://oauth.net/core/1.0/endpoint/request</Type>
8
+ <Type>http://oauth.net/core/1.0/parameters/auth-header</Type>
9
+ <Type>http://oauth.net/core/1.0/parameters/uri-query</Type>
10
+ <Type>http://oauth.net/core/1.0/signature/PLAINTEXT</Type>
11
+ <URI><%= request.scheme %>://<%= request.env['HTTP_HOST'] %>/oauth/request_tokens</URI>
12
+ </Service>
13
+ <Service priority="10">
14
+ <Type>http://oauth.net/core/1.0/endpoint/authorize</Type>
15
+ <Type>http://oauth.net/core/1.0/parameters/uri-query</Type>
16
+ <URI><%= request.scheme %>://<%= request.env['HTTP_HOST'] %>/oauth/authorization</URI>
17
+ </Service>
18
+ <Service priority="10">
19
+ <Type>http://oauth.net/core/1.0/endpoint/access</Type>
20
+ <Type>http://oauth.net/core/1.0/parameters/auth-header</Type>
21
+ <Type>http://oauth.net/core/1.0/parameters/uri-query</Type>
22
+ <Type>http://oauth.net/core/1.0/signature/PLAINTEXT</Type>
23
+ <URI><%= request.scheme %>://<%= request.env['HTTP_HOST'] %>/oauth/access_tokens</URI>
24
+ </Service>
25
+ <Service priority="10">
26
+ <Type>http://oauth.net/core/1.0/endpoint/resource</Type>
27
+ <Type>http://oauth.net/core/1.0/parameters/auth-header</Type>
28
+ <Type>http://oauth.net/core/1.0/parameters/uri-query</Type>
29
+ <Type>http://oauth.net/core/1.0/signature/HMAC-SHA1</Type>
30
+ </Service>
31
+ <Service priority="10">
32
+ <Type>http://oauth.net/discovery/1.0/consumer-identity/static</Type>
33
+ <LocalID>cloudkitconsumer</LocalID>
34
+ </Service>
35
+ </XRD>
36
+ <XRD xmlns="xri://$XRD*($v*2.0)" version="2.0">
37
+ <Type>xri://$xrds*simple</Type>
38
+ <Service priority="10">
39
+ <Type>http://oauth.net/discovery/1.0</Type>
40
+ <URI>#oauth</URI>
41
+ </Service>
42
+ </XRD>
43
+ </XRDS>
@@ -0,0 +1,8 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <XRD>
3
+ <Type>http://oauth.net/discovery/1.0</Type>
4
+ <Service>
5
+ <Type>http://oauth.net/discovery/1.0/rel/provider</Type>
6
+ <URI><%= request.scheme %>://<%= request.env['HTTP_HOST'] %>/oauth</URI>
7
+ </Service>
8
+ </XRD>
@@ -0,0 +1,31 @@
1
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
2
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
3
+ <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
4
+ <head>
5
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
6
+ <title>Sign Up or Sign In</title>
7
+ <style type="text/css">
8
+ body {
9
+ font-family: 'Helvetica', 'Arial', san-serif;
10
+ }
11
+ #openid_url {
12
+ background: url(data:image/gif;base64,R0lGODlhEAAQANUAAPr6+t/f37Gxsby8vJycnKWlpaampq+vr/zUpfz8/Lm5ufLy8r6+vp2dncvLy/muVsjIyLW1tcTExKGhof7+/sDAwMHBwbS0tNnZ2erq6qioqPn5+bi4uLu7u+np6e3t7evr66mpqeXl5b29vaqqqtfX18zMzO7u7u/v756enveTHv///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAAAAAAALAAAAAAQABAAAAZlwJVwKEQQj0eECslcqZbN4zMqhTY/EoFzKbCAVoDNKjFIERTbFaeRcpwWCU3qgBJOVxVDhOEpH4hTFBkBIyQiKSkBgFZEiBiLTBApBgBDd0gXKROKaU0mZiEFl0wAJR0FD1SqQ0EAOw%3D%3D) no-repeat #FFF 5px;
13
+ padding-left: 25px;
14
+ width: 300px;
15
+ }
16
+ #submit {
17
+ display: block;
18
+ margin-top: 10px;
19
+ }
20
+ </style>
21
+ </head>
22
+ <body>
23
+ <%= request.flash[:error] %>
24
+ <%= request.flash[:info] %>
25
+ <p>Sign Up or Sign In with OpenID</p>
26
+ <form action="/login" method="POST">
27
+ <input type="text" id="openid_url" name="openid_url"/>
28
+ <input id="submit" type="submit" value="Sign In"/>
29
+ </form>
30
+ </body>
31
+ </html>
@@ -0,0 +1,23 @@
1
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
2
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
3
+ <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
4
+ <head>
5
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
6
+ <title>OAuth Authorization Requested</title>
7
+ <style type="text/css">
8
+ body {
9
+ font-family: 'Helvetica', 'Arial', san-serif;
10
+ }
11
+ </style>
12
+ </head>
13
+ <body>
14
+ <p>
15
+ Another application or website has requested access to your data.
16
+ </p>
17
+ <form action="/oauth/authorized_request_tokens/<%= request['oauth_token'] %>" method="POST">
18
+ <input type="hidden" name="_method" value="PUT"/>
19
+ <input type="submit" value="Approve"/>
20
+ <input type="submit" value="Deny"/>
21
+ </form>
22
+ </body>
23
+ </html>
@@ -0,0 +1,18 @@
1
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
2
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
3
+ <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
4
+ <head>
5
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
6
+ <title>OAuth Request Token Authorization Denied</title>
7
+ <style type="text/css">
8
+ body {
9
+ font-family: 'Helvetica', 'Arial', san-serif;
10
+ }
11
+ </style>
12
+ </head>
13
+ <body>
14
+ <p>
15
+ Your OAuth Request Token was not denied and has been removed.
16
+ </p>
17
+ </body>
18
+ </html>
@@ -0,0 +1,44 @@
1
+ module CloudKit
2
+
3
+ # A thin layer on top of CloudKit::Store providing consistent URIs and
4
+ # automatic schema upgrades if required for future releases.
5
+ class UserStore
6
+ @@store = nil
7
+
8
+ def initialize(uri=nil)
9
+ unless @@store
10
+ login_view = ExtractionView.new(
11
+ :cloudkit_login_view,
12
+ :observe => :cloudkit_users,
13
+ :extract => [:identity_url, :remember_me_token, :remember_me_expiration])
14
+ @@store = Store.new(
15
+ :collections => [:cloudkit_users],
16
+ :views => [login_view],
17
+ :adapter => SQLAdapter.new(uri))
18
+ end
19
+ end
20
+
21
+ def get(uri, options={}) #:nodoc:
22
+ @@store.get(uri, options)
23
+ end
24
+
25
+ def post(uri, options={}) #:nodoc:
26
+ @@store.post(uri, options)
27
+ end
28
+
29
+ def put(uri, options={}) #:nodoc:
30
+ @@store.put(uri, options)
31
+ end
32
+
33
+ def delete(uri, options={}) #:nodoc:
34
+ @@store.delete(uri, options)
35
+ end
36
+
37
+ def resolve_uris(uris) #:nodoc:
38
+ @@store.resolve_uris(uris)
39
+ end
40
+
41
+ # Return the version for this UserStore
42
+ def version; 1; end
43
+ end
44
+ end
@@ -0,0 +1,60 @@
1
+ module CloudKit
2
+ module Util
3
+
4
+ # Render ERB content
5
+ def erb(request, template, headers={'Content-Type' => 'text/html'}, status=200)
6
+ template_file = open(
7
+ File.join(File.dirname(__FILE__),
8
+ 'templates',
9
+ "#{template.to_s}.erb"))
10
+ template = ERB.new(template_file.read)
11
+ [status, headers, [template.result(binding)]]
12
+ end
13
+
14
+ # Build a Rack::Router instance
15
+ def r(method, path, params=[])
16
+ Rack::Router.new(method, path, params)
17
+ end
18
+
19
+ # Remove the outer double quotes from a given string.
20
+ def unquote(text)
21
+ (text =~ /^\".*\"$/) ? text[1..-2] : text
22
+ end
23
+
24
+ # Return the key used to store the authenticated user.
25
+ def auth_key; 'cloudkit.user'; end
26
+
27
+ # Return the key used to indicate the presence of auth in a stack.
28
+ def auth_presence_key; 'cloudkit.auth'; end
29
+
30
+ # Return the key used to store auth challenge headers shared between
31
+ # OpenID and OAuth middleware.
32
+ def challenge_key; 'cloudkit.challenge'; end
33
+
34
+ # Return the 'via' key used to announce and track upstream middleware.
35
+ def via_key; 'cloudkit.via'; end
36
+
37
+ # Return the key used to store the 'flash' in the session.
38
+ def flash_key; 'cloudkit.flash'; end
39
+
40
+ # Return the 'via' key for the OAuth filter.
41
+ def oauth_filter_key; 'cloudkit.filter.oauth'; end
42
+
43
+ # Return the 'via' key for the OpenID filter.
44
+ def openid_filter_key; 'cloudkit.filter.openid'; end
45
+
46
+ # Return the key used to store the shared storage URI for the stack.
47
+ def storage_uri_key; 'cloudkit.storage.uri'; end
48
+
49
+ # Return the key for the login URL used in OpenID and OAuth middleware
50
+ # components.
51
+ def login_url_key; 'cloudkit.filter.openid.url.login'; end
52
+
53
+ # Return the key for the logout URL used in OpenID and OAuth middleware
54
+ # components.
55
+ def logout_url_key; 'cloudkit.filter.openid.url.logout'; end
56
+
57
+ # Return the outer namespace key for the JSON store.
58
+ def store_key; :cloudkit_json_store; end
59
+ end
60
+ end