diff options
Diffstat (limited to 'users')
36 files changed, 764 insertions, 183 deletions
diff --git a/users/app/assets/javascripts/srp b/users/app/assets/javascripts/srp -Subproject d6a78049f3356d9d645143362eca74434410bf6 +Subproject fff770a866b44abce6fe0fc5d5ffde034225436 diff --git a/users/app/assets/javascripts/users.js.coffee b/users/app/assets/javascripts/users.js.coffee index 160a7f0..76a6d79 100644 --- a/users/app/assets/javascripts/users.js.coffee +++ b/users/app/assets/javascripts/users.js.coffee @@ -1,41 +1,35 @@ -# Place all the behaviors and hooks related to the matching controller here. -# All this logic will automatically be available in application.js. -# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/ -# +preventDefault = (event) -> + event.preventDefault() -validate_password = (event) -> +srp.session = new srp.Session() +srp.signedUp = -> + srp.login - password = $('#srp_password').val() - confirmation = $('#srp_password_confirmation').val() - login = $('#srp_username').val() +srp.loggedIn = -> + window.location = '/' - if password != confirmation - alert "Password and Confirmation do not match!" - $('#srp_password').focus() - return false - if password == login - alert "Password and Login may not match!" - $('#srp_password').focus() - return false - if password.length < 8 - alert "Password needs to be at least 8 characters long!" - $('#srp_password').focus() - return false - - return true - +#// TODO: not sure this is what we want. +srp.updated = -> + window.location = '/' -insert_verifier = (event) -> - # TODO: verify password confimation - srp = new SRP - salt = srp.session.getSalt() - $('#srp_salt').val(salt) - $('#srp_password_verifier').val(srp.session.getV().toString(16)) - # clear the password so we do not submit it - $('#srp_password').val('cleared out - use verifier instead') - $('#srp_password_confirmation').val('using srp - store verifier') +srp.error = (message) -> + if $.isPlainObject(message) && message.errors + for field, error of message.errors + element = $('form input[name$="['+field+']"]') + next unless element + element.trigger('element:validate:fail.ClientSideValidations', error).data('valid', false) + else + alert(message) + +pollUsers = (query, process) -> + $.get( "/users.json", query: query).done(process) $(document).ready -> - $('#new_user').submit validate_password - $('#new_user').submit insert_verifier + $('#new_user').submit preventDefault + $('#new_user').submit srp.signup + $('#new_session').submit preventDefault + $('#new_session').submit srp.login + $('.user.form.edit').submit srp.update + $('.user.form.edit').submit preventDefault + $('.user.typeahead').typeahead({source: pollUsers}); diff --git a/users/app/controllers/controller_extension/authentication.rb b/users/app/controllers/controller_extension/authentication.rb new file mode 100644 index 0000000..6ac7a5b --- /dev/null +++ b/users/app/controllers/controller_extension/authentication.rb @@ -0,0 +1,38 @@ +module ControllerExtension::Authentication + extend ActiveSupport::Concern + + private + + included do + helper_method :current_user, :logged_in?, :admin? + end + + def authentication_errors + return unless errors = warden.winning_strategy.try(:message) + errors.inject({}) do |translated,err| + translated[err.first] = I18n.t(err.last) + translated + end + end + + def logged_in? + !!current_user + end + + def authorize + access_denied unless logged_in? + end + + def access_denied + redirect_to login_url, :alert => "Not authorized" + end + + def admin? + current_user && current_user.is_admin? + end + + def authorize_admin + access_denied unless admin? + end + +end diff --git a/users/app/controllers/sessions_controller.rb b/users/app/controllers/sessions_controller.rb index 284c0e2..bc910b5 100644 --- a/users/app/controllers/sessions_controller.rb +++ b/users/app/controllers/sessions_controller.rb @@ -3,33 +3,24 @@ class SessionsController < ApplicationController skip_before_filter :verify_authenticity_token def new + @session = Session.new + if authentication_errors + @errors = authentication_errors + render :status => 422 + end end def create - @user = User.find_by_param(params[:login]) - session[:handshake] = @user.initialize_auth(params['A'].hex) - User.current = @user #? - render :json => session[:handshake] - rescue RECORD_NOT_FOUND - render :json => {:errors => {:login => ["unknown user"]}} + authenticate! end def update - # TODO: validate the id belongs to the session - @user = User.find_by_param(params[:id]) - @srp_session = session.delete(:handshake) - @srp_session.authenticate!(params[:client_auth].hex) - session[:user_id] = @user.id - User.current = @user #? - render :json => @srp_session - rescue WRONG_PASSWORD - session[:handshake] = nil - render :json => {:errors => {"password" => ["wrong password"]}} + authenticate! + render :json => session.delete(:handshake) end def destroy - session[:user_id] = nil - User.current = nil #? + logout redirect_to root_path end end diff --git a/users/app/controllers/users_controller.rb b/users/app/controllers/users_controller.rb index 82d2eac..cffc8c6 100644 --- a/users/app/controllers/users_controller.rb +++ b/users/app/controllers/users_controller.rb @@ -1,18 +1,48 @@ class UsersController < ApplicationController - skip_before_filter :verify_authenticity_token + skip_before_filter :verify_authenticity_token, :only => [:create] + + before_filter :fetch_user, :only => [:edit, :update, :destroy] + before_filter :authorize_admin, :only => [:index] respond_to :json, :html + def index + if params[:query] + @users = User.by_login.startkey(params[:query]).endkey(params[:query].succ) + else + @users = User.by_created_at.descending + end + @users = @users.limit(10) + respond_with @users.map(&:login).sort + end + def new @user = User.new end def create - @user = User.create!(params[:user]) - respond_with(@user, :location => root_url, :notice => "Signed up!") - rescue VALIDATION_FAILED => e - @user = e.document - respond_with(@user, :location => new_user_path) + @user = User.create(params[:user]) + respond_with @user + end + + def edit + end + + def update + @user.update_attributes(params[:user]) + respond_with @user + end + + def destroy + @user.destroy + redirect_to admin? ? users_path : login_path + end + + protected + + def fetch_user + @user = User.find_by_param(params[:id]) + access_denied unless admin? or (@user == current_user) end end diff --git a/users/app/models/session.rb b/users/app/models/session.rb new file mode 100644 index 0000000..a9fdb1b --- /dev/null +++ b/users/app/models/session.rb @@ -0,0 +1,34 @@ +class Session < SRP::Session + include ActiveModel::Validations + + attr_accessor :login + + validates :login, + :presence => true, + :format => { :with => /\A[A-Za-z\d_]+\z/, + :message => "Only letters, digits and _ allowed" } + + def initialize(user = nil, aa = nil) + super(user, aa) if user + end + + def persisted? + false + end + + def new_record? + true + end + + def to_model + self + end + + def to_key + [object_id] + end + + def to_param + nil + end +end diff --git a/users/app/models/user.rb b/users/app/models/user.rb index 1afb9db..325c981 100644 --- a/users/app/models/user.rb +++ b/users/app/models/user.rb @@ -9,42 +9,46 @@ class User < CouchRest::Model::Base :presence => true validates :login, - :uniqueness => true + :uniqueness => true, + :if => :serverside? validates :login, :format => { :with => /\A[A-Za-z\d_]+\z/, :message => "Only letters, digits and _ allowed" } validates :password_salt, :password_verifier, - :format => { :with => /\A[\dA-Fa-f]+\z/, - :message => "Only hex numbers allowed" } + :format => { :with => /\A[\dA-Fa-f]+\z/, :message => "Only hex numbers allowed" } + + validates :password, :presence => true, + :confirmation => true, + :format => { :with => /.{8}.*/, :message => "needs to be at least 8 characters long" } timestamps! design do view :by_login + view :by_created_at end class << self - def find_by_param(login) - return find_by_login(login) || raise(RECORD_NOT_FOUND) - end + alias_method :find_by_param, :find # valid set of attributes for testing def valid_attributes_hash { :login => "me", - :password_verifier => "1234ABC", + :password_verifier => "1234ABCD", :password_salt => "4321AB" } end end - def to_param - self.login - end + alias_method :to_param, :id def to_json(options={}) - super(options.merge(:only => ['login', 'password_salt'])) + { + :login => login, + :ok => valid? + }.to_json(options) end def initialize_auth(aa) @@ -63,11 +67,18 @@ class User < CouchRest::Model::Base login end - def self.current - Thread.current[:user] + # Since we are storing admins by login, we cannot allow admins to change their login. + def is_admin? + APP_CONFIG['admins'].include? self.login end - def self.current=(user) - Thread.current[:user] = user + + protected + def password + password_verifier end + # used as a condition for validations that are server side only + def serverside? + true + end end diff --git a/users/app/views/sessions/_admin_nav.html.haml b/users/app/views/sessions/_admin_nav.html.haml new file mode 100644 index 0000000..14dfbdc --- /dev/null +++ b/users/app/views/sessions/_admin_nav.html.haml @@ -0,0 +1,6 @@ +%a#admin-menu{"data-toggle" => "dropdown", :role => :button} + Admin +%ul.dropdown-menu{:role => "menu", "aria-labelledby" => "admin-menu"} + %li + = link_to Ticket.model_name.human(:count => ""), tickets_path, {:tabindex => -1} + = link_to User.model_name.human(:count => ""), users_path, {:tabindex => -1} diff --git a/users/app/views/sessions/_nav.html.haml b/users/app/views/sessions/_nav.html.haml new file mode 100644 index 0000000..5306d0e --- /dev/null +++ b/users/app/views/sessions/_nav.html.haml @@ -0,0 +1,13 @@ +- if logged_in? + - if admin? + %li.dropdown + = render 'sessions/admin_nav' + %li + = link_to current_user.login, edit_user_path(current_user) + %li + = link_to t(:logout), logout_path +- else + %li + = link_to t(:login), login_path + %li + = link_to t(:signup), signup_path diff --git a/users/app/views/sessions/new.html.haml b/users/app/views/sessions/new.html.haml index 39ee7bf..a04f584 100644 --- a/users/app/views/sessions/new.html.haml +++ b/users/app/views/sessions/new.html.haml @@ -1,7 +1,8 @@ -%h2=t :login -= simple_form_for :session, :url => sessions_path, :html => { :id => :new_session } do |f| - %legend=t :login_message - = f.input :login, :input_html => { :id => :srp_username } - = f.input :password, :required => true, :input_html => { :id => :srp_password } - = f.button :submit, :value => t(:login), :class => 'btn-primary' - = link_to t(:cancel), root_url, :class => :btn +.span8.offset2 + %h2=t :login + = simple_form_for @session, :validate => true, :html => { :id => :new_session, :class => 'form-horizontal' } do |f| + %legend=t :login_message + = f.input :login, :input_html => { :id => :srp_username } + = f.input :password, :required => true, :input_html => { :id => :srp_password } + = f.button :submit, :value => t(:login), :class => 'btn-primary' + = link_to t(:cancel), root_url, :class => :btn diff --git a/users/app/views/sessions/new.json.erb b/users/app/views/sessions/new.json.erb new file mode 100644 index 0000000..36154b8 --- /dev/null +++ b/users/app/views/sessions/new.json.erb @@ -0,0 +1,3 @@ +{ +"errors": <%= raw @errors.to_json %> +} diff --git a/users/app/views/users/_cancel_account.html.haml b/users/app/views/users/_cancel_account.html.haml new file mode 100644 index 0000000..41580b0 --- /dev/null +++ b/users/app/views/users/_cancel_account.html.haml @@ -0,0 +1,6 @@ +%legend + =t :cancel_account + %small You will not be able to login anymore. += link_to user_path(@user), :method => :delete, :class => "btn btn-danger" do + %i.icon-remove.icon-white + Remove my Account diff --git a/users/app/views/users/_form.html.haml b/users/app/views/users/_form.html.haml new file mode 100644 index 0000000..39e26a6 --- /dev/null +++ b/users/app/views/users/_form.html.haml @@ -0,0 +1,15 @@ +- only = local_assigns[:only] +- html = {:class => 'form-horizontal user form ' + (@user.new_record? ? 'new' : 'edit')} += simple_form_for @user, :validate => true, :format => :json, :html => html do |f| + %legend + = t(only || :signup_message) + - if !only || only == :change_login + = f.input :login, :input_html => { :id => :srp_username } + - if !only || only == :change_password + = f.input :password, :required => true, :validate => true, :input_html => { :id => :srp_password } + = f.input :password_confirmation, :required => true, :input_html => { :id => :srp_password_confirmation } + .pull-right + = f.button :submit, :class => 'btn-primary' + - unless only + = link_to t(:cancel), root_url, :class => :btn + .clearfix diff --git a/users/app/views/users/_user.html.haml b/users/app/views/users/_user.html.haml new file mode 100644 index 0000000..7db0041 --- /dev/null +++ b/users/app/views/users/_user.html.haml @@ -0,0 +1,10 @@ +%tr + %td= user.login + %td= time_ago_in_words(user.created_at) + " ago" + %td + = link_to edit_user_path(user), :class => "btn btn-mini btn-primary" do + %i.icon-edit.icon-white + Edit + = link_to user_path(user), :method => :delete, :class => "btn btn-danger btn-mini" do + %i.icon-remove.icon-white + Remove diff --git a/users/app/views/users/edit.html.haml b/users/app/views/users/edit.html.haml new file mode 100644 index 0000000..25da71a --- /dev/null +++ b/users/app/views/users/edit.html.haml @@ -0,0 +1,5 @@ +.span8.offset2 + %h2=t :settings + = render :partial => 'form', :locals => {:only => :change_login} + = render :partial => 'form', :locals => {:only => :change_password} + = render 'cancel_account' if @user == current_user diff --git a/users/app/views/users/index.html.haml b/users/app/views/users/index.html.haml new file mode 100644 index 0000000..9e6a179 --- /dev/null +++ b/users/app/views/users/index.html.haml @@ -0,0 +1,17 @@ +.page-header + %h1= User.model_name.human(:count =>User.count) +.row + .span8 + %h2= params[:query] ? "Users starting with '#{params[:query]}'" : "Last users who signed up" + %table.table.table-hover + %tr + %th Login + %th Created + %th Action + = render @users.all + .span4 + %h4 Find user + = form_tag users_path, :method => :get, :class => "form-search" do + .input-append + = text_field_tag :query, "", :class => "user typeahead span2 search-query", :autocomplete => :off + %button.btn{:type => :submit} Search diff --git a/users/app/views/users/new.html.haml b/users/app/views/users/new.html.haml index f6ece3a..c1c4208 100644 --- a/users/app/views/users/new.html.haml +++ b/users/app/views/users/new.html.haml @@ -1,10 +1,3 @@ -%h2=t :signup -= simple_form_for @user do |f| - %legend=t :signup_message - = f.input :login, :input_html => { :id => :srp_username } - = f.input :password, :required => true, :input_html => { :id => :srp_password } - = f.input :password_confirmation, :required => true, :input_html => { :id => :srp_password_confirmation } - = f.input :password_verifier, :as => :hidden, :input_html => { :id => :srp_password_verifier } - = f.input :password_salt, :as => :hidden, :input_html => { :id => :srp_salt } - = f.button :submit, :value => t(:signup), :class => 'btn-primary' - = link_to t(:cancel), root_url, :class => :btn +.span8.offset2 + %h2=t :signup + = render 'form' diff --git a/users/config/initializers/add_controller_methods.rb b/users/config/initializers/add_controller_methods.rb new file mode 100644 index 0000000..2579176 --- /dev/null +++ b/users/config/initializers/add_controller_methods.rb @@ -0,0 +1,3 @@ +ActiveSupport.on_load(:application_controller) do + include ControllerExtension::Authentication +end diff --git a/users/config/initializers/warden.rb b/users/config/initializers/warden.rb new file mode 100644 index 0000000..45feb6c --- /dev/null +++ b/users/config/initializers/warden.rb @@ -0,0 +1,7 @@ +Rails.configuration.middleware.use RailsWarden::Manager do |config| + config.default_strategies :secure_remote_password + config.failure_app = SessionsController +end + +RailsWarden.unauthenticated_action = :new + diff --git a/users/config/locales/en.yml b/users/config/locales/en.yml new file mode 100644 index 0000000..1260494 --- /dev/null +++ b/users/config/locales/en.yml @@ -0,0 +1,14 @@ +en: + signup: "Sign up" + signup_message: "Please create an account." + cancel: "Cancel" + login: "Login" + login_message: "Please login with your account." + wrong_password: "wrong password" + user_not_found: "could not be found" + + activemodel: + models: + user: + one: User + other: "%{count} Users" diff --git a/users/config/routes.rb b/users/config/routes.rb index cfc0407..1d144b4 100644 --- a/users/config/routes.rb +++ b/users/config/routes.rb @@ -1,10 +1,10 @@ Rails.application.routes.draw do - get "log_in" => "sessions#new", :as => "log_in" - get "log_out" => "sessions#destroy", :as => "log_out" + get "login" => "sessions#new", :as => "login" + get "logout" => "sessions#destroy", :as => "logout" resources :sessions, :only => [:new, :create, :update, :destroy] - get "sign_up" => "users#new", :as => "sign_up" - resources :users, :only => [:new, :create] + get "signup" => "users#new", :as => "signup" + resources :users end diff --git a/users/leap_web_users.gemspec b/users/leap_web_users.gemspec index f64a76a..0682a99 100644 --- a/users/leap_web_users.gemspec +++ b/users/leap_web_users.gemspec @@ -17,5 +17,6 @@ Gem::Specification.new do |s| s.add_dependency "leap_web_core", LeapWeb::VERSION - s.add_dependency "ruby-srp", "~> 0.1.1" + s.add_dependency "ruby-srp", "~> 0.1.4" + s.add_dependency "rails_warden" end diff --git a/users/lib/leap_web_users/engine.rb b/users/lib/leap_web_users/engine.rb index 9b7545e..7033576 100644 --- a/users/lib/leap_web_users/engine.rb +++ b/users/lib/leap_web_users/engine.rb @@ -1,8 +1,12 @@ # thou shall require all your dependencies in an engine. require "leap_web_core" require "leap_web_core/ui_dependencies" +require "rails_warden" require "ruby-srp" +require "warden/session_serializer" +require "warden/strategies/secure_remote_password" + module LeapWebUsers class Engine < ::Rails::Engine diff --git a/users/lib/warden/session_serializer.rb b/users/lib/warden/session_serializer.rb new file mode 100644 index 0000000..81d7076 --- /dev/null +++ b/users/lib/warden/session_serializer.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/users/lib/warden/strategies/secure_remote_password.rb b/users/lib/warden/strategies/secure_remote_password.rb new file mode 100644 index 0000000..594e27e --- /dev/null +++ b/users/lib/warden/strategies/secure_remote_password.rb @@ -0,0 +1,58 @@ +module Warden + module Strategies + class SecureRemotePassword < Warden::Strategies::Base + + def valid? + handshake? || authentication? + end + + def authenticate! + if authentication? + validate! + else # handshake + initialize! + end + end + + protected + + def handshake? + params['A'] && params['login'] + end + + def authentication? + params['client_auth'] && session[:handshake] + end + + def validate! + user = session[:handshake].authenticate(params['client_auth'].hex) + user ? success!(user) : fail!(:password => "wrong_password") + end + + def initialize! + if user = User.find_by_login(id) + session[:handshake] = user.initialize_auth(params['A'].hex) + custom! json_response(session[:handshake]) + else + fail! :login => "user_not_found" + end + end + + def json_response(object) + [ 200, + {"Content-Type" => "application/json; charset=utf-8"}, + [object.to_json] + ] + end + + def id + params["id"] || params["login"] + end + end + end + Warden::Strategies.add :secure_remote_password, + Warden::Strategies::SecureRemotePassword + +end + + diff --git a/users/test/functional/application_controller_test.rb b/users/test/functional/application_controller_test.rb new file mode 100644 index 0000000..857bae5 --- /dev/null +++ b/users/test/functional/application_controller_test.rb @@ -0,0 +1,28 @@ +require 'test_helper' + +class ApplicationControllerTest < ActionController::TestCase + + def setup + # so we can test the effect on the response + @controller.response = @response + end + + def test_authorize_redirect + @controller.send(:authorize) + assert_access_denied + end + + def test_authorized + login + @controller.send(:authorize) + assert_access_denied(false) + end + + def test_authorize_admin + login + @current_user.expects(:is_admin?).returns(false) + @controller.send(:authorize_admin) + assert_access_denied + end + +end diff --git a/users/test/functional/helper_methods_test.rb b/users/test/functional/helper_methods_test.rb new file mode 100644 index 0000000..2b2375c --- /dev/null +++ b/users/test/functional/helper_methods_test.rb @@ -0,0 +1,39 @@ +# +# Testing and documenting the helper methods available from +# ApplicationController +# + +require 'test_helper' + +class HelperMethodsTest < ActionController::TestCase + tests ApplicationController + + # we test them right in here... + include ApplicationController._helpers + + # they all reference the controller. + def controller + @controller + end + + def test_current_user + login + assert_equal @current_user, current_user + end + + def test_logged_in + login + assert logged_in? + end + + def test_logged_out + assert !logged_in? + end + + def test_admin + login + @current_user.expects(:is_admin?).returns(bool = stub) + assert_equal bool, admin? + end + +end diff --git a/users/test/functional/sessions_controller_test.rb b/users/test/functional/sessions_controller_test.rb index b6e56a7..9df4455 100644 --- a/users/test/functional/sessions_controller_test.rb +++ b/users/test/functional/sessions_controller_test.rb @@ -1,79 +1,72 @@ require 'test_helper' +# This is a simple controller unit test. +# We're stubbing out both warden and srp. +# There's an integration test testing the full rack stack and srp class SessionsControllerTest < ActionController::TestCase - def setup + setup do + @user = stub :login => "me", :id => 123 @client_hex = 'a123' - @client_rnd = @client_hex.hex - @server_hex = 'b123' - @server_rnd = @server_hex.hex - @server_rnd_exp = 'e123'.hex - @salt = 'stub user salt' - @server_handshake = stub :aa => @client_rnd, :bb => @server_rnd, :b => @server_rnd_exp - @server_auth = 'adfe' end test "should get login screen" do + request.env['warden'].expects(:winning_strategy) get :new assert_response :success + assert_equal "text/html", response.content_type + assert_template "sessions/new" end - test "should perform handshake" do - user = stub :login => "me", :id => 123 - user.expects(:initialize_auth). - with(@client_rnd). - returns(@server_handshake) - @server_handshake.expects(:to_json). - returns({'B' => @server_hex, 'salt' => @salt}.to_json) - User.expects(:find_by_param).with(user.login).returns(user) - post :create, :login => user.login, 'A' => @client_hex - assert_equal @server_handshake, session[:handshake] + test "renders json" do + request.env['warden'].expects(:winning_strategy) + get :new, :format => :json assert_response :success - assert_json_response :B => @server_hex, :salt => @salt + assert_json_error nil end - test "should report user not found" do - unknown = "login_that_does_not_exist" - User.expects(:find_by_param).with(unknown).raises(RECORD_NOT_FOUND) - post :create, :login => unknown - assert_response :success - assert_json_response :errors => {"login" => ["unknown user"]} + test "renders warden errors" do + strategy = stub :message => {:field => :translate_me} + request.env['warden'].stubs(:winning_strategy).returns(strategy) + I18n.expects(:t).with(:translate_me).at_least_once.returns("translation stub") + get :new, :format => :json + assert_response 422 + assert_json_error :field => "translation stub" end - test "should authorize" do - session[:handshake] = @server_handshake - user = stub :login => "me", :id => 123 - @server_handshake.expects(:authenticate!). - with(@client_rnd). - returns(@server_auth) - @server_handshake.expects(:to_json). - returns({:M2 => @server_auth}.to_json) - User.expects(:find_by_param).with(user.login).returns(user) - post :update, :id => user.login, :client_auth => @client_hex - assert_nil session[:handshake] - assert_json_response :M2 => @server_auth - assert_equal user.id, session[:user_id] + # Warden takes care of parsing the params and + # rendering the response. So not much to test here. + test "should perform handshake" do + request.env['warden'].expects(:authenticate!) + # make sure we don't get a template missing error: + @controller.stubs(:render) + post :create, :login => @user.login, 'A' => @client_hex end - test "should report wrong password" do - session[:handshake] = @server_handshake - user = stub :login => "me", :id => 123 - @server_handshake.expects(:authenticate!). - with(@client_rnd). - raises(WRONG_PASSWORD) - User.expects(:find_by_param).with(user.login).returns(user) - post :update, :id => user.login, :client_auth => @client_hex + test "should authorize" do + request.env['warden'].expects(:authenticate!) + handshake = stub(:to_json => "JSON") + session[:handshake] = handshake + post :update, :id => @user.login, :client_auth => @client_hex assert_nil session[:handshake] - assert_nil session[:user_id] - assert_json_response :errors => {"password" => ["wrong password"]} + assert_response :success + assert_equal handshake.to_json, @response.body end - test "logout should reset sessions user_id" do - session[:user_id] = "set" + test "logout should reset warden user" do + expect_warden_logout delete :destroy - assert_nil session[:user_id] assert_response :redirect assert_redirected_to root_url end + def expect_warden_logout + raw = mock('raw session') do + expects(:inspect) + end + request.env['warden'].expects(:raw_session).returns(raw) + request.env['warden'].expects(:logout) + end + + end diff --git a/users/test/functional/users_controller_test.rb b/users/test/functional/users_controller_test.rb index 1cb28a6..939d105 100644 --- a/users/test/functional/users_controller_test.rb +++ b/users/test/functional/users_controller_test.rb @@ -1,33 +1,132 @@ require 'test_helper' class UsersControllerTest < ActionController::TestCase + include StubRecordHelper + test "should get new" do get :new + assert_equal User, assigns(:user).class assert_response :success end test "should create new user" do - params = User.valid_attributes_hash - user = stub params.merge(:id => 123) - params.stringify_keys! - User.expects(:create!).with(params).returns(user) - post :create, :user => params + user = stub_record User + User.expects(:create).with(user.params).returns(user) + + post :create, :user => user.params, :format => :json + assert_nil session[:user_id] - assert_response :redirect - assert_redirected_to root_url + assert_json_response user + assert_response :success end test "should redirect to signup form on failed attempt" do params = User.valid_attributes_hash.slice(:login) user = User.new(params) params.stringify_keys! - User.expects(:create!).with(params).raises(VALIDATION_FAILED.new(user)) - post :create, :user => params - assert_nil session[:user_id] + assert !user.valid? + User.expects(:create).with(params).returns(user) + + post :create, :user => params, :format => :json + + assert_json_error user.errors.messages + assert_response 422 + end + + test "should get edit view" do + user = find_record User + + login user + get :edit, :id => user.id + assert_equal user, assigns[:user] + end + + test "should process updated params" do + user = find_record User + user.expects(:update_attributes).with(user.params).returns(true) + + login user + put :update, :user => user.params, :id => user.id, :format => :json + + assert_equal user, assigns[:user] + assert_response 204 + assert_equal " ", @response.body + end + + test "admin can update user" do + user = find_record User + user.expects(:update_attributes).with(user.params).returns(true) + + login :is_admin? => true + put :update, :user => user.params, :id => user.id, :format => :json + + assert_equal user, assigns[:user] + assert_response 204 + assert_equal " ", @response.body + end + + test "admin can destroy user" do + user = find_record User + user.expects(:destroy) + + login :is_admin? => true + delete :destroy, :id => user.id + + assert_response :redirect + assert_redirected_to users_path + end + + test "user can cancel account" do + user = find_record User + user.expects(:destroy) + + login user + delete :destroy, :id => @current_user.id + assert_response :redirect - assert_redirected_to new_user_path + assert_redirected_to login_path + end + + test "non-admin can't destroy user" do + user = stub_record User + + login + delete :destroy, :id => user.id + + assert_access_denied + end + + test "admin can list users" do + login :is_admin? => true + get :index + + assert_response :success + assert assigns(:users) + end + + test "non-admin can't list users" do + login + get :index + + assert_access_denied + end + + test "admin can autocomplete users" do + login :is_admin? => true + get :index, :format => :json + + assert_response :success + assert assigns(:users) + end + + test "admin can search users" do + login :is_admin? => true + get :index, :query => "a" + + assert_response :success + assert assigns(:users) end end diff --git a/users/test/integration/api/account_flow_test.rb b/users/test/integration/api/account_flow_test.rb index 66de1e5..add12fe 100644 --- a/users/test/integration/api/account_flow_test.rb +++ b/users/test/integration/api/account_flow_test.rb @@ -1,23 +1,19 @@ require 'test_helper' -class AccountFlowTest < ActionDispatch::IntegrationTest +CONFIG_RU = (Rails.root + 'config.ru').to_s +OUTER_APP = Rack::Builder.parse_file(CONFIG_RU).first - # this test wraps the api and implements the interface the ruby-srp client. - def handshake(login, aa) - post "sessions", :login => login, 'A' => aa.to_s(16) - assert_response :success - response = JSON.parse(@response.body) - if response['errors'] - raise RECORD_NOT_FOUND.new(response['errors']) - else - return response['B'].hex - end +class AccountFlowTest < ActiveSupport::TestCase + include Rack::Test::Methods + include Warden::Test::Helpers + include LeapWebCore::AssertResponses + + def app + OUTER_APP end - def validate(m) - put "sessions/" + @login, :client_auth => m.to_s(16) - assert_response :success - return JSON.parse(@response.body) + def teardown + Warden.test_reset! end def setup @@ -38,13 +34,30 @@ class AccountFlowTest < ActionDispatch::IntegrationTest @user.destroy if @user # make sure we can run this test again end + # this test wraps the api and implements the interface the ruby-srp client. + def handshake(login, aa) + post "/sessions.json", :login => login, 'A' => aa.to_s(16), :format => :json + response = JSON.parse(last_response.body) + if response['errors'] + raise RECORD_NOT_FOUND.new(response['errors']) + else + return response['B'].hex + end + end + + def validate(m) + put "/sessions/" + @login + '.json', :client_auth => m.to_s(16), :format => :json + return JSON.parse(last_response.body) + end + test "signup response" do - assert_json_response @user_params.slice(:login, :password_salt) - assert_response :success + assert_json_response :login => @login, :ok => true + assert last_response.successful? end test "signup and login with srp via api" do server_auth = @srp.authenticate(self) + assert last_response.successful? assert_nil server_auth["errors"] assert server_auth["M2"] end @@ -52,7 +65,8 @@ class AccountFlowTest < ActionDispatch::IntegrationTest test "signup and wrong password login attempt" do srp = SRP::Client.new(@login, "wrong password") server_auth = srp.authenticate(self) - assert_equal ["wrong password"], server_auth["errors"]['password'] + assert_json_error :password => "wrong password" + assert !last_response.successful? assert_nil server_auth["M2"] end @@ -62,6 +76,8 @@ class AccountFlowTest < ActionDispatch::IntegrationTest assert_raises RECORD_NOT_FOUND do server_auth = srp.authenticate(self) end + assert_json_error :login => "could not be found" + assert !last_response.successful? assert_nil server_auth end diff --git a/users/test/integration/api/python/flow_with_srp.py b/users/test/integration/api/python/flow_with_srp.py index 0a11aec..b599252 100755 --- a/users/test/integration/api/python/flow_with_srp.py +++ b/users/test/integration/api/python/flow_with_srp.py @@ -16,7 +16,7 @@ def id_generator(size=6, chars=string.ascii_uppercase + string.digits): return ''.join(random.choice(chars) for x in range(size)) # using globals for a start -server = 'http://springbok/1/' +server = 'http://springbok.leap.se/1/' login = id_generator() password = id_generator() + id_generator() diff --git a/users/test/support/auth_test_helper.rb b/users/test/support/auth_test_helper.rb new file mode 100644 index 0000000..f3506ae --- /dev/null +++ b/users/test/support/auth_test_helper.rb @@ -0,0 +1,35 @@ +module AuthTestHelper + include StubRecordHelper + extend ActiveSupport::Concern + + # Controller will fetch current user from warden. + # Make it pick up our current_user + included do + setup do + request.env['warden'] ||= stub :user => nil + end + end + + def login(user_or_method_hash = {}) + @current_user = stub_record(User, user_or_method_hash) + unless @current_user.respond_to? :is_admin? + @current_user.stubs(:is_admin?).returns(false) + end + request.env['warden'] = stub :user => @current_user + return @current_user + end + + def assert_access_denied(denied = true) + if denied + assert_equal({:alert => "Not authorized"}, flash.to_hash) + assert_redirected_to login_path + else + assert flash[:alert].blank? + end + end + +end + +class ActionController::TestCase + include AuthTestHelper +end diff --git a/users/test/support/stub_record_helper.rb b/users/test/support/stub_record_helper.rb new file mode 100644 index 0000000..2e1a533 --- /dev/null +++ b/users/test/support/stub_record_helper.rb @@ -0,0 +1,41 @@ +module StubRecordHelper + + # Will expect find_by_param or find_by_id to be called on klass and + # return the record given. + # If no record is given but a hash or nil will create a stub based on + # that instead and returns the stub. + def find_record(klass, record_or_method_hash = {}) + record = stub_record(klass, record_or_method_hash) + finder = klass.respond_to?(:find_by_param) ? :find_by_param : :find_by_id + klass.expects(finder).with(record.to_param).returns(record) + return record + end + + # Create a stub that has the usual functions of a database record. + # It won't fail on rendering a form for example. + # + # If the second parameter is a record we return the record itself. + # This way you can build functions that either take a record or a + # method hash to stub from. See find_record for an example. + def stub_record(klass, record_or_method_hash = {}, persisted = true) + if record_or_method_hash && !record_or_method_hash.is_a?(Hash) + return record_or_method_hash + end + stub record_params_for(klass, record_or_method_hash, persisted) + end + + def record_params_for(klass, params = {}, persisted = true) + if klass.respond_to?(:valid_attributes_hash) + params.reverse_merge!(klass.valid_attributes_hash) + end + params[:params] = params.stringify_keys + params.reverse_merge! :id => "A123", + :to_param => "A123", + :class => klass, + :to_key => ['123'], + :to_json => %Q({"stub":"#{klass.name}"}), + :new_record? => !persisted, + :persisted? => persisted + end + +end diff --git a/users/test/test_helper.rb b/users/test/test_helper.rb index 08d4d41..52dff53 100644 --- a/users/test/test_helper.rb +++ b/users/test/test_helper.rb @@ -1,10 +1,9 @@ ENV["RAILS_ENV"] = "test" require File.expand_path('../../../test/dummy/config/environment', __FILE__) require 'rails/test_help' -require 'mocha' +require 'mocha/setup' Rails.backtrace_cleaner.remove_silencers! # Load support files Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } - diff --git a/users/test/unit/user_test.rb b/users/test/unit/user_test.rb index 822ef33..cce11c2 100644 --- a/users/test/unit/user_test.rb +++ b/users/test/unit/user_test.rb @@ -5,6 +5,7 @@ class UserTest < ActiveSupport::TestCase include SRP::Util setup do @attribs = User.valid_attributes_hash + User.find_by_login(@attribs[:login]).try(:destroy) @user = User.new(@attribs) end @@ -19,18 +20,18 @@ class UserTest < ActiveSupport::TestCase end test "test require alphanumerical for login" do - @user.login = "qwär" + @user.login = "qw#r" assert !@user.valid? end - test "find_by_param gets User by login" do + test "find_by_param gets User by id" do @user.save - assert_equal @user, User.find_by_param(@user.login) + assert_equal @user, User.find_by_param(@user.id) @user.destroy end - test "to_param gives user login" do - assert_equal @user.login, @user.to_param + test "to_param gives user id" do + assert_equal @user.id, @user.to_param end test "verifier returns number for the hex in password_verifier" do diff --git a/users/test/unit/warden_strategy_secure_remote_password_test.rb b/users/test/unit/warden_strategy_secure_remote_password_test.rb new file mode 100644 index 0000000..319809a --- /dev/null +++ b/users/test/unit/warden_strategy_secure_remote_password_test.rb @@ -0,0 +1,63 @@ +class WardenStrategySecureRemotePasswordTest < ActiveSupport::TestCase + +# TODO : turn this into sth. real +=begin + setup do + @user = stub :login => "me", :id => 123 + @client_hex = 'a123' + @client_rnd = @client_hex.hex + @server_hex = 'b123' + @server_rnd = @server_hex.hex + @server_rnd_exp = 'e123'.hex + @salt = 'stub user salt' + @server_handshake = stub :aa => @client_rnd, :bb => @server_rnd, :b => @server_rnd_exp + @server_auth = 'adfe' + end + + + test "should perform handshake" do + @user.expects(:initialize_auth). + with(@client_rnd). + returns(@server_handshake) + @server_handshake.expects(:to_json). + returns({'B' => @server_hex, 'salt' => @salt}.to_json) + User.expects(:find_by_param).with(@user.login).returns(@user) + assert_equal @server_handshake, session[:handshake] + assert_response :success + assert_json_response :B => @server_hex, :salt => @salt + end + + test "should report user not found" do + unknown = "login_that_does_not_exist" + User.expects(:find_by_param).with(unknown).raises(RECORD_NOT_FOUND) + post :create, :login => unknown + assert_response :success + assert_json_error "login" => ["unknown user"] + end + + test "should authorize" do + session[:handshake] = @server_handshake + @server_handshake.expects(:authenticate!). + with(@client_rnd). + returns(@user) + @server_handshake.expects(:to_json). + returns({:M2 => @server_auth}.to_json) + post :update, :id => @user.login, :client_auth => @client_hex + assert_nil session[:handshake] + assert_json_response :M2 => @server_auth + assert_equal @user.id, session[:user_id] + end + + test "should report wrong password" do + session[:handshake] = @server_handshake + @server_handshake.expects(:authenticate!). + with(@client_rnd). + raises(WRONG_PASSWORD) + post :update, :id => @user.login, :client_auth => @client_hex + assert_nil session[:handshake] + assert_nil session[:user_id] + assert_json_error "password" => ["wrong password"] + end + +=end +end |