Commit e9a853378d9311740f61ef2037486d0135cfaf3c
Exists in
theme-brasil-digital-from-staging
and in
9 other branches
Merge branch 'login-captcha' into staging
Showing
10 changed files
with
217 additions
and
1 deletions
Show diff stats
| @@ -0,0 +1,30 @@ | @@ -0,0 +1,30 @@ | ||
| 1 | +class Noosfero::API::CaptchaSessionStore | ||
| 2 | + | ||
| 3 | + attr_accessor :data | ||
| 4 | + attr_reader :private_token | ||
| 5 | + | ||
| 6 | + def self.create | ||
| 7 | + key = SecureRandom.hex | ||
| 8 | + store = Noosfero::API::CaptchaSessionStore.new(key) | ||
| 9 | + Rails.cache.write(store.private_token, store, expires_in: 300) | ||
| 10 | + return store | ||
| 11 | + end | ||
| 12 | + | ||
| 13 | + def initialize(key) | ||
| 14 | + @private_token = key | ||
| 15 | + end | ||
| 16 | + | ||
| 17 | + def self.get(key) | ||
| 18 | + Rails.cache.fetch(key) | ||
| 19 | + end | ||
| 20 | + | ||
| 21 | + def store | ||
| 22 | + Rails.cache.write(@private_token, self) | ||
| 23 | + end | ||
| 24 | + | ||
| 25 | + def destroy | ||
| 26 | + Rails.cache.delete(@private_token) | ||
| 27 | + end | ||
| 28 | + | ||
| 29 | + | ||
| 30 | +end |
lib/noosfero/api/helpers.rb
| @@ -21,6 +21,16 @@ require 'grape' | @@ -21,6 +21,16 @@ require 'grape' | ||
| 21 | plugins | 21 | plugins |
| 22 | end | 22 | end |
| 23 | 23 | ||
| 24 | + def current_tmp_user | ||
| 25 | + private_token = (params[PRIVATE_TOKEN_PARAM] || headers['Private-Token']).to_s | ||
| 26 | + @current_tmp_user = Noosfero::API::CaptchaSessionStore.get(private_token) | ||
| 27 | + @current_tmp_user | ||
| 28 | + end | ||
| 29 | + | ||
| 30 | + def logout_tmp_user | ||
| 31 | + @current_tmp_user = nil | ||
| 32 | + end | ||
| 33 | + | ||
| 24 | def current_user | 34 | def current_user |
| 25 | private_token = (params[PRIVATE_TOKEN_PARAM] || headers['Private-Token']).to_s | 35 | private_token = (params[PRIVATE_TOKEN_PARAM] || headers['Private-Token']).to_s |
| 26 | @current_user ||= User.find_by_private_token(private_token) | 36 | @current_user ||= User.find_by_private_token(private_token) |
| @@ -257,6 +267,13 @@ require 'grape' | @@ -257,6 +267,13 @@ require 'grape' | ||
| 257 | unauthorized! unless current_user | 267 | unauthorized! unless current_user |
| 258 | end | 268 | end |
| 259 | 269 | ||
| 270 | + # Allows the anonymous captcha user authentication | ||
| 271 | + # to pass the check. Used by the articles/vote to allow | ||
| 272 | + # the vote without login | ||
| 273 | + def authenticate_allow_captcha! | ||
| 274 | + unauthorized! unless current_tmp_user || current_user | ||
| 275 | + end | ||
| 276 | + | ||
| 260 | # Checks the occurrences of uniqueness of attributes, each attribute must be present in the params hash | 277 | # Checks the occurrences of uniqueness of attributes, each attribute must be present in the params hash |
| 261 | # or a Bad Request error is invoked. | 278 | # or a Bad Request error is invoked. |
| 262 | # | 279 | # |
| @@ -331,6 +348,8 @@ require 'grape' | @@ -331,6 +348,8 @@ require 'grape' | ||
| 331 | 348 | ||
| 332 | def set_session_cookie | 349 | def set_session_cookie |
| 333 | cookies['_noosfero_api_session'] = { value: @current_user.private_token, httponly: true } if @current_user.present? | 350 | cookies['_noosfero_api_session'] = { value: @current_user.private_token, httponly: true } if @current_user.present? |
| 351 | + # Set also the private_token for the current_tmp_user | ||
| 352 | + cookies['_noosfero_api_session'] = { value: @current_tmp_user.private_token, httponly: true } if @current_tmp_user.present? | ||
| 334 | end | 353 | end |
| 335 | 354 | ||
| 336 | def setup_multitenancy | 355 | def setup_multitenancy |
lib/noosfero/api/session.rb
| @@ -4,6 +4,22 @@ module Noosfero | @@ -4,6 +4,22 @@ module Noosfero | ||
| 4 | module API | 4 | module API |
| 5 | class Session < Grape::API | 5 | class Session < Grape::API |
| 6 | 6 | ||
| 7 | + ################################ | ||
| 8 | + # => Login with captcha only | ||
| 9 | + # This method will attempt to login the user using only the captcha. | ||
| 10 | + # To do this, we generate a temporary in-memory user and generate a private | ||
| 11 | + # token to it. | ||
| 12 | + ################################ | ||
| 13 | + post "/login-captcha" do | ||
| 14 | + remote_ip = (request.respond_to?(:remote_ip) && request.remote_ip) || (env && env['REMOTE_ADDR']) | ||
| 15 | + # test_captcha will render_api_error! and exit in case of any problem | ||
| 16 | + # this return is just to improve the clarity of the execution path | ||
| 17 | + return unless test_captcha(remote_ip, params, environment) | ||
| 18 | + ## Creates and caches a captcha session store | ||
| 19 | + store = Noosfero::API::CaptchaSessionStore.create | ||
| 20 | + { "private_token" => "#{store.private_token}" } | ||
| 21 | + end | ||
| 22 | + | ||
| 7 | # Login to get token | 23 | # Login to get token |
| 8 | # | 24 | # |
| 9 | # Parameters: | 25 | # Parameters: |
lib/noosfero/api/v1/articles.rb
| @@ -133,7 +133,8 @@ module Noosfero | @@ -133,7 +133,8 @@ module Noosfero | ||
| 133 | named 'ArticleVote' | 133 | named 'ArticleVote' |
| 134 | end | 134 | end |
| 135 | post ':id/vote' do | 135 | post ':id/vote' do |
| 136 | - authenticate! | 136 | + ## The vote api should allow regular login or with captcha |
| 137 | + authenticate_allow_captcha! | ||
| 137 | value = (params[:value] || 1).to_i | 138 | value = (params[:value] || 1).to_i |
| 138 | # FIXME verify allowed values | 139 | # FIXME verify allowed values |
| 139 | render_api_error!('Vote value not allowed', 400) unless [-1, 1].include?(value) | 140 | render_api_error!('Vote value not allowed', 400) unless [-1, 1].include?(value) |
| @@ -0,0 +1,15 @@ | @@ -0,0 +1,15 @@ | ||
| 1 | +class SerproCaptchaPlugin < Noosfero::Plugin | ||
| 2 | + | ||
| 3 | + def self.plugin_name | ||
| 4 | + _('Serpro captcha plugin') | ||
| 5 | + end | ||
| 6 | + | ||
| 7 | + def self.plugin_description | ||
| 8 | + _("Provide a plugin to Serpro's captcha infrastructure.") | ||
| 9 | + end | ||
| 10 | + | ||
| 11 | + def self.api_mount_points | ||
| 12 | + [SerproCaptchaPlugin::API ] | ||
| 13 | + end | ||
| 14 | + | ||
| 15 | +end |
| @@ -0,0 +1,16 @@ | @@ -0,0 +1,16 @@ | ||
| 1 | +class SerproCaptchaPlugin::API < Grape::API | ||
| 2 | + | ||
| 3 | + # resource :dialoga_plugin do | ||
| 4 | + # get 'random_topics/:discussion_id' do | ||
| 5 | + # discussion = ProposalsDiscussionPlugin::Discussion.find(params[:discussion_id]) | ||
| 6 | + # | ||
| 7 | + # # render articles using Entity Article | ||
| 8 | + # present discussion.random_topics_one_by_category, :with => Noosfero::API::Entities::Article, :fields => params[:fields] | ||
| 9 | + # end | ||
| 10 | + | ||
| 11 | + get 'test_captcha' do | ||
| 12 | + present 'chegou no test_captcha do SerproCaptchaPlugin' | ||
| 13 | + end | ||
| 14 | + # end | ||
| 15 | + | ||
| 16 | +end |
| @@ -0,0 +1 @@ | @@ -0,0 +1 @@ | ||
| 1 | +require File.dirname(__FILE__) + '/../../../test/test_helper' |
| @@ -0,0 +1,73 @@ | @@ -0,0 +1,73 @@ | ||
| 1 | +require File.dirname(__FILE__) + '/test_helper' | ||
| 2 | + | ||
| 3 | +class LoginCaptchaTest < ActiveSupport::TestCase | ||
| 4 | + | ||
| 5 | + def setup() | ||
| 6 | + @environment = Environment.default | ||
| 7 | + @environment.api_captcha_settings = { | ||
| 8 | + enabled: true, | ||
| 9 | + provider: 'serpro', | ||
| 10 | + serpro_client_id: '0000000000000000', | ||
| 11 | + verify_uri: 'http://captcha.serpro.gov.br/validate', | ||
| 12 | + } | ||
| 13 | + @environment.save! | ||
| 14 | + @url = "/api/v1/login-captcha?" | ||
| 15 | + end | ||
| 16 | + | ||
| 17 | + def create_article(name) | ||
| 18 | + person = fast_create(Person, :environment_id => @environment.id) | ||
| 19 | + fast_create(Article, :profile_id => person.id, :name => name) | ||
| 20 | + end | ||
| 21 | + | ||
| 22 | + should 'not perform a vote without authentication' do | ||
| 23 | + article = create_article('Article 1') | ||
| 24 | + params = {} | ||
| 25 | + params[:value] = 1 | ||
| 26 | + | ||
| 27 | + post "/api/v1/articles/#{article.id}/vote?#{params.to_query}" | ||
| 28 | + json = JSON.parse(last_response.body) | ||
| 29 | + assert_equal 401, last_response.status | ||
| 30 | + end | ||
| 31 | + | ||
| 32 | + should 'perform login from helpers' do | ||
| 33 | + login_with_captcha | ||
| 34 | + assert_not_nil @private_token | ||
| 35 | + end | ||
| 36 | + | ||
| 37 | + | ||
| 38 | + should 'perform a vote in an article identified by id' do | ||
| 39 | + login_with_captcha | ||
| 40 | + article = create_article('Article 1') | ||
| 41 | + params[:value] = 1 | ||
| 42 | + | ||
| 43 | + post "/api/v1/articles/#{article.id}/vote?#{params.to_query}" | ||
| 44 | + json = JSON.parse(last_response.body) | ||
| 45 | + | ||
| 46 | + assert_not_equal 401, last_response.status | ||
| 47 | + assert_equal true, json['vote'] | ||
| 48 | + end | ||
| 49 | + | ||
| 50 | + should 'not follow any article' do | ||
| 51 | + login_with_captcha | ||
| 52 | + article = create_article('Article 1') | ||
| 53 | + post "/api/v1/articles/#{article.id}/follow?#{params.to_query}" | ||
| 54 | + json = JSON.parse(last_response.body) | ||
| 55 | + | ||
| 56 | + assert_equal 401, last_response.status | ||
| 57 | + end | ||
| 58 | + | ||
| 59 | + should 'not generate private token when login without captcha' do | ||
| 60 | + params = {} | ||
| 61 | + post "#{@url}#{params.to_query}" | ||
| 62 | + json = JSON.parse(last_response.body) | ||
| 63 | + assert json["private_token"].blank? | ||
| 64 | + end | ||
| 65 | + | ||
| 66 | + should 'generate private token when login with captcha' do | ||
| 67 | + json = login_with_captcha | ||
| 68 | + ret = json["private_token"] | ||
| 69 | + assert !ret.blank? | ||
| 70 | + assert ret == @private_token | ||
| 71 | + end | ||
| 72 | + | ||
| 73 | +end | ||
| 0 | \ No newline at end of file | 74 | \ No newline at end of file |
test/unit/api/test_helper.rb
| @@ -8,6 +8,38 @@ class ActiveSupport::TestCase | @@ -8,6 +8,38 @@ class ActiveSupport::TestCase | ||
| 8 | Noosfero::API::API | 8 | Noosfero::API::API |
| 9 | end | 9 | end |
| 10 | 10 | ||
| 11 | + def login_with_captcha | ||
| 12 | + json = do_login_captcha_from_api | ||
| 13 | + @private_token = json["private_token"] | ||
| 14 | + @params = { "private_token" => @private_token} | ||
| 15 | + json | ||
| 16 | + end | ||
| 17 | + | ||
| 18 | + ## Performs a login using the session.rb but mocking the | ||
| 19 | + ## real HTTP request to validate the captcha. | ||
| 20 | + def do_login_captcha_from_api | ||
| 21 | + # Request mocking | ||
| 22 | + #Net::HTTP::Post Mock | ||
| 23 | + request = mock | ||
| 24 | + #Net::HTTP Mock | ||
| 25 | + http = mock | ||
| 26 | + uri = URI(environment.api_captcha_settings[:verify_uri]) | ||
| 27 | + Net::HTTP.expects(:new).with(uri.host, uri.port).returns(http) | ||
| 28 | + Net::HTTP::Post.expects(:new).with(uri.path).returns(request) | ||
| 29 | + | ||
| 30 | + # Captcha required codes | ||
| 31 | + request.stubs(:body=).with("0000000000000000&4324343&4030320") | ||
| 32 | + http.stubs(:request).with(request).returns(http) | ||
| 33 | + | ||
| 34 | + # Captcha validation success !! | ||
| 35 | + http.stubs(:body).returns("1") | ||
| 36 | + | ||
| 37 | + params = {:txtToken_captcha_serpro_gov_br => '4324343', :captcha_text => '4030320'} | ||
| 38 | + post "#{@url}#{params.to_query}" | ||
| 39 | + json = JSON.parse(last_response.body) | ||
| 40 | + json | ||
| 41 | + end | ||
| 42 | + | ||
| 11 | def login_api | 43 | def login_api |
| 12 | @environment = Environment.default | 44 | @environment = Environment.default |
| 13 | @user = User.create!(:login => 'testapi', :password => 'testapi', :password_confirmation => 'testapi', :email => 'test@test.org', :environment => @environment) | 45 | @user = User.create!(:login => 'testapi', :password => 'testapi', :password_confirmation => 'testapi', :email => 'test@test.org', :environment => @environment) |