commit 0d15277178b520da2487f39239bb1964ec2ff934 Author: Sebastian Kippe Date: Mon Feb 27 16:32:12 2012 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1d3ed4c --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +config.yml diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..38741b7 --- /dev/null +++ b/Gemfile @@ -0,0 +1,9 @@ +source "http://rubygems.org" + +gem "sinatra" +gem "sinatra-contrib" +gem "riak-client" + +group :test do + gem 'purdytest', :require => false +end diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..25fc62a --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,43 @@ +GEM + remote: http://rubygems.org/ + specs: + backports (2.3.0) + beefcake (0.3.7) + builder (3.0.0) + eventmachine (0.12.10) + i18n (0.6.0) + minitest (2.10.0) + multi_json (1.0.4) + purdytest (1.0.0) + minitest (~> 2.2) + rack (1.3.5) + rack-protection (1.1.4) + rack + rack-test (0.6.1) + rack (>= 1.0) + riak-client (1.0.0) + beefcake (~> 0.3.7) + builder (>= 2.1.2) + i18n (>= 0.4.0) + multi_json (~> 1.0.0) + sinatra (1.3.1) + rack (>= 1.3.4, ~> 1.3) + rack-protection (>= 1.1.2, ~> 1.1) + tilt (>= 1.3.3, ~> 1.3) + sinatra-contrib (1.3.1) + backports (>= 2.0) + eventmachine + rack-protection + rack-test + sinatra (~> 1.3.0) + tilt (~> 1.3) + tilt (1.3.3) + +PLATFORMS + ruby + +DEPENDENCIES + purdytest + riak-client + sinatra + sinatra-contrib diff --git a/README.md b/README.md new file mode 100644 index 0000000..8507315 --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +# Liquor Cabinet + +Liquor Cabinet is where Frank stores all his stuff. It's a +remoteStorage-compatible storage provider API, based on Sinatra and currently +using Riak as backend. You can use it on its own, or e.g. mount it from a Rails +application. + +It's merely implementing the storage API, not including the Webfinger and Oauth +parts of remoteStorage. You have to set the authorization keys/values in the +database yourself. + +If you have any questions about this thing, drop by #unhosted on Freenode, and +we'll happily answer them. diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..9618286 --- /dev/null +++ b/Rakefile @@ -0,0 +1,5 @@ +require 'rake/testtask' + +Rake::TestTask.new do |t| + t.pattern = 'spec/**/*_spec.rb' +end diff --git a/config.ru b/config.ru new file mode 100644 index 0000000..19bd439 --- /dev/null +++ b/config.ru @@ -0,0 +1,7 @@ +require 'rubygems' +require 'bundler' + +Bundler.require + +require './liquor_cabinet' +run LiquorCabinet diff --git a/config.yml.example b/config.yml.example new file mode 100644 index 0000000..6e90101 --- /dev/null +++ b/config.yml.example @@ -0,0 +1,11 @@ +development: + riak: + host: localhost + http_port: 8098 + +test: + riak: + host: localhost + http_port: 8098 + +production: diff --git a/lib/remote_storage/riak.rb b/lib/remote_storage/riak.rb new file mode 100644 index 0000000..296236f --- /dev/null +++ b/lib/remote_storage/riak.rb @@ -0,0 +1,43 @@ +require "riak" + +module RemoteStorage + module Riak + + def authorize_request(user, category, token) + return true if category == "public" && env["REQUEST_METHOD"] == "GET" + + client = ::Riak::Client.new(settings.riak_config) + categories = client.bucket("authorizations").get("#{user}:#{token}").data + + halt 403 unless categories.include?(category) + rescue ::Riak::HTTPFailedRequest + halt 403 + end + + def get_data(user, category, key) + client = ::Riak::Client.new(settings.riak_config) + client.bucket("user_data").get("#{user}:#{category}:#{key}").data + rescue ::Riak::HTTPFailedRequest + halt 404 + end + + def put_data(user, category, key, data) + client = ::Riak::Client.new(settings.riak_config) + object = client.bucket("user_data").new("#{user}:#{category}:#{key}") + object.content_type = "text/plain" + object.data = data + object.store + rescue ::Riak::HTTPFailedRequest + halt 422 + end + + def delete_data(user, category, key) + client = ::Riak::Client.new(settings.riak_config) + riak_response = client.bucket("user_data").delete("#{user}:#{category}:#{key}") + halt riak_response[:code] + rescue ::Riak::HTTPFailedRequest + halt 404 + end + + end +end diff --git a/liquor-cabinet.gemspec b/liquor-cabinet.gemspec new file mode 100644 index 0000000..a48d80c --- /dev/null +++ b/liquor-cabinet.gemspec @@ -0,0 +1,26 @@ +# -*- encoding: utf-8 -*- +lib = File.expand_path('../lib/', __FILE__) +$:.unshift lib unless $:.include?(lib) + +require 'bundler/version' + +Gem::Specification.new do |s| + s.name = "liquor-cabinet" + s.version = "0.0.1" + s.platform = Gem::Platform::RUBY + s.authors = ["Sebastian Kippe"] + s.email = ["sebastian@5apps.com"] + s.homepage = "" + s.summary = "" + s.description = "" + + s.required_rubygems_version = ">= 1.3.6" + + s.add_dependency('sinatra') + s.add_dependency('sinatra-contrib') + s.add_dependency('riak-client') + + s.files = Dir.glob("{bin,lib}/**/*") + Dir['*.rb'] + # s.executables = ['config.ru'] + s.require_paths << '.' +end diff --git a/liquor-cabinet.rb b/liquor-cabinet.rb new file mode 100644 index 0000000..f5124ca --- /dev/null +++ b/liquor-cabinet.rb @@ -0,0 +1,51 @@ +$LOAD_PATH << File.join(File.expand_path(File.dirname(__FILE__)), 'lib') + +require "json" +require "sinatra/base" +require "sinatra/reloader" +require "remote_storage/riak" + +class LiquorCabinet < Sinatra::Base + + include RemoteStorage::Riak + + configure :development do + register Sinatra::Reloader + enable :logging + end + + configure :development, :test, :production do + config = File.read(File.expand_path('config.yml', File.dirname(__FILE__))) + riak_config = YAML.load(config)[ENV['RACK_ENV']]['riak'].symbolize_keys + set :riak_config, riak_config + end + + before "/:user/:category/:key" do + @user, @category, @key = params[:user], params[:category], params[:key] + token = env["HTTP_AUTHORIZATION"] ? env["HTTP_AUTHORIZATION"].split(" ")[1] : "" + + authorize_request(@user, @category, token) + end + + get "/ohai" do + "Ohai." + end + + get "/headers" do + env["HTTP_AUTHORIZATION"] + end + + get "/:user/:category/:key" do + get_data(@user, @category, @key) + end + + put "/:user/:category/:key" do + data = request.body.read + put_data(@user, @category, @key, data) + end + + delete "/:user/:category/:key" do + delete_data(@user, @category, @key) + end + +end diff --git a/spec/app_spec.rb b/spec/app_spec.rb new file mode 100644 index 0000000..61d1f6a --- /dev/null +++ b/spec/app_spec.rb @@ -0,0 +1,137 @@ +ENV["RACK_ENV"] = "test" +require_relative "spec_helper" + +describe "App" do + include Rack::Test::Methods + include RemoteStorage::Riak + + def app + LiquorCabinet + end + + def storage_client + ::Riak::Client.new(settings.riak_config) + end + + it "should say hello" do + get "/ohai" + assert last_response.ok? + last_response.body.must_include "Ohai." + end + + it "should return 404 on non-existing routes" do + get "/myunclesam" + last_response.status.must_equal 404 + end + + describe "GET public data" do + before do + object = storage_client.bucket("user_data").new("jimmy:public:foo") + object.content_type = "text/plain" + object.data = "some text data" + object.store + end + + after do + storage_client.bucket("user_data").delete("jimmy:public:foo") + end + + it "returns the value on all get requests" do + get "/jimmy/public/foo" + + last_response.status.must_equal 200 + last_response.body.must_equal "some text data" + end + end + + describe "private data" do + before do + object = storage_client.bucket("user_data").new("jimmy:documents:foo") + object.content_type = "text/plain" + object.data = "some private text data" + object.store + + auth = storage_client.bucket("authorizations").new("jimmy:123") + auth.data = ["documents", "public"] + auth.store + end + + after do + storage_client.bucket("user_data").delete("jimmy:documents:foo") + storage_client.bucket("authorizations").delete("jimmy:123") + end + + describe "GET" do + it "returns the value" do + header "Authorization", "Bearer 123" + get "/jimmy/documents/foo" + + last_response.status.must_equal 200 + last_response.body.must_equal "some private text data" + end + end + + describe "GET nonexisting key" do + it "returns a 404" do + header "Authorization", "Bearer 123" + get "/jimmy/documents/somestupidkey" + + last_response.status.must_equal 404 + end + end + + describe "PUT" do + it "saves the value" do + header "Authorization", "Bearer 123" + put "/jimmy/documents/bar", "another text" + + last_response.status.must_equal 200 + storage_client.bucket("user_data").get("jimmy:documents:bar").data.must_equal "another text" + end + end + + describe "DELETE" do + it "removes the key" do + header "Authorization", "Bearer 123" + delete "/jimmy/documents/foo" + + last_response.status.must_equal 204 + lambda {storage_client.bucket("user_data").get("jimmy:documents:foo")}.must_raise Riak::HTTPFailedRequest + end + end + end + + describe "unauthorized access" do + before do + auth = storage_client.bucket("authorizations").new("jimmy:123") + auth.data = ["documents", "public"] + auth.store + + header "Authorization", "Bearer 321" + end + + describe "GET" do + it "returns a 403" do + get "/jimmy/documents/foo" + + last_response.status.must_equal 403 + end + end + + describe "PUT" do + it "returns a 403" do + put "/jimmy/documents/foo", "some text" + + last_response.status.must_equal 403 + end + end + + describe "DELETE" do + it "returns a 403" do + delete "/jimmy/documents/foo" + + last_response.status.must_equal 403 + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..6a8e504 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,15 @@ +require 'rubygems' +require 'bundler' +Bundler.require + +require_relative '../liquor_cabinet' +require 'minitest/autorun' +require 'rack/test' +require 'purdytest' +require 'riak' + +set :environment, :test + +config = File.read(File.expand_path('../config.yml', File.dirname(__FILE__))) +riak_config = YAML.load(config)[ENV['RACK_ENV']]['riak'].symbolize_keys +set :riak_config, riak_config