From 97098bffaf70579f7268675cd7aeeee516df635f Mon Sep 17 00:00:00 2001 From: Fernando Blat Date: Mon, 14 Feb 2011 13:08:19 +0100 Subject: [PATCH] =?UTF-8?q?Allowing=20authorization=20via=20Api=20key=20pa?= =?UTF-8?q?rameter,=20associated=20with=20a=20domain=E2=80=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/api/json/tables_controller.rb | 54 +++++++++++-------- app/controllers/application_controller.rb | 8 +++ app/controllers/sessions_controller.rb | 9 +++- app/models/api_key.rb | 9 ++++ app/models/user.rb | 7 +++ config/initializers/warden.rb | 24 ++++++++- db/migrate/20110214111010_api_keys.rb | 16 ++++++ db/schema.rb | 13 ++++- spec/acceptance/api/tables_spec.rb | 27 ++++++++++ spec/models/user_spec.rb | 10 ++++ spec/support/test_jsonp.html | 54 +++++++++++++++++++ 11 files changed, 206 insertions(+), 25 deletions(-) create mode 100644 app/models/api_key.rb create mode 100644 db/migrate/20110214111010_api_keys.rb create mode 100644 spec/support/test_jsonp.html diff --git a/app/controllers/api/json/tables_controller.rb b/app/controllers/api/json/tables_controller.rb index 8adb49adb1..3e5e55f663 100644 --- a/app/controllers/api/json/tables_controller.rb +++ b/app/controllers/api/json/tables_controller.rb @@ -2,7 +2,7 @@ class Api::Json::TablesController < ApplicationController skip_before_filter :verify_authenticity_token - before_filter :login_required + before_filter :api_authorization_required before_filter :load_table, :except => [:index, :create, :query] # Get the list of tables of a user @@ -26,7 +26,8 @@ class Api::Json::TablesController < ApplicationController @tables = Table.select(:id,:user_id,:name,:privacy).all respond_to do |format| format.json do - render :json => @tables.map{ |table| {:id => table.id, :name => table.name, :privacy => table_privacy_text(table)} }.to_json + render :json => @tables.map{ |table| {:id => table.id, :name => table.name, :privacy => table_privacy_text(table)} }.to_json, + :callback => params[:callback] end end end @@ -47,7 +48,8 @@ class Api::Json::TablesController < ApplicationController def show respond_to do |format| format.json do - render :json => @table.to_json(:owner => current_user, :rows_per_page => params[:rows_per_page], :page => params[:page]) + render :json => @table.to_json(:owner => current_user, :rows_per_page => params[:rows_per_page], :page => params[:page]), + :callback => params[:callback] end end end @@ -67,7 +69,7 @@ class Api::Json::TablesController < ApplicationController def query respond_to do |format| format.json do - render :json => current_user.run_query(params[:query]).to_json + render :json => current_user.run_query(params[:query]).to_json, :callback => params[:callback] end end end @@ -82,7 +84,7 @@ class Api::Json::TablesController < ApplicationController def schema respond_to do |format| format.json do - render :json => @table.schema.to_json + render :json => @table.schema.to_json, :callback => params[:callback] end end end @@ -97,7 +99,7 @@ class Api::Json::TablesController < ApplicationController @table.toggle_privacy! respond_to do |format| format.json do - render :json => { :privacy => table_privacy_text(@table) }.to_json + render :json => { :privacy => table_privacy_text(@table) }.to_json, :callback => params[:callback] end end end @@ -128,12 +130,13 @@ class Api::Json::TablesController < ApplicationController begin @table.set_all(params) if @table.save - render :json => params.merge(@table.reload.values).to_json, :status => 200 + render :json => params.merge(@table.reload.values).to_json, :status => 200, :callback => params[:callback] else - render :json => { :errors => @table.errors.full_messages}.to_json, :status => 400 + render :json => { :errors => @table.errors.full_messages}.to_json, :status => 400, :callback => params[:callback] end rescue => e - render :json => { :errors => [translate_error(e.message.split("\n").first)] }.to_json, :status => 400 and return + render :json => { :errors => [translate_error(e.message.split("\n").first)] }.to_json, + :status => 400, :callback => params[:callback] and return end end end @@ -166,22 +169,25 @@ class Api::Json::TablesController < ApplicationController begin if params[:what] == 'add' resp = @table.add_column!(params[:column]) - render :json => resp.to_json, :status => 200 and return + render :json => resp.to_json, :status => 200, :callback => params[:callback] and return elsif params[:what] == 'drop' @table.drop_column!(params[:column]) - render :json => ''.to_json, :status => 200 and return + render :json => ''.to_json, :status => 200, :callback => params[:callback] and return else resp = @table.modify_column!(params[:column]) - render :json => resp.to_json, :status => 200 and return + render :json => resp.to_json, :status => 200, :callback => params[:callback] and return end rescue => e - render :json => { :errors => [translate_error(e.message.split("\n").first)] }.to_json, :status => 400 and return + render :json => { :errors => [translate_error(e.message.split("\n").first)] }.to_json, :status => 400, + :callback => params[:callback] and return end else - render :json => { :errors => ["column parameter can't be blank"] }.to_json, :status => 400 and return + render :json => { :errors => ["column parameter can't be blank"] }.to_json, :status => 400, + :callback => params[:callback] and return end else - render :json => { :errors => ["what parameter has an invalid value"] }.to_json, :status => 400 and return + render :json => { :errors => ["what parameter has an invalid value"] }.to_json, :status => 400, + :callback => params[:callback] and return end end end @@ -207,7 +213,7 @@ class Api::Json::TablesController < ApplicationController @table.insert_row!(params) respond_to do |format| format.json do - render :json => ''.to_json, :status => 200 + render :json => ''.to_json, :status => 200, :callback => params[:callback] end end end @@ -237,11 +243,13 @@ class Api::Json::TablesController < ApplicationController else case resp when 404 - render :json => { :errors => ["row with id = #{params[:row_id]} not found"] }.to_json, :status => 400 and return + render :json => { :errors => ["row with id = #{params[:row_id]} not found"] }.to_json, + :status => 400, :callback => params[:callback] and return end end else - render :json => { :errors => ["row_id can't be blank"] }.to_json, :status => 400 and return + render :json => { :errors => ["row_id can't be blank"] }.to_json, + :status => 400, :callback => params[:callback] and return end end end @@ -256,7 +264,7 @@ class Api::Json::TablesController < ApplicationController # * body: _nothing_ def delete @table.destroy - render :json => ''.to_json, :status => 200, :location => dashboard_path + render :json => ''.to_json, :status => 200, :location => dashboard_path, :callback => params[:callback] end # Create a new table @@ -282,12 +290,14 @@ class Api::Json::TablesController < ApplicationController end @table.force_schema = params[:schema] if params[:schema] if @table.valid? && @table.save - render :json => { :id => @table.id }.to_json, :status => 200, :location => table_path(@table) + render :json => { :id => @table.id }.to_json, :status => 200, :location => table_path(@table), + :callback => params[:callback] else - render :json => { :errors => @table.errors.full_messages }.to_json, :status => 400 + render :json => { :errors => @table.errors.full_messages }.to_json, :status => 400, :callback => params[:callback] end rescue => e - render :json => { :errors => [translate_error(e.message.split("\n").first)] }.to_json, :status => 400 and return + render :json => { :errors => [translate_error(e.message.split("\n").first)] }.to_json, + :status => 400, :callback => params[:callback] and return end protected diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 7100cd9ee5..dd1ab64439 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -10,6 +10,14 @@ class ApplicationController < ActionController::Base protected + def api_authorization_required + api_authenticated? || authenticated? || not_authorized + end + + def api_authenticated? + env['warden'].authenticate(:api_key) + end + def render_404 respond_to do |format| format.html do diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 6ec7d7a60b..187b603735 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -22,7 +22,14 @@ class SessionsController < ApplicationController def unauthenticated flash[:alert] = 'Your account or your password is not ok' - render :action => 'new' and return + respond_to do |format| + format.html do + render :action => 'new' and return + end + format.json do + render :nothing => true, :status => 401 + end + end end end diff --git a/app/models/api_key.rb b/app/models/api_key.rb new file mode 100644 index 0000000000..663ef42d98 --- /dev/null +++ b/app/models/api_key.rb @@ -0,0 +1,9 @@ +# coding: UTF-8 + +class APIKey < Sequel::Model(:api_keys) + + def user + User[:id => user_id] + end + +end \ No newline at end of file diff --git a/app/models/user.rb b/app/models/user.rb index 84c7d46b97..db16c85dcb 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -121,4 +121,11 @@ class User < Sequel::Model Table.filter(:user_id => self.id).order(:id).reverse end + def create_key(domain) + raise "domain argument can't be blank" if domain.blank? + key = self.class.secure_digest(domain) + APIKey.create :api_key => key, :user_id => self.id, :domain => domain + key + end + end diff --git a/config/initializers/warden.rb b/config/initializers/warden.rb index 0996e6437b..8ceabdc56b 100644 --- a/config/initializers/warden.rb +++ b/config/initializers/warden.rb @@ -1,5 +1,5 @@ Rails.configuration.middleware.use RailsWarden::Manager do |manager| - manager.default_strategies :password + manager.default_strategies :password, :api_key manager.failure_app = SessionsController end @@ -26,4 +26,26 @@ Warden::Strategies.add(:password) do fail! end end +end + +Warden::Strategies.add(:api_key) do + def authenticate! + if params[:api_key] + if api_key = APIKey[:api_key => params[:api_key]] + if api_key.domain == request.host + success!(api_key.user) + else + fail! + end + else + fail! + end + else + fail! + end + end + + def fail! + render :status => 401, :nothing => true + end end \ No newline at end of file diff --git a/db/migrate/20110214111010_api_keys.rb b/db/migrate/20110214111010_api_keys.rb new file mode 100644 index 0000000000..780f3b05f1 --- /dev/null +++ b/db/migrate/20110214111010_api_keys.rb @@ -0,0 +1,16 @@ +class ApiKeysMigration < Sequel::Migration + + def up + create_table :api_keys do + primary_key :id + String :api_key, :null => false, :unique => true, :index => true + Integer :user_id, :null => false + String :domain, :null => false + end + end + + def down + drop_table :api_keys + end + +end diff --git a/db/schema.rb b/db/schema.rb index a86cdbd14c..a60ad72d50 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -1,5 +1,15 @@ Sequel.migration do up do + create_table(:api_keys, :ignore_index_errors=>true) do + primary_key :id + String :api_key, :text=>true, :null=>false + Integer :user_id, :null=>false + String :domain, :text=>true, :null=>false + + index [:api_key] + index [:api_key], :name=>:api_keys_api_key_key, :unique=>true + end + create_table(:schema_migrations) do String :filename, :text=>true, :null=>false @@ -40,6 +50,7 @@ Sequel.migration do String :database_name, :text=>true String :username, :text=>true, :null=>false Integer :tables_count, :default=>0, :null=>false + String :keys, :text=>true index [:email], :name=>:users_email_key, :unique=>true index [:username], :name=>:users_username_key, :unique=>true @@ -47,6 +58,6 @@ Sequel.migration do end down do - drop_table(:schema_migrations, :tags, :user_tables, :users) + drop_table(:api_keys, :schema_migrations, :tags, :user_tables, :users) end end diff --git a/spec/acceptance/api/tables_spec.rb b/spec/acceptance/api/tables_spec.rb index 33157c5b46..7c46eb8682 100644 --- a/spec/acceptance/api/tables_spec.rb +++ b/spec/acceptance/api/tables_spec.rb @@ -539,4 +539,31 @@ feature "Tables JSON API" do json_response['rows'][1].symbolize_keys[:name_of_species].should == "Eulagisca gigantea" end + + scenario "Run a query against a table using JSONP and key authorization" do + Capybara.current_driver = :selenium + + user = create_user + api_key = user.create_key "example.org" + api_key2 = user.create_key "127.0.0.1" + + post_json "/api/json/tables", { + :api_key => api_key, + :name => "antantaric species", + :file => Rack::Test::UploadedFile.new("#{Rails.root}/db/fake_data/import_csv_1.csv", "text/csv") + } + response.status.should == 200 + response.location.should =~ /tables\/(\d+)$/ + json_response = JSON(response.body) + table_id = response.location.match(/\/(\d+)$/)[1].to_i + + FileUtils.cp("#{Rails.root}/spec/support/test_jsonp.html", "#{Rails.root}/public/") + + visit "/test_jsonp.html?api_key=#{api_key2}" + + page.find("div#results").text.should == "Barrukia cristata" + + FileUtils.rm("#{Rails.root}/public/test_jsonp.html") + end + end \ No newline at end of file diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 6251931e92..d18f0a07c4 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -128,4 +128,14 @@ describe User do end end + it "can have different keys for accessing via JSONP API requrests" do + user = create_user + lambda { + user.create_key "" + }.should raise_error + key = user.create_key "mymashup.com" + api_key = APIKey.filter(:user_id => user.id, :domain => 'mymashup.com').first + api_key.api_key.should == key + end + end diff --git a/spec/support/test_jsonp.html b/spec/support/test_jsonp.html new file mode 100644 index 0000000000..17930647a8 --- /dev/null +++ b/spec/support/test_jsonp.html @@ -0,0 +1,54 @@ + + + + + JSONP getJSON Jquery cross domain example + + + + + + +
+ +
+ +
+ No results +
+ + + \ No newline at end of file