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
@@ -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