From faa31affa8207305e9826e805c3bc08fbe83dd65 Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 8 Jul 2014 10:23:05 +0200 Subject: rename warden extension to patch the original the Warden::SessionSerializer was not getting loaded at all because we had a file by the same name. We want it to get loaded and be patched instead. --- config/initializers/warden.rb | 1 + lib/extensions/warden.rb | 13 +++++++++++++ lib/warden/session_serializer.rb | 13 ------------- 3 files changed, 14 insertions(+), 13 deletions(-) create mode 100644 lib/extensions/warden.rb delete mode 100644 lib/warden/session_serializer.rb diff --git a/config/initializers/warden.rb b/config/initializers/warden.rb index 22892b3..43e31ba 100644 --- a/config/initializers/warden.rb +++ b/config/initializers/warden.rb @@ -1,4 +1,5 @@ require "warden/session_serializer" +require "extensions/warden" require "warden/strategies/secure_remote_password" Rails.configuration.middleware.use RailsWarden::Manager do |config| diff --git a/lib/extensions/warden.rb b/lib/extensions/warden.rb new file mode 100644 index 0000000..81d7076 --- /dev/null +++ b/lib/extensions/warden.rb @@ -0,0 +1,13 @@ +module Warden + # Setup Session Serialization + class SessionSerializer + def serialize(record) + [record.class.name, record.id] + end + + def deserialize(keys) + klass, id = keys + klass.constantize.find(id) + end + end +end diff --git a/lib/warden/session_serializer.rb b/lib/warden/session_serializer.rb deleted file mode 100644 index 81d7076..0000000 --- a/lib/warden/session_serializer.rb +++ /dev/null @@ -1,13 +0,0 @@ -module Warden - # Setup Session Serialization - class SessionSerializer - def serialize(record) - [record.class.name, record.id] - end - - def deserialize(keys) - klass, id = keys - klass.constantize.find(id) - end - end -end -- cgit v1.2.3 From cf71d4ef08d88ee85763b258b2738fc26e3ed3eb Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 8 Jul 2014 10:24:24 +0200 Subject: separate login_required from access denied response They are very different. Let's handle them in different methods. --- .../controller_extension/authentication.rb | 24 +++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/app/controllers/controller_extension/authentication.rb b/app/controllers/controller_extension/authentication.rb index 1f73f38..fae5145 100644 --- a/app/controllers/controller_extension/authentication.rb +++ b/app/controllers/controller_extension/authentication.rb @@ -16,7 +16,7 @@ module ControllerExtension::Authentication end def require_login - access_denied unless logged_in? + login_required unless logged_in? end # some actions only make sense if you are not logged in yet. @@ -29,14 +29,24 @@ module ControllerExtension::Authentication def access_denied respond_to do |format| format.html do - if logged_in? - redirect_to home_url, :alert => t(:not_authorized) - else - redirect_to login_url, :alert => t(:not_authorized_login) - end + redirect_to home_url, :alert => t(:not_authorized) end format.json do - render :json => {'error' => t(:not_authorized)}, status: :unprocessable_entity + render :json => {'error' => t(:not_authorized)}, status: :forbidden + end + end + end + + def login_required + respond_to do |format| + format.html do + redirect_to login_url, alert: t(:not_authorized_login) + end + format.json do + # Warden will intercept the 401 response and call + # SessionController#unauthenticated instead. + render json: {error: t(:not_authorized_login)}, + status: :unauthorized end end end -- cgit v1.2.3 From b79a97235b5474e4775c07be1fb7c6208a29f5b4 Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 8 Jul 2014 10:28:50 +0200 Subject: SessionsController#unauthenticated for 401s Warden will catch all 401 responses at the rack level and call the app for failures. By default that is SessionsController#unauthenticated. I'm sticking with this. If we ever have other rack endpoints they can just send a 401 and the webapp will take care of the message. Other options would have been to tell warden not to take care of 401 either during initialization or by calling custom_failure! in the login_required method. We probably want a response that has a unique identifier for the error to process by the client and a translated message later on. For now i think the 401 suffices to identify the issue at hand. --- app/controllers/sessions_controller.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 8919a4d..4818191 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -15,6 +15,14 @@ class SessionsController < ApplicationController redirect_to home_url end + # + # Warden will catch all 401s and run this instead: + # + def unauthenticated + render json: {error: t(:not_authorized_login)}, + status: :unauthorized + end + # # this is a bad hack, but user_url(user) is not available # also, this doesn't work because the redirect happens as a PUT. no idea why. -- cgit v1.2.3 From 4dbfdf30c3235eb19e4f0ad959f65125ed18b39a Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 8 Jul 2014 10:57:44 +0200 Subject: render valid json error if provider file not found --- app/controllers/static_config_controller.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/static_config_controller.rb b/app/controllers/static_config_controller.rb index c669316..450cbb2 100644 --- a/app/controllers/static_config_controller.rb +++ b/app/controllers/static_config_controller.rb @@ -17,8 +17,8 @@ class StaticConfigController < ActionController::Base render :text => File.read(PROVIDER_JSON) end else - render :text => 'not found', :status => 404 + render json: {error: 'not found'}, status: 404 end end -end \ No newline at end of file +end -- cgit v1.2.3 From 303ec07901af3798efc873cbe050aa5cb4ba7655 Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 8 Jul 2014 11:00:40 +0200 Subject: use cucumber; initial ConfigsController --- Gemfile | 4 + Gemfile.lock | 20 +++++ app/controllers/v1/configs_controller.rb | 11 +++ config/cucumber.yml | 8 ++ config/routes.rb | 1 + features/config.feature | 39 +++++++++ features/step_definitions/.gitkeep | 0 features/step_definitions/api_steps.rb | 132 ++++++++++++++++++++++++++++++ features/step_definitions/config_steps.rb | 6 ++ features/support/env.rb | 58 +++++++++++++ features/support/hooks.rb | 6 ++ lib/tasks/cucumber.rake | 65 +++++++++++++++ script/cucumber | 10 +++ 13 files changed, 360 insertions(+) create mode 100644 app/controllers/v1/configs_controller.rb create mode 100644 config/cucumber.yml create mode 100644 features/config.feature create mode 100644 features/step_definitions/.gitkeep create mode 100644 features/step_definitions/api_steps.rb create mode 100644 features/step_definitions/config_steps.rb create mode 100644 features/support/env.rb create mode 100644 features/support/hooks.rb create mode 100644 lib/tasks/cucumber.rake create mode 100755 script/cucumber diff --git a/Gemfile b/Gemfile index 79e6e45..93f34e0 100644 --- a/Gemfile +++ b/Gemfile @@ -55,6 +55,10 @@ group :test do # billing tests gem 'fake_braintree', require: false + + # we use cucumber to document and test the api + gem 'cucumber-rails', require: false + gem 'jsonpath', require: false end group :test, :development do diff --git a/Gemfile.lock b/Gemfile.lock index 1060d70..a286d6f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -88,6 +88,18 @@ GEM actionpack couchrest couchrest_model + cucumber (1.3.15) + builder (>= 2.1.2) + diff-lcs (>= 1.1.3) + gherkin (~> 2.12) + multi_json (>= 1.7.5, < 2.0) + multi_test (>= 0.1.1) + cucumber-rails (1.4.1) + capybara (>= 1.1.2, < 3) + cucumber (>= 1.3.8, < 2) + mime-types (~> 1.16) + nokogiri (~> 1.5) + rails (>= 3, < 5) daemons (1.1.9) debugger (1.6.6) columnize (>= 0.3.1) @@ -95,6 +107,7 @@ GEM debugger-ruby_core_source (~> 1.3.2) debugger-linecache (1.2.0) debugger-ruby_core_source (1.3.2) + diff-lcs (1.2.5) erubis (2.7.0) eventmachine (1.0.3) execjs (2.0.2) @@ -113,6 +126,8 @@ GEM faker (1.2.0) i18n (~> 0.5) ffi (1.9.3) + gherkin (2.12.2) + multi_json (~> 1.3) haml (3.1.8) haml-rails (0.3.5) actionpack (>= 3.1, < 4.1) @@ -129,6 +144,8 @@ GEM railties (>= 3.0, < 5.0) thor (>= 0.14, < 2.0) json (1.8.1) + jsonpath (0.5.6) + multi_json kaminari (0.13.0) actionpack (>= 3.0.0) activesupport (>= 3.0.0) @@ -146,6 +163,7 @@ GEM mocha (0.13.3) metaclass (~> 0.0.1) multi_json (1.10.0) + multi_test (0.1.1) nokogiri (1.6.1) mini_portile (~> 0.5.0) phantomjs-binaries (1.9.2.3) @@ -249,6 +267,7 @@ DEPENDENCIES couchrest (~> 1.1.3) couchrest_model (~> 2.0.0) couchrest_session_store (~> 0.2.4) + cucumber-rails debugger factory_girl_rails fake_braintree @@ -259,6 +278,7 @@ DEPENDENCIES i18n-missing_translations jquery-rails json + jsonpath kaminari (= 0.13.0) launchy leap_web_billing! diff --git a/app/controllers/v1/configs_controller.rb b/app/controllers/v1/configs_controller.rb new file mode 100644 index 0000000..a43861b --- /dev/null +++ b/app/controllers/v1/configs_controller.rb @@ -0,0 +1,11 @@ +class V1::ConfigsController < ApplicationController + + before_filter :require_login + + def index + end + + def show + end + +end diff --git a/config/cucumber.yml b/config/cucumber.yml new file mode 100644 index 0000000..19b288d --- /dev/null +++ b/config/cucumber.yml @@ -0,0 +1,8 @@ +<% +rerun = File.file?('rerun.txt') ? IO.read('rerun.txt') : "" +rerun_opts = rerun.to_s.strip.empty? ? "--format #{ENV['CUCUMBER_FORMAT'] || 'progress'} features" : "--format #{ENV['CUCUMBER_FORMAT'] || 'pretty'} #{rerun}" +std_opts = "--format #{ENV['CUCUMBER_FORMAT'] || 'pretty'} --strict --tags ~@wip" +%> +default: <%= std_opts %> features +wip: --tags @wip:3 --wip features +rerun: <%= rerun_opts %> --format rerun --out rerun.txt --strict --tags ~@wip diff --git a/config/routes.rb b/config/routes.rb index 468e14e..3936824 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -35,6 +35,7 @@ LeapWeb::Application.routes.draw do resource :cert, :only => [:show, :create] resource :smtp_cert, :only => [:create] resource :service, :only => [:show] + resources :configs, :only => [:index, :show] end scope "(:locale)", :locale => MATCH_LOCALE do diff --git a/features/config.feature b/features/config.feature new file mode 100644 index 0000000..2d237f2 --- /dev/null +++ b/features/config.feature @@ -0,0 +1,39 @@ +Feature: Download Provider Configuration + + The LEAP Provider exposes parts of its configuration through the API. + + This can be used to find out about services offered. The big picture can be retrieved from `/provider.json`. More detailed settings of the services are available after authentication. You can get a list of the available settings from `/1/configs.json`. + + Background: + Given I set headers: + | Accept | application/json | + | Content-Type | application/json | + + @tempfile + Scenario: Fetch provider config + Given the provider config is: + """ + {"config": "me"} + """ + When I send a GET request to "/provider.json" + Then the response status should be "200" + And the response should be: + """ + {"config": "me"} + """ + + Scenario: Missing provider config + When I send a GET request to "/provider.json" + Then the response status should be "404" + And the response should be: + """ + {"error": "not found"} + """ + + Scenario: Authentication required for list of configs + When I send a GET request to "/1/configs" + Then the response status should be "401" + And the response should be: + """ + {"error": "Please log in to perform that action."} + """ diff --git a/features/step_definitions/.gitkeep b/features/step_definitions/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/features/step_definitions/api_steps.rb b/features/step_definitions/api_steps.rb new file mode 100644 index 0000000..0e52f7a --- /dev/null +++ b/features/step_definitions/api_steps.rb @@ -0,0 +1,132 @@ +require 'jsonpath' + +if defined?(Rack) + + # Monkey patch Rack::MockResponse to work properly with response debugging + class Rack::MockResponse + def to_str + body + end + end + + World(Rack::Test::Methods) + +end + +Given /^I set headers:$/ do |headers| + headers.rows_hash.each {|k,v| header k, v } +end + +Given /^I send and accept (XML|JSON)$/ do |type| + header 'Accept', "application/#{type.downcase}" + header 'Content-Type', "application/#{type.downcase}" +end + +Given /^I send and accept HTML$/ do + header 'Accept', "text/html" + header 'Content-Type', "application/x-www-form-urlencoded" +end + +When /^I authenticate as the user "([^"]*)" with the password "([^"]*)"$/ do |user, pass| + authorize user, pass +end + +When /^I digest\-authenticate as the user "(.*?)" with the password "(.*?)"$/ do |user, pass| + digest_authorize user, pass +end + +When /^I send a (GET|POST|PUT|DELETE) request (?:for|to) "([^"]*)"(?: with the following:)?$/ do |*args| + request_type = args.shift + path = args.shift + input = args.shift + + request_opts = {method: request_type.downcase.to_sym} + + unless input.nil? + if input.class == Cucumber::Ast::Table + request_opts[:params] = input.rows_hash + else + request_opts[:input] = input + end + end + + request path, request_opts +end + +Then /^show me the (unparsed)?\s?response$/ do |unparsed| + if unparsed == 'unparsed' + puts last_response.body + elsif last_response.headers['Content-Type'] =~ /json/ + json_response = JSON.parse(last_response.body) + puts JSON.pretty_generate(json_response) + else + puts last_response.headers + puts last_response.body + end +end + +Then /^the response status should be "([^"]*)"$/ do |status| + if self.respond_to? :should + last_response.status.should == status.to_i + else + assert_equal status.to_i, last_response.status + end +end + +Then /^the response should (not)?\s?have "([^"]*)"$/ do |negative, json_path| + json = JSON.parse(last_response.body) + results = JsonPath.new(json_path).on(json).to_a.map(&:to_s) + if self.respond_to?(:should) + if negative.present? + results.should be_empty + else + results.should_not be_empty + end + else + if negative.present? + assert results.empty? + else + assert !results.empty? + end + end +end + + +Then /^the response should (not)?\s?have "([^"]*)" with the text "([^"]*)"$/ do |negative, json_path, text| + json = JSON.parse(last_response.body) + results = JsonPath.new(json_path).on(json).to_a.map(&:to_s) + if self.respond_to?(:should) + if negative.present? + results.should_not include(text) + else + results.should include(text) + end + else + if negative.present? + assert !results.include?(text) + else + assert results.include?(text) + end + end +end + +Then /^the response should be:$/ do |json| + expected = JSON.parse(json) + actual = JSON.parse(last_response.body) + + if self.respond_to?(:should) + actual.should == expected + else + assert_equal expected, actual + end +end + +Then /^the response should have "([^"]*)" with a length of (\d+)$/ do |json_path, length| + json = JSON.parse(last_response.body) + results = JsonPath.new(json_path).on(json) + if self.respond_to?(:should) + results.length.should == length.to_i + else + assert_equal length.to_i, results.length + end +end diff --git a/features/step_definitions/config_steps.rb b/features/step_definitions/config_steps.rb new file mode 100644 index 0000000..50ae829 --- /dev/null +++ b/features/step_definitions/config_steps.rb @@ -0,0 +1,6 @@ +Given /the provider config is:$/ do |config| + @tempfile = Tempfile.new('provider.json') + @tempfile.write config + @tempfile.close + StaticConfigController::PROVIDER_JSON = @tempfile.path +end diff --git a/features/support/env.rb b/features/support/env.rb new file mode 100644 index 0000000..d3067db --- /dev/null +++ b/features/support/env.rb @@ -0,0 +1,58 @@ +# IMPORTANT: This file is generated by cucumber-rails - edit at your own peril. +# It is recommended to regenerate this file in the future when you upgrade to a +# newer version of cucumber-rails. Consider adding your own code to a new file +# instead of editing this one. Cucumber will automatically load all features/**/*.rb +# files. + +require 'cucumber/rails' + +# Capybara defaults to CSS3 selectors rather than XPath. +# If you'd prefer to use XPath, just uncomment this line and adjust any +# selectors in your step definitions to use the XPath syntax. +# Capybara.default_selector = :xpath + +# By default, any exception happening in your Rails application will bubble up +# to Cucumber so that your scenario will fail. This is a different from how +# your application behaves in the production environment, where an error page will +# be rendered instead. +# +# Sometimes we want to override this default behaviour and allow Rails to rescue +# exceptions and display an error page (just like when the app is running in production). +# Typical scenarios where you want to do this is when you test your error pages. +# There are two ways to allow Rails to rescue exceptions: +# +# 1) Tag your scenario (or feature) with @allow-rescue +# +# 2) Set the value below to true. Beware that doing this globally is not +# recommended as it will mask a lot of errors for you! +# +ActionController::Base.allow_rescue = false + +# Remove/comment out the lines below if your app doesn't have a database. +# For some databases (like MongoDB and CouchDB) you may need to use :truncation instead. +begin + #DatabaseCleaner.strategy = :truncation +rescue NameError + raise "You need to add database_cleaner to your Gemfile (in the :test group) if you wish to use it." +end + +# You may also want to configure DatabaseCleaner to use different strategies for certain features and scenarios. +# See the DatabaseCleaner documentation for details. Example: +# +# Before('@no-txn,@selenium,@culerity,@celerity,@javascript') do +# # { :except => [:widgets] } may not do what you expect here +# # as Cucumber::Rails::Database.javascript_strategy overrides +# # this setting. +# DatabaseCleaner.strategy = :truncation +# end +# +# Before('~@no-txn', '~@selenium', '~@culerity', '~@celerity', '~@javascript') do +# DatabaseCleaner.strategy = :transaction +# end +# + +# Possible values are :truncation and :transaction +# The :transaction strategy is faster, but might give you threading problems. +# See https://github.com/cucumber/cucumber-rails/blob/master/features/choose_javascript_database_strategy.feature +Cucumber::Rails::Database.javascript_strategy = :truncation + diff --git a/features/support/hooks.rb b/features/support/hooks.rb new file mode 100644 index 0000000..360f231 --- /dev/null +++ b/features/support/hooks.rb @@ -0,0 +1,6 @@ +After('@tempfile') do + if @tempfile + @tempfile.close + @tempfile.unlink + end +end diff --git a/lib/tasks/cucumber.rake b/lib/tasks/cucumber.rake new file mode 100644 index 0000000..9f53ce4 --- /dev/null +++ b/lib/tasks/cucumber.rake @@ -0,0 +1,65 @@ +# IMPORTANT: This file is generated by cucumber-rails - edit at your own peril. +# It is recommended to regenerate this file in the future when you upgrade to a +# newer version of cucumber-rails. Consider adding your own code to a new file +# instead of editing this one. Cucumber will automatically load all features/**/*.rb +# files. + + +unless ARGV.any? {|a| a =~ /^gems/} # Don't load anything when running the gems:* tasks + +vendored_cucumber_bin = Dir["#{Rails.root}/vendor/{gems,plugins}/cucumber*/bin/cucumber"].first +$LOAD_PATH.unshift(File.dirname(vendored_cucumber_bin) + '/../lib') unless vendored_cucumber_bin.nil? + +begin + require 'cucumber/rake/task' + + namespace :cucumber do + Cucumber::Rake::Task.new({:ok => 'test:prepare'}, 'Run features that should pass') do |t| + t.binary = vendored_cucumber_bin # If nil, the gem's binary is used. + t.fork = true # You may get faster startup if you set this to false + t.profile = 'default' + end + + Cucumber::Rake::Task.new({:wip => 'test:prepare'}, 'Run features that are being worked on') do |t| + t.binary = vendored_cucumber_bin + t.fork = true # You may get faster startup if you set this to false + t.profile = 'wip' + end + + Cucumber::Rake::Task.new({:rerun => 'test:prepare'}, 'Record failing features and run only them if any exist') do |t| + t.binary = vendored_cucumber_bin + t.fork = true # You may get faster startup if you set this to false + t.profile = 'rerun' + end + + desc 'Run all features' + task :all => [:ok, :wip] + + task :statsetup do + require 'rails/code_statistics' + ::STATS_DIRECTORIES << %w(Cucumber\ features features) if File.exist?('features') + ::CodeStatistics::TEST_TYPES << "Cucumber features" if File.exist?('features') + end + end + desc 'Alias for cucumber:ok' + task :cucumber => 'cucumber:ok' + + task :default => :cucumber + + task :features => :cucumber do + STDERR.puts "*** The 'features' task is deprecated. See rake -T cucumber ***" + end + + # In case we don't have the generic Rails test:prepare hook, append a no-op task that we can depend upon. + task 'test:prepare' do + end + + task :stats => 'cucumber:statsetup' +rescue LoadError + desc 'cucumber rake task not available (cucumber not installed)' + task :cucumber do + abort 'Cucumber rake task is not available. Be sure to install cucumber as a gem or plugin' + end +end + +end diff --git a/script/cucumber b/script/cucumber new file mode 100755 index 0000000..7fa5c92 --- /dev/null +++ b/script/cucumber @@ -0,0 +1,10 @@ +#!/usr/bin/env ruby + +vendored_cucumber_bin = Dir["#{File.dirname(__FILE__)}/../vendor/{gems,plugins}/cucumber*/bin/cucumber"].first +if vendored_cucumber_bin + load File.expand_path(vendored_cucumber_bin) +else + require 'rubygems' unless ENV['NO_RUBYGEMS'] + require 'cucumber' + load Cucumber::BINARY +end -- cgit v1.2.3 From f1a8cefb810bef263d3a96edffbec511dbe15291 Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 8 Jul 2014 12:48:33 +0200 Subject: send static list of configs for now Also added authentication steps to cucumber --- app/controllers/v1/configs_controller.rb | 9 +++++++++ features/authentication.feature | 24 ++++++++++++++++++++++++ features/config.feature | 17 +++++++++++++++++ features/step_definitions/api_steps.rb | 5 ++++- features/step_definitions/auth_steps.rb | 6 ++++++ 5 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 features/authentication.feature create mode 100644 features/step_definitions/auth_steps.rb diff --git a/app/controllers/v1/configs_controller.rb b/app/controllers/v1/configs_controller.rb index a43861b..b11b0a9 100644 --- a/app/controllers/v1/configs_controller.rb +++ b/app/controllers/v1/configs_controller.rb @@ -1,8 +1,17 @@ class V1::ConfigsController < ApplicationController + CONFIGS = { + services: { + soledad: "/1/configs/soledad-service.json", + eip: "/1/configs/eip-service.json", + smtp: "/1/configs/smtp-service.json" + } + } + before_filter :require_login def index + render json: CONFIGS end def show diff --git a/features/authentication.feature b/features/authentication.feature new file mode 100644 index 0000000..52b562f --- /dev/null +++ b/features/authentication.feature @@ -0,0 +1,24 @@ +Feature: Authentication + + Authentication is handled with SRP. Once the SRP handshake has been successful a token will be transmitted. This token is used to authenticate further requests. + + In the scenarios MY_AUTH_TOKEN will serve as a placeholder for the actual token received. + + Background: + Given I set headers: + | Accept | application/json | + | Content-Type | application/json | + + Scenario: Submitting a valid token + Given I authenticated + And I set headers: + | Authorization | Token token="MY_AUTH_TOKEN" | + When I send a GET request to "/1/configs.json" + Then the response status should be "200" + + Scenario: Submitting an invalid token + Given I authenticated + And I set headers: + | Authorization | Token token="InvalidToken" | + When I send a GET request to "/1/configs.json" + Then the response status should be "401" diff --git a/features/config.feature b/features/config.feature index 2d237f2..f53d0bf 100644 --- a/features/config.feature +++ b/features/config.feature @@ -37,3 +37,20 @@ Feature: Download Provider Configuration """ {"error": "Please log in to perform that action."} """ + + Scenario: Fetch list of available configs + Given I authenticated + And I set headers: + | Authorization | Token token="MY_AUTH_TOKEN" | + When I send a GET request to "/1/configs.json" + Then the response status should be "200" + And the response should be: + """ + { + "services": { + "soledad": "/1/configs/soledad-service.json", + "eip": "/1/configs/eip-service.json", + "smtp": "/1/configs/smtp-service.json" + } + } + """ diff --git a/features/step_definitions/api_steps.rb b/features/step_definitions/api_steps.rb index 0e52f7a..3a24d68 100644 --- a/features/step_definitions/api_steps.rb +++ b/features/step_definitions/api_steps.rb @@ -14,7 +14,10 @@ if defined?(Rack) end Given /^I set headers:$/ do |headers| - headers.rows_hash.each {|k,v| header k, v } + headers.rows_hash.each do |key,value| + value.sub!('MY_AUTH_TOKEN', @my_auth_token.to_s) if @my_auth_token + header key, value + end end Given /^I send and accept (XML|JSON)$/ do |type| diff --git a/features/step_definitions/auth_steps.rb b/features/step_definitions/auth_steps.rb new file mode 100644 index 0000000..00d9004 --- /dev/null +++ b/features/step_definitions/auth_steps.rb @@ -0,0 +1,6 @@ + +Given /^I authenticated$/ do + @user = FactoryGirl.create(:user) + @my_auth_token = Token.create user_id: @user.id +end + -- cgit v1.2.3 From 091793265e23452890c6ca27fc64feb54df2ad0b Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 8 Jul 2014 19:08:39 +0200 Subject: move unauthenticated api endpoints into separate feature --- features/config.feature | 17 +++++------------ features/unauthenticated.feature | 31 +++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 12 deletions(-) create mode 100644 features/unauthenticated.feature diff --git a/features/config.feature b/features/config.feature index f53d0bf..6e6c429 100644 --- a/features/config.feature +++ b/features/config.feature @@ -2,12 +2,16 @@ Feature: Download Provider Configuration The LEAP Provider exposes parts of its configuration through the API. - This can be used to find out about services offered. The big picture can be retrieved from `/provider.json`. More detailed settings of the services are available after authentication. You can get a list of the available settings from `/1/configs.json`. + This can be used to find out about services offered. The big picture can be retrieved from `/provider.json`. Which is available without authentication (see unauthenticated.feature). + + More detailed settings of the services are available after authentication. You can get a list of the available settings from `/1/configs.json`. Background: + Given I authenticated Given I set headers: | Accept | application/json | | Content-Type | application/json | + | Authorization | Token token="MY_AUTH_TOKEN" | @tempfile Scenario: Fetch provider config @@ -30,18 +34,7 @@ Feature: Download Provider Configuration {"error": "not found"} """ - Scenario: Authentication required for list of configs - When I send a GET request to "/1/configs" - Then the response status should be "401" - And the response should be: - """ - {"error": "Please log in to perform that action."} - """ - Scenario: Fetch list of available configs - Given I authenticated - And I set headers: - | Authorization | Token token="MY_AUTH_TOKEN" | When I send a GET request to "/1/configs.json" Then the response status should be "200" And the response should be: diff --git a/features/unauthenticated.feature b/features/unauthenticated.feature new file mode 100644 index 0000000..b810bea --- /dev/null +++ b/features/unauthenticated.feature @@ -0,0 +1,31 @@ +Feature: Unauthenticated API endpoints + + Most of the LEAP Provider API requires authentication. + However there are a few exceptions - mostly prerequisits of authenticating. This feature and the authentication feature document these. + + Background: + Given I set headers: + | Accept | application/json | + | Content-Type | application/json | + + @tempfile + Scenario: Fetch provider config + Given the provider config is: + """ + {"config": "me"} + """ + When I send a GET request to "/provider.json" + Then the response status should be "200" + And the response should be: + """ + {"config": "me"} + """ + + Scenario: Authentication required for all other API endpoints + When I send a GET request to "/1/configs" + Then the response status should be "401" + And the response should be: + """ + {"error": "Please log in to perform that action."} + """ + -- cgit v1.2.3 From 60052d15ca02b1c40ed265bed6515880d2851b8f Mon Sep 17 00:00:00 2001 From: Azul Date: Thu, 10 Jul 2014 12:13:30 +0200 Subject: clean up and simplify error responses and test code --- .../controller_extension/authentication.rb | 24 +++++++-------- .../controller_extension/token_authentication.rb | 2 +- .../test/functional/customers_controller_test.rb | 6 ++-- .../test/functional/tickets_controller_test.rb | 2 +- test/functional/application_controller_test.rb | 4 +-- test/functional/users_controller_test.rb | 2 +- test/functional/v1/messages_controller_test.rb | 2 +- test/functional/v1/users_controller_test.rb | 4 ++- test/integration/api/cert_test.rb | 2 +- test/integration/api/login_test.rb | 2 +- test/integration/api/smtp_cert_test.rb | 2 +- test/integration/api/update_account_test.rb | 2 +- test/support/api_integration_test.rb | 5 ++++ test/support/auth_test_helper.rb | 34 ++++++++++++---------- test/support/rack_test.rb | 7 ++++- 15 files changed, 56 insertions(+), 44 deletions(-) diff --git a/app/controllers/controller_extension/authentication.rb b/app/controllers/controller_extension/authentication.rb index fae5145..687bc6e 100644 --- a/app/controllers/controller_extension/authentication.rb +++ b/app/controllers/controller_extension/authentication.rb @@ -27,26 +27,24 @@ module ControllerExtension::Authentication end def access_denied - respond_to do |format| - format.html do - redirect_to home_url, :alert => t(:not_authorized) - end - format.json do - render :json => {'error' => t(:not_authorized)}, status: :forbidden - end - end + respond_to_error :not_authorized, :forbidden, home_url end def login_required + # Warden will intercept the 401 response and call + # SessionController#unauthenticated instead. + respond_to_error :not_authorized_login, :unauthorized, login_url + end + + def respond_to_error(message, status=nil, redirect=nil) + message = t(message) if message.is_a?(Symbol) respond_to do |format| format.html do - redirect_to login_url, alert: t(:not_authorized_login) + redirect_to redirect, alert: message end format.json do - # Warden will intercept the 401 response and call - # SessionController#unauthenticated instead. - render json: {error: t(:not_authorized_login)}, - status: :unauthorized + status ||= :unprocessable_entity + render json: {error: message}, status: status end end end diff --git a/app/controllers/controller_extension/token_authentication.rb b/app/controllers/controller_extension/token_authentication.rb index b0ed624..1cb6ffa 100644 --- a/app/controllers/controller_extension/token_authentication.rb +++ b/app/controllers/controller_extension/token_authentication.rb @@ -12,7 +12,7 @@ module ControllerExtension::TokenAuthentication end def require_token - access_denied unless token_authenticate + login_required unless token_authenticate end def logout diff --git a/engines/billing/test/functional/customers_controller_test.rb b/engines/billing/test/functional/customers_controller_test.rb index 46c33c9..cc82fc1 100644 --- a/engines/billing/test/functional/customers_controller_test.rb +++ b/engines/billing/test/functional/customers_controller_test.rb @@ -27,11 +27,11 @@ class CustomersControllerTest < ActionController::TestCase test "no access if not logged in" do get :new - assert_access_denied(true, false) + assert_login_required get :show, :id => @customer.braintree_customer_id - assert_access_denied(true, false) + assert_login_required get :edit, :id => @customer.braintree_customer_id - assert_access_denied(true, false) + assert_login_required end diff --git a/engines/support/test/functional/tickets_controller_test.rb b/engines/support/test/functional/tickets_controller_test.rb index e36f5f6..a7a2011 100644 --- a/engines/support/test/functional/tickets_controller_test.rb +++ b/engines/support/test/functional/tickets_controller_test.rb @@ -45,7 +45,7 @@ class TicketsControllerTest < ActionController::TestCase user = find_record :user ticket = find_record :ticket, :created_by => user.id get :show, :id => ticket.id - assert_login_required + assert_access_denied end test "user tickets are visible to creator" do diff --git a/test/functional/application_controller_test.rb b/test/functional/application_controller_test.rb index c4c922b..b558ad8 100644 --- a/test/functional/application_controller_test.rb +++ b/test/functional/application_controller_test.rb @@ -9,13 +9,13 @@ class ApplicationControllerTest < ActionController::TestCase def test_require_login_redirect @controller.send(:require_login) - assert_access_denied(true, false) + assert_login_required end def test_require_login login @controller.send(:require_login) - assert_access_denied(false) + assert_access_granted end def test_require_admin diff --git a/test/functional/users_controller_test.rb b/test/functional/users_controller_test.rb index 4af9ca6..7d1745c 100644 --- a/test/functional/users_controller_test.rb +++ b/test/functional/users_controller_test.rb @@ -52,7 +52,7 @@ class UsersControllerTest < ActionController::TestCase nonid = 'thisisnotanexistinguserid' get :show, :id => nonid - assert_access_denied(true, false) + assert_login_required end test "may not show non-existing user without admin" do diff --git a/test/functional/v1/messages_controller_test.rb b/test/functional/v1/messages_controller_test.rb index 24a5b1f..a50fded 100644 --- a/test/functional/v1/messages_controller_test.rb +++ b/test/functional/v1/messages_controller_test.rb @@ -51,7 +51,7 @@ class V1::MessagesControllerTest < ActionController::TestCase test "fails if not authenticated" do get :index, :format => :json - assert_access_denied + assert_login_required end end diff --git a/test/functional/v1/users_controller_test.rb b/test/functional/v1/users_controller_test.rb index fe3cfe7..ffe2484 100644 --- a/test/functional/v1/users_controller_test.rb +++ b/test/functional/v1/users_controller_test.rb @@ -34,7 +34,9 @@ class V1::UsersControllerTest < ActionController::TestCase test "user cannot update other user" do user = find_record :user login - put :update, :user => record_attributes_for(:user_with_settings), :id => user.id, :format => :json + put :update, id: user.id, + user: record_attributes_for(:user_with_settings), + :format => :json assert_access_denied end diff --git a/test/integration/api/cert_test.rb b/test/integration/api/cert_test.rb index 74d439a..118fb9f 100644 --- a/test/integration/api/cert_test.rb +++ b/test/integration/api/cert_test.rb @@ -14,7 +14,7 @@ class CertTest < ApiIntegrationTest test "fetching certs requires login by default" do get '/1/cert', {}, RACK_ENV - assert_json_response error: I18n.t(:not_authorized) + assert_login_required end test "retrieve anonymous eip cert" do diff --git a/test/integration/api/login_test.rb b/test/integration/api/login_test.rb index 92d153f..f37639e 100644 --- a/test/integration/api/login_test.rb +++ b/test/integration/api/login_test.rb @@ -45,6 +45,6 @@ class LoginTest < SrpTest test "logout requires token" do authenticate logout(nil, {}) - assert_equal 422, last_response.status + assert_login_required end end diff --git a/test/integration/api/smtp_cert_test.rb b/test/integration/api/smtp_cert_test.rb index 7697e44..aee52cf 100644 --- a/test/integration/api/smtp_cert_test.rb +++ b/test/integration/api/smtp_cert_test.rb @@ -48,7 +48,7 @@ class SmtpCertTest < ApiIntegrationTest test "no anonymous smtp certs" do with_config allow_anonymous_certs: true do post '/1/smtp_cert', {}, RACK_ENV - assert_json_response error: I18n.t(:not_authorized) + assert_login_required end end end diff --git a/test/integration/api/update_account_test.rb b/test/integration/api/update_account_test.rb index 63429e7..16bbb8c 100644 --- a/test/integration/api/update_account_test.rb +++ b/test/integration/api/update_account_test.rb @@ -16,7 +16,7 @@ class UpdateAccountTest < SrpTest authenticate put "http://api.lvh.me:3000/1/users/" + @user.id + '.json', user_params(password: "No! Verify me instead.") - assert_access_denied + assert_login_required end test "update password via api" do diff --git a/test/support/api_integration_test.rb b/test/support/api_integration_test.rb index bd10f11..ccf7066 100644 --- a/test/support/api_integration_test.rb +++ b/test/support/api_integration_test.rb @@ -14,6 +14,11 @@ class ApiIntegrationTest < ActionDispatch::IntegrationTest @token.save end + def assert_login_required + assert_equal 401, get_response.status + assert_json_response error: I18n.t(:not_authorized_login) + end + teardown do if @user && @user.persisted? Identity.destroy_all_for @user diff --git a/test/support/auth_test_helper.rb b/test/support/auth_test_helper.rb index 38c2ea1..79d07d6 100644 --- a/test/support/auth_test_helper.rb +++ b/test/support/auth_test_helper.rb @@ -20,28 +20,30 @@ module AuthTestHelper end def assert_login_required - assert_access_denied(true, false) + assert_error_response :not_authorized_login, :unauthorized, login_url end - def assert_access_denied(denied = true, logged_in = true) - if denied - if @response.content_type == 'application/json' - assert_json_response('error' => I18n.t(:not_authorized)) - assert_response :unprocessable_entity - else - if logged_in - assert_equal({:alert => I18n.t(:not_authorized)}, flash.to_hash) - assert_redirected_to home_url - else - assert_equal({:alert => I18n.t(:not_authorized_login)}, flash.to_hash) - assert_redirected_to login_url - end - end + def assert_access_denied + assert_error_response :not_authorized, :forbidden, home_url + end + + def assert_error_response(message, status=nil, redirect=nil) + message = I18n.t(message) if message.is_a? Symbol + if @response.content_type == 'application/json' + status ||= :unprocessable_entity + assert_json_response('error' => message) + assert_response status else - assert flash[:alert].blank? + assert_equal({:alert => message}, flash.to_hash) + assert_redirected_to redirect end end + def assert_access_granted + assert flash[:alert].blank?, + "expected to have access but there was a flash alert" + end + def expect_logout expect_warden_logout @token.expects(:destroy) if @token diff --git a/test/support/rack_test.rb b/test/support/rack_test.rb index 806339a..83adf6c 100644 --- a/test/support/rack_test.rb +++ b/test/support/rack_test.rb @@ -13,7 +13,12 @@ class RackTest < ActiveSupport::TestCase def assert_access_denied assert_json_response('error' => I18n.t(:not_authorized)) - assert_response :unprocessable_entity + assert_response :forbidden + end + + def assert_login_required + assert_json_response('error' => I18n.t(:not_authorized_login)) + assert_response :unauthorized end # inspired by rails 4 -- cgit v1.2.3 From 3885308e9a2aa48f25313567525e375362253f47 Mon Sep 17 00:00:00 2001 From: Azul Date: Thu, 10 Jul 2014 12:54:34 +0200 Subject: minor: fix identity test for storing certs we compare the cert that expires last to the one we just saved. So we need to make sure the one we saved is the one that expires last. --- test/unit/identity_test.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/unit/identity_test.rb b/test/unit/identity_test.rb index cb0f6bd..77104b6 100644 --- a/test/unit/identity_test.rb +++ b/test/unit/identity_test.rb @@ -177,7 +177,9 @@ class IdentityTest < ActiveSupport::TestCase end def cert_stub - @cert_stub ||= stub expiry: 1.month.from_now, + # make this expire later than the others so it's on top + # when sorting by expiry descending. + @cert_stub ||= stub expiry: 2.month.from_now, fingerprint: SecureRandom.hex end end -- cgit v1.2.3 From b80be9832526ee956b3a73a634896c6cd8d2914e Mon Sep 17 00:00:00 2001 From: Azul Date: Mon, 14 Jul 2014 12:18:18 +0200 Subject: ApiController with API style auth require_login is require_token for the api controller It also skips the verify_authenticity_token before filter. So all Subclasses of the ApiController will only support token auth. Also made the V1::UsersController a bit more strict. Now way for admins to alter other users through the api. We don't support that yet so let's not allow it either. --- app/controllers/api_controller.rb | 11 +++++++++++ app/controllers/v1/certs_controller.rb | 2 +- app/controllers/v1/configs_controller.rb | 18 +++++++++--------- app/controllers/v1/messages_controller.rb | 7 ++----- app/controllers/v1/services_controller.rb | 4 +--- app/controllers/v1/sessions_controller.rb | 5 ++--- app/controllers/v1/smtp_certs_controller.rb | 2 +- app/controllers/v1/users_controller.rb | 14 +++++++++++--- 8 files changed, 38 insertions(+), 25 deletions(-) create mode 100644 app/controllers/api_controller.rb diff --git a/app/controllers/api_controller.rb b/app/controllers/api_controller.rb new file mode 100644 index 0000000..0aa9507 --- /dev/null +++ b/app/controllers/api_controller.rb @@ -0,0 +1,11 @@ +class ApiController < ApplicationController + + skip_before_filter :verify_authenticity_token + respond_to :json + + def require_login + require_token + end + +end + diff --git a/app/controllers/v1/certs_controller.rb b/app/controllers/v1/certs_controller.rb index b6d1d0b..68d6586 100644 --- a/app/controllers/v1/certs_controller.rb +++ b/app/controllers/v1/certs_controller.rb @@ -1,4 +1,4 @@ -class V1::CertsController < ApplicationController +class V1::CertsController < ApiController before_filter :require_login, :unless => :anonymous_certs_allowed? diff --git a/app/controllers/v1/configs_controller.rb b/app/controllers/v1/configs_controller.rb index b11b0a9..537123f 100644 --- a/app/controllers/v1/configs_controller.rb +++ b/app/controllers/v1/configs_controller.rb @@ -1,12 +1,4 @@ -class V1::ConfigsController < ApplicationController - - CONFIGS = { - services: { - soledad: "/1/configs/soledad-service.json", - eip: "/1/configs/eip-service.json", - smtp: "/1/configs/smtp-service.json" - } - } +class V1::ConfigsController < ApiController before_filter :require_login @@ -17,4 +9,12 @@ class V1::ConfigsController < ApplicationController def show end + CONFIGS = { + services: { + soledad: "/1/configs/soledad-service.json", + eip: "/1/configs/eip-service.json", + smtp: "/1/configs/smtp-service.json" + } + } + end diff --git a/app/controllers/v1/messages_controller.rb b/app/controllers/v1/messages_controller.rb index 85156b7..a9b93a9 100644 --- a/app/controllers/v1/messages_controller.rb +++ b/app/controllers/v1/messages_controller.rb @@ -1,10 +1,7 @@ module V1 - class MessagesController < ApplicationController + class MessagesController < ApiController - skip_before_filter :verify_authenticity_token - before_filter :require_token - - respond_to :json + before_filter :require_login def index render json: current_user.messages diff --git a/app/controllers/v1/services_controller.rb b/app/controllers/v1/services_controller.rb index 594940e..114870f 100644 --- a/app/controllers/v1/services_controller.rb +++ b/app/controllers/v1/services_controller.rb @@ -1,6 +1,4 @@ -class V1::ServicesController < ApplicationController - - respond_to :json +class V1::ServicesController < ApiController def show respond_with current_user.effective_service_level diff --git a/app/controllers/v1/sessions_controller.rb b/app/controllers/v1/sessions_controller.rb index d88fcdc..a343d9b 100644 --- a/app/controllers/v1/sessions_controller.rb +++ b/app/controllers/v1/sessions_controller.rb @@ -1,8 +1,7 @@ module V1 - class SessionsController < ApplicationController + class SessionsController < ApiController - skip_before_filter :verify_authenticity_token - before_filter :require_token, only: :destroy + before_filter :require_login, only: :destroy def new @session = Session.new diff --git a/app/controllers/v1/smtp_certs_controller.rb b/app/controllers/v1/smtp_certs_controller.rb index 377a49c..fa53b26 100644 --- a/app/controllers/v1/smtp_certs_controller.rb +++ b/app/controllers/v1/smtp_certs_controller.rb @@ -1,4 +1,4 @@ -class V1::SmtpCertsController < ApplicationController +class V1::SmtpCertsController < ApiController before_filter :require_login before_filter :require_email_account diff --git a/app/controllers/v1/users_controller.rb b/app/controllers/v1/users_controller.rb index abaefd8..5c9e33f 100644 --- a/app/controllers/v1/users_controller.rb +++ b/app/controllers/v1/users_controller.rb @@ -1,10 +1,9 @@ module V1 - class UsersController < UsersBaseController + class UsersController < ApiController - skip_before_filter :verify_authenticity_token before_filter :fetch_user, :only => [:update] before_filter :require_admin, :only => [:index] - before_filter :require_token, :only => [:update] + before_filter :require_login, :only => [:index, :update] before_filter :require_registration_allowed, only: :create respond_to :json @@ -29,11 +28,20 @@ module V1 respond_with @user end + protected + def require_registration_allowed unless APP_CONFIG[:allow_registration] head :forbidden end end + def fetch_user + @user = User.find(params[:id]) + if @user != current_user + access_denied + end + end + end end -- cgit v1.2.3 From f07c952c870bfb8634ef0d80737b67a1eec760f6 Mon Sep 17 00:00:00 2001 From: Azul Date: Mon, 14 Jul 2014 13:04:30 +0200 Subject: send config files from ConfigsController --- app/controllers/controller_extension/json_file.rb | 22 +++++++++++++++ app/controllers/static_config_controller.rb | 33 +++++++++++++---------- app/controllers/v1/configs_controller.rb | 28 ++++++++++++++----- 3 files changed, 62 insertions(+), 21 deletions(-) create mode 100644 app/controllers/controller_extension/json_file.rb diff --git a/app/controllers/controller_extension/json_file.rb b/app/controllers/controller_extension/json_file.rb new file mode 100644 index 0000000..0cb4b6d --- /dev/null +++ b/app/controllers/controller_extension/json_file.rb @@ -0,0 +1,22 @@ +module ControllerExtension::JsonFile + extend ActiveSupport::Concern + + protected + + def send_file + if stale?(:last_modified => @file.mtime) + response.content_type = 'application/json' + render :text => @file.read + end + end + + def fetch_file + if File.exists?(@filename) + @file = File.new(@filename) + else + not_found + end + end + +end + diff --git a/app/controllers/static_config_controller.rb b/app/controllers/static_config_controller.rb index 450cbb2..c78e006 100644 --- a/app/controllers/static_config_controller.rb +++ b/app/controllers/static_config_controller.rb @@ -2,23 +2,28 @@ # This controller is responsible for returning some static config files, such as /provider.json # class StaticConfigController < ActionController::Base + include ControllerExtension::JsonFile - PROVIDER_JSON = File.join(Rails.root, 'config', 'provider', 'provider.json') + before_filter :set_minimum_client_version + before_filter :set_filename + before_filter :fetch_file + + PROVIDER_JSON = Rails.root.join('config', 'provider', 'provider.json') - # - # return the provider.json, ensuring that the header X-Minimum-Client-Version is sent - # regardless if a 200 or 304 (not modified) response is sent. - # def provider - response.headers["X-Minimum-Client-Version"] = APP_CONFIG[:minimum_client_version].to_s - if File.exists?(PROVIDER_JSON) - if stale?(:last_modified => File.mtime(PROVIDER_JSON)) - response.content_type = 'application/json' - render :text => File.read(PROVIDER_JSON) - end - else - render json: {error: 'not found'}, status: 404 - end + send_file end + protected + + # ensure that the header X-Minimum-Client-Version is sent + # regardless if a 200 or 304 (not modified) or 404 response is sent. + def set_minimum_client_version + response.headers["X-Minimum-Client-Version"] = + APP_CONFIG[:minimum_client_version].to_s + end + + def set_filename + @filename = PROVIDER_JSON + end end diff --git a/app/controllers/v1/configs_controller.rb b/app/controllers/v1/configs_controller.rb index 537123f..0b2a64a 100644 --- a/app/controllers/v1/configs_controller.rb +++ b/app/controllers/v1/configs_controller.rb @@ -1,20 +1,34 @@ class V1::ConfigsController < ApiController + include ControllerExtension::JsonFile before_filter :require_login + before_filter :sanitize_filename, only: :show + before_filter :fetch_file, only: :show def index - render json: CONFIGS + render json: {services: service_paths} end def show + send_file end - CONFIGS = { - services: { - soledad: "/1/configs/soledad-service.json", - eip: "/1/configs/eip-service.json", - smtp: "/1/configs/smtp-service.json" - } + SERVICES = { + soledad: "soledad-service.json", + eip: "eip-service.json", + smtp: "smtp-service.json" } + protected + + def service_paths + Hash[SERVICES.map{|k,v| [k,"/1/configs/#{str}"] } ] + end + + def sanitize_filename + @filename = params[:id].downcase + @filename += '.json' unless @filename.ends_with?('.json') + access_denied unless SERVICES.values.include? name + @filename = Rails.root.join('public', '1', 'config', @filename) + end end -- cgit v1.2.3 From 67f70b31bd16b05759e1f8393f077ee17f2c34be Mon Sep 17 00:00:00 2001 From: Azul Date: Mon, 14 Jul 2014 15:49:31 +0200 Subject: move fetch_user into module so it can be mixed in We have an ApiController that wants to call #fetch_user. Since we can only inherit from one class i moved fetch_user into an extension. --- app/controllers/controller_extension/fetch_user.rb | 20 ++++++++++++++++++++ app/controllers/users_base_controller.rb | 18 ------------------ app/controllers/users_controller.rb | 3 ++- app/controllers/v1/users_controller.rb | 9 +-------- 4 files changed, 23 insertions(+), 27 deletions(-) create mode 100644 app/controllers/controller_extension/fetch_user.rb delete mode 100644 app/controllers/users_base_controller.rb diff --git a/app/controllers/controller_extension/fetch_user.rb b/app/controllers/controller_extension/fetch_user.rb new file mode 100644 index 0000000..695d723 --- /dev/null +++ b/app/controllers/controller_extension/fetch_user.rb @@ -0,0 +1,20 @@ +# +# fetch the user taking into account permissions. +# While normal users can only change settings for themselves +# admins can change things for all users. +# +module ControllerExtension::FetchUser + extend ActiveSupport::Concern + + protected + + def fetch_user + @user = User.find(params[:user_id] || params[:id]) + if !@user && admin? + redirect_to users_url, :alert => t(:no_such_thing, :thing => 'user') + elsif !admin? && @user != current_user + access_denied + end + end + +end diff --git a/app/controllers/users_base_controller.rb b/app/controllers/users_base_controller.rb deleted file mode 100644 index 9becf0d..0000000 --- a/app/controllers/users_base_controller.rb +++ /dev/null @@ -1,18 +0,0 @@ -# -# common base class for all user related controllers -# - -class UsersBaseController < ApplicationController - - protected - - def fetch_user - @user = User.find(params[:user_id] || params[:id]) - if !@user && admin? - redirect_to users_url, :alert => t(:no_such_thing, :thing => 'user') - elsif !admin? && @user != current_user - access_denied - end - end - -end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 0f822cb..dcf7607 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -2,7 +2,8 @@ # This is an HTML-only controller. For the JSON-only controller, see v1/users_controller.rb # -class UsersController < UsersBaseController +class UsersController < ApplicationController + include ControllerExtension::FetchUser before_filter :require_login, :except => [:new] before_filter :redirect_if_logged_in, :only => [:new] diff --git a/app/controllers/v1/users_controller.rb b/app/controllers/v1/users_controller.rb index 5c9e33f..bfa04fc 100644 --- a/app/controllers/v1/users_controller.rb +++ b/app/controllers/v1/users_controller.rb @@ -1,5 +1,6 @@ module V1 class UsersController < ApiController + include ControllerExtension::FetchUser before_filter :fetch_user, :only => [:update] before_filter :require_admin, :only => [:index] @@ -35,13 +36,5 @@ module V1 head :forbidden end end - - def fetch_user - @user = User.find(params[:id]) - if @user != current_user - access_denied - end - end - end end -- cgit v1.2.3 From 2f1ceb63bfef2fa7d92fcbad73a5ead5bd17b23e Mon Sep 17 00:00:00 2001 From: Azul Date: Mon, 14 Jul 2014 17:59:09 +0200 Subject: minor: remove @s added by search and replace meant to move id -> @id, also turned identity in the test titles into @identity. --- test/unit/identity_test.rb | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/unit/identity_test.rb b/test/unit/identity_test.rb index 77104b6..f5c95f8 100644 --- a/test/unit/identity_test.rb +++ b/test/unit/identity_test.rb @@ -14,23 +14,23 @@ class IdentityTest < ActiveSupport::TestCase end end - test "blank @identity does not crash on valid?" do + test "blank identity does not crash on valid?" do @id = Identity.new assert !@id.valid? end - test "enabled @identity requires destination" do + test "enabled identity requires destination" do @id = Identity.new user: @user, address: @user.email_address assert !@id.valid? assert_equal ["can't be blank"], @id.errors[:destination] end - test "disabled @identity requires no destination" do + test "disabled identity requires no destination" do @id = Identity.new address: @user.email_address assert @id.valid? end - test "initial @identity for a user" do + test "initial identity for a user" do @id = Identity.for(@user) assert_equal @user.email_address, @id.address assert_equal @user.email_address, @id.destination @@ -90,7 +90,7 @@ class IdentityTest < ActiveSupport::TestCase assert_equal @id.keys[:pgp], result["value"] end - test "fail to add non-local email address as @identity address" do + test "fail to add non-local email address as identity address" do @id = Identity.for @user, address: forward_address assert !@id.valid? assert_match /needs to end in/, @id.errors[:address].first @@ -110,7 +110,7 @@ class IdentityTest < ActiveSupport::TestCase assert @id.errors.messages[:destination].include? "needs to be a valid email address" end - test "disabled @identity" do + test "disabled identity" do @id = Identity.for(@user) @id.disable assert_equal @user.email_address, @id.address @@ -120,7 +120,7 @@ class IdentityTest < ActiveSupport::TestCase assert @id.valid? end - test "disabled @identity blocks handle" do + test "disabled identity blocks handle" do @id = Identity.for(@user) @id.disable @id.save -- cgit v1.2.3 From bb10a669e1129c662ba01f223bd5a0ee7f2a0344 Mon Sep 17 00:00:00 2001 From: Azul Date: Mon, 14 Jul 2014 18:00:14 +0200 Subject: fix controller refactor and features Also save debug log on failing features --- .../controller_extension/authentication.rb | 23 --------------- app/controllers/controller_extension/errors.rb | 34 ++++++++++++++++++++++ app/controllers/controller_extension/json_file.rb | 1 + .../controller_extension/token_authentication.rb | 2 ++ app/controllers/v1/configs_controller.rb | 2 +- config/initializers/add_controller_methods.rb | 1 + features/config.feature | 5 +--- features/support/hooks.rb | 14 ++++++++- 8 files changed, 53 insertions(+), 29 deletions(-) create mode 100644 app/controllers/controller_extension/errors.rb diff --git a/app/controllers/controller_extension/authentication.rb b/app/controllers/controller_extension/authentication.rb index 687bc6e..e2b24f0 100644 --- a/app/controllers/controller_extension/authentication.rb +++ b/app/controllers/controller_extension/authentication.rb @@ -26,29 +26,6 @@ module ControllerExtension::Authentication redirect_to home_url if logged_in? end - def access_denied - respond_to_error :not_authorized, :forbidden, home_url - end - - def login_required - # Warden will intercept the 401 response and call - # SessionController#unauthenticated instead. - respond_to_error :not_authorized_login, :unauthorized, login_url - end - - def respond_to_error(message, status=nil, redirect=nil) - message = t(message) if message.is_a?(Symbol) - respond_to do |format| - format.html do - redirect_to redirect, alert: message - end - format.json do - status ||= :unprocessable_entity - render json: {error: message}, status: status - end - end - end - def admin? current_user.is_admin? end diff --git a/app/controllers/controller_extension/errors.rb b/app/controllers/controller_extension/errors.rb new file mode 100644 index 0000000..8f8edde --- /dev/null +++ b/app/controllers/controller_extension/errors.rb @@ -0,0 +1,34 @@ +module ControllerExtension::Errors + extend ActiveSupport::Concern + + protected + + def access_denied + respond_to_error :not_authorized, :forbidden, home_url + end + + def login_required + # Warden will intercept the 401 response and call + # SessionController#unauthenticated instead. + respond_to_error :not_authorized_login, :unauthorized, login_url + end + + def not_found + respond_to_error :not_found, :not_found, home_url + end + + + def respond_to_error(message, status=nil, redirect=nil) + error = message + message = t(message) if message.is_a?(Symbol) + respond_to do |format| + format.html do + redirect_to redirect, alert: message + end + format.json do + status ||= :unprocessable_entity + render json: {error: error, message: message}, status: status + end + end + end +end diff --git a/app/controllers/controller_extension/json_file.rb b/app/controllers/controller_extension/json_file.rb index 0cb4b6d..6be919a 100644 --- a/app/controllers/controller_extension/json_file.rb +++ b/app/controllers/controller_extension/json_file.rb @@ -1,5 +1,6 @@ module ControllerExtension::JsonFile extend ActiveSupport::Concern + include ControllerExtension::Errors protected diff --git a/app/controllers/controller_extension/token_authentication.rb b/app/controllers/controller_extension/token_authentication.rb index 1cb6ffa..4ad1977 100644 --- a/app/controllers/controller_extension/token_authentication.rb +++ b/app/controllers/controller_extension/token_authentication.rb @@ -1,6 +1,8 @@ module ControllerExtension::TokenAuthentication extend ActiveSupport::Concern + protected + def token @token ||= authenticate_with_http_token do |token, options| Token.find_by_token(token) diff --git a/app/controllers/v1/configs_controller.rb b/app/controllers/v1/configs_controller.rb index 0b2a64a..accdf5a 100644 --- a/app/controllers/v1/configs_controller.rb +++ b/app/controllers/v1/configs_controller.rb @@ -22,7 +22,7 @@ class V1::ConfigsController < ApiController protected def service_paths - Hash[SERVICES.map{|k,v| [k,"/1/configs/#{str}"] } ] + Hash[SERVICES.map{|k,v| [k,"/1/configs/#{v}"] } ] end def sanitize_filename diff --git a/config/initializers/add_controller_methods.rb b/config/initializers/add_controller_methods.rb index 03e8393..f107544 100644 --- a/config/initializers/add_controller_methods.rb +++ b/config/initializers/add_controller_methods.rb @@ -2,4 +2,5 @@ ActiveSupport.on_load(:application_controller) do include ControllerExtension::Authentication include ControllerExtension::TokenAuthentication include ControllerExtension::Flash + include ControllerExtension::Errors end diff --git a/features/config.feature b/features/config.feature index 6e6c429..066d4c4 100644 --- a/features/config.feature +++ b/features/config.feature @@ -29,10 +29,7 @@ Feature: Download Provider Configuration Scenario: Missing provider config When I send a GET request to "/provider.json" Then the response status should be "404" - And the response should be: - """ - {"error": "not found"} - """ + And the response should have ".error" with the text "not_found" Scenario: Fetch list of available configs When I send a GET request to "/1/configs.json" diff --git a/features/support/hooks.rb b/features/support/hooks.rb index 360f231..19928d8 100644 --- a/features/support/hooks.rb +++ b/features/support/hooks.rb @@ -1,6 +1,18 @@ -After('@tempfile') do +After '@tempfile' do if @tempfile @tempfile.close @tempfile.unlink end end + +After do |scenario| + if scenario.failed? + logfile_path = Rails.root + 'tmp' + logfile_path += "#{scenario.title.gsub(/\s/, '_')}.log" + File.open(logfile_path, 'w') do |test_log| + test_log.puts scenario.title + test_log.puts "=========================" + test_log.puts `tail log/test.log -n 200` + end + end +end -- cgit v1.2.3 From e8a3df62d14c8dd775811f4af885cf7e76d5d3f6 Mon Sep 17 00:00:00 2001 From: Azul Date: Thu, 17 Jul 2014 11:18:57 +0200 Subject: clean up error assertions in tests We're not testing the redirects anymore. But the error messages should be pretty clear already. We can start testing redirects again once we redirect to different places for different actions. --- app/controllers/sessions_controller.rb | 3 +-- test/integration/api/smtp_cert_test.rb | 2 +- test/integration/api/srp_test.rb | 1 - test/support/api_integration_test.rb | 5 ----- test/support/assert_responses.rb | 19 +++++++++++++++++++ test/support/auth_test_helper.rb | 20 -------------------- test/support/rack_test.rb | 11 +---------- 7 files changed, 22 insertions(+), 39 deletions(-) diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 4818191..66eba40 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -19,8 +19,7 @@ class SessionsController < ApplicationController # Warden will catch all 401s and run this instead: # def unauthenticated - render json: {error: t(:not_authorized_login)}, - status: :unauthorized + login_required end # diff --git a/test/integration/api/smtp_cert_test.rb b/test/integration/api/smtp_cert_test.rb index aee52cf..b1bfd43 100644 --- a/test/integration/api/smtp_cert_test.rb +++ b/test/integration/api/smtp_cert_test.rb @@ -42,7 +42,7 @@ class SmtpCertTest < ApiIntegrationTest test "fetching smtp certs requires email account" do login post '/1/smtp_cert', {}, RACK_ENV - assert_json_response error: I18n.t(:not_authorized) + assert_access_denied end test "no anonymous smtp certs" do diff --git a/test/integration/api/srp_test.rb b/test/integration/api/srp_test.rb index 26adc8c..946450e 100644 --- a/test/integration/api/srp_test.rb +++ b/test/integration/api/srp_test.rb @@ -1,5 +1,4 @@ class SrpTest < RackTest - include AssertResponses teardown do if @user diff --git a/test/support/api_integration_test.rb b/test/support/api_integration_test.rb index ccf7066..bd10f11 100644 --- a/test/support/api_integration_test.rb +++ b/test/support/api_integration_test.rb @@ -14,11 +14,6 @@ class ApiIntegrationTest < ActionDispatch::IntegrationTest @token.save end - def assert_login_required - assert_equal 401, get_response.status - assert_json_response error: I18n.t(:not_authorized_login) - end - teardown do if @user && @user.persisted? Identity.destroy_all_for @user diff --git a/test/support/assert_responses.rb b/test/support/assert_responses.rb index 19c2768..1c9d49d 100644 --- a/test/support/assert_responses.rb +++ b/test/support/assert_responses.rb @@ -55,6 +55,25 @@ module AssertResponses get_response.headers["Content-Disposition"] end + def assert_login_required + assert_error_response :not_authorized_login, :unauthorized + end + + def assert_access_denied + assert_error_response :not_authorized, :forbidden + end + + def assert_error_response(key, status=nil) + message = I18n.t(key) + if content_type == 'application/json' + status ||= :unprocessable_entity + assert_json_response('error' => key.to_s, 'message' => message) + assert_response status + else + assert_equal({:alert => message}, flash.to_hash) + end + end + end class ::ActionController::TestCase diff --git a/test/support/auth_test_helper.rb b/test/support/auth_test_helper.rb index 79d07d6..7af3341 100644 --- a/test/support/auth_test_helper.rb +++ b/test/support/auth_test_helper.rb @@ -19,26 +19,6 @@ module AuthTestHelper return @current_user end - def assert_login_required - assert_error_response :not_authorized_login, :unauthorized, login_url - end - - def assert_access_denied - assert_error_response :not_authorized, :forbidden, home_url - end - - def assert_error_response(message, status=nil, redirect=nil) - message = I18n.t(message) if message.is_a? Symbol - if @response.content_type == 'application/json' - status ||= :unprocessable_entity - assert_json_response('error' => message) - assert_response status - else - assert_equal({:alert => message}, flash.to_hash) - assert_redirected_to redirect - end - end - def assert_access_granted assert flash[:alert].blank?, "expected to have access but there was a flash alert" diff --git a/test/support/rack_test.rb b/test/support/rack_test.rb index 83adf6c..2c9fa9a 100644 --- a/test/support/rack_test.rb +++ b/test/support/rack_test.rb @@ -3,6 +3,7 @@ require_relative 'assert_responses' class RackTest < ActiveSupport::TestCase include Rack::Test::Methods include Warden::Test::Helpers + include AssertResponses CONFIG_RU = (Rails.root + 'config.ru').to_s OUTER_APP = Rack::Builder.parse_file(CONFIG_RU).first @@ -11,16 +12,6 @@ class RackTest < ActiveSupport::TestCase OUTER_APP end - def assert_access_denied - assert_json_response('error' => I18n.t(:not_authorized)) - assert_response :forbidden - end - - def assert_login_required - assert_json_response('error' => I18n.t(:not_authorized_login)) - assert_response :unauthorized - end - # inspired by rails 4 # -> actionpack/lib/action_dispatch/testing/assertions/response.rb def assert_response(type, message = nil) -- cgit v1.2.3 From d19e748ab10c0f119a84a0d7c9a1560e246b7505 Mon Sep 17 00:00:00 2001 From: Azul Date: Thu, 17 Jul 2014 11:54:51 +0200 Subject: make sure i18n key can be found (cascade) Also reformated long haml lines some. You can add a linebreak after a comma. --- .../support/app/views/tickets/_new_comment_form.html.haml | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/engines/support/app/views/tickets/_new_comment_form.html.haml b/engines/support/app/views/tickets/_new_comment_form.html.haml index 711421d..f285b8b 100644 --- a/engines/support/app/views/tickets/_new_comment_form.html.haml +++ b/engines/support/app/views/tickets/_new_comment_form.html.haml @@ -4,10 +4,17 @@ = simple_form_for @ticket, :html => {:class => 'slim'} do |f| = hidden_ticket_fields = f.simple_fields_for :comments, @comment, :wrapper => :none, :html => {:class => 'slim'} do |c| - = c.input :body, :label => false, :as => :text, :input_html => {:class => "full-width", :rows=> 5} + = c.input :body, :label => false, :as => :text, + :input_html => {:class => "full-width", :rows=> 5} - if admin? - = c.input :private, :as => :boolean, :label => false, :inline_label => true - = f.button :loading, t(".post_reply"), class: 'btn-primary', value: 'post_reply' + = c.input :private, + :as => :boolean, + :label => false, + :inline_label => true + = f.button :loading, t(".post_reply", cascade: true), + class: 'btn-primary', + value: 'post_reply' - if logged_in? && @ticket.is_open - = f.button :loading, t(".reply_and_close"), value: 'reply_and_close' + = f.button :loading, t(".reply_and_close", cascade: true), + value: 'reply_and_close' = btn t(".cancel"), auto_tickets_path -- cgit v1.2.3 From e86cccb4b89540f3bd403110d051b2723be781b9 Mon Sep 17 00:00:00 2001 From: Azul Date: Thu, 17 Jul 2014 11:55:31 +0200 Subject: cuke: drop jsonpath, use simple keys instead Also fixed the test for login_required --- Gemfile | 1 - Gemfile.lock | 3 --- features/config.feature | 2 +- features/step_definitions/api_steps.rb | 24 ++++++++++-------------- features/unauthenticated.feature | 6 ++---- 5 files changed, 13 insertions(+), 23 deletions(-) diff --git a/Gemfile b/Gemfile index 93f34e0..62ba3e8 100644 --- a/Gemfile +++ b/Gemfile @@ -58,7 +58,6 @@ group :test do # we use cucumber to document and test the api gem 'cucumber-rails', require: false - gem 'jsonpath', require: false end group :test, :development do diff --git a/Gemfile.lock b/Gemfile.lock index a286d6f..38a8793 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -144,8 +144,6 @@ GEM railties (>= 3.0, < 5.0) thor (>= 0.14, < 2.0) json (1.8.1) - jsonpath (0.5.6) - multi_json kaminari (0.13.0) actionpack (>= 3.0.0) activesupport (>= 3.0.0) @@ -278,7 +276,6 @@ DEPENDENCIES i18n-missing_translations jquery-rails json - jsonpath kaminari (= 0.13.0) launchy leap_web_billing! diff --git a/features/config.feature b/features/config.feature index 066d4c4..6adaed9 100644 --- a/features/config.feature +++ b/features/config.feature @@ -29,7 +29,7 @@ Feature: Download Provider Configuration Scenario: Missing provider config When I send a GET request to "/provider.json" Then the response status should be "404" - And the response should have ".error" with the text "not_found" + And the response should have "error" with "not_found" Scenario: Fetch list of available configs When I send a GET request to "/1/configs.json" diff --git a/features/step_definitions/api_steps.rb b/features/step_definitions/api_steps.rb index 3a24d68..a4f369c 100644 --- a/features/step_definitions/api_steps.rb +++ b/features/step_definitions/api_steps.rb @@ -1,5 +1,3 @@ -require 'jsonpath' - if defined?(Rack) # Monkey patch Rack::MockResponse to work properly with response debugging @@ -76,39 +74,37 @@ Then /^the response status should be "([^"]*)"$/ do |status| end end -Then /^the response should (not)?\s?have "([^"]*)"$/ do |negative, json_path| +Then /^the response should (not)?\s?have "([^"]*)"$/ do |negative, key| json = JSON.parse(last_response.body) - results = JsonPath.new(json_path).on(json).to_a.map(&:to_s) if self.respond_to?(:should) if negative.present? - results.should be_empty + json[key].should be_blank else - results.should_not be_empty + json[key].should be_present end else if negative.present? - assert results.empty? + assert json[key].blank? else - assert !results.empty? + assert json[key].present? end end end -Then /^the response should (not)?\s?have "([^"]*)" with the text "([^"]*)"$/ do |negative, json_path, text| +Then /^the response should (not)?\s?have "([^"]*)" with(?: the text)? "([^"]*)"$/ do |negative, key, text| json = JSON.parse(last_response.body) - results = JsonPath.new(json_path).on(json).to_a.map(&:to_s) if self.respond_to?(:should) if negative.present? - results.should_not include(text) + json[key].should_not == text else - results.should include(text) + results.should == text end else if negative.present? - assert !results.include?(text) + assert ! json[key] == text else - assert results.include?(text) + assert_equal text, json[key] end end end diff --git a/features/unauthenticated.feature b/features/unauthenticated.feature index b810bea..120274b 100644 --- a/features/unauthenticated.feature +++ b/features/unauthenticated.feature @@ -24,8 +24,6 @@ Feature: Unauthenticated API endpoints Scenario: Authentication required for all other API endpoints When I send a GET request to "/1/configs" Then the response status should be "401" - And the response should be: - """ - {"error": "Please log in to perform that action."} - """ + And the response should have "error" with "not_authorized_login" + And the response should have "message" -- cgit v1.2.3