Merge pull request #528 from ChrisBr/devise

Implements iChain authentication
This commit is contained in:
Christian Bruckmayer 2014-11-06 09:48:52 +01:00
commit 79de791f22
24 changed files with 305 additions and 136 deletions

View file

@ -14,6 +14,8 @@ gem 'paper_trail'
# Use devise as authentification framework
gem 'devise'
gem 'devise_ichain_authenticatable'
# Use omniauth to support openID authentication
gem 'omniauth'
gem 'omniauth-facebook'

View file

@ -118,6 +118,8 @@ GEM
railties (>= 3.2.6, < 5)
thread_safe (~> 0.1)
warden (~> 1.2.3)
devise_ichain_authenticatable (0.3.1)
devise (>= 2.2)
diff-lcs (1.2.5)
docile (1.1.3)
erubis (2.7.0)
@ -432,6 +434,7 @@ DEPENDENCIES
database_cleaner
delayed_job_active_record
devise
devise_ichain_authenticatable
factory_girl_rails
font-awesome-rails
formtastic (~> 2.3.0.rc3)

View file

@ -1,4 +1,9 @@
$(function () {
$("#user_biography").bind('keyup', function() {
word_count(this, 'bio_length', 150);
} );
/**
* Displays a modal with the questions of the registration.
*/

View file

@ -8,24 +8,29 @@ class ApplicationController < ActionController::Base
check_authorization unless: :devise_controller?
def store_location
session[:return_to] = request.fullpath if request.get? && controller_name != 'user_sessions' && controller_name != 'sessions'
# store last url - this is needed for post-login redirect to whatever the user last visited.
return unless request.get?
if (request.path != '/accounts/sign_in' &&
request.path != '/accounts/sign_up' &&
request.path != '/accounts/password/new' &&
request.path != '/accounts/password/edit' &&
request.path != '/accounts/confirmation' &&
request.path != '/accounts/sign_out' &&
request.path != '/users/ichain_registration/ichain_sign_up' &&
!request.path.starts_with?(Devise.ichain_base_url) &&
!request.xhr?) # don't store ajax calls
session[:return_to] = request.fullpath
end
end
def after_sign_in_path_for(resource)
def after_sign_in_path_for(_resource)
if (can? :view, Conference) &&
(!session[:return_to] ||
session[:return_to] &&
session[:return_to] == root_path)
admin_conference_index_path
else
if session[:return_to] &&
!session[:return_to].start_with?(user_registration_path)
logger.debug "Returning to #{session[:return_to]}"
session[:return_to]
else
logger.debug "Not returning to #{session[:return_to]} because it would loop"
super
end
session[:return_to] || root_path
end
end
@ -53,6 +58,13 @@ class ApplicationController < ActionController::Base
redirect_to root_path, alert: exception.message
end
rescue_from IChainRecordNotFound do
Rails.logger.debug('IChain Record was not Unique!')
sign_out(current_user)
flash[:error] = 'Your E-Mail adress is already registered at OSEM. Please contact the admin if you want to attach your openSUSE Account to OSEM!'
redirect_to root_path
end
def not_found
raise ActionController::RoutingError.new('Not Found')
end

View file

@ -3,55 +3,12 @@ class RegistrationsController < Devise::RegistrationsController
def edit
@openids = Openid.where(user_id: current_user.id).order(:provider)
super
end
def update
@openids = Openid.where(user_id: current_user.id).order(:provider)
@user = User.find(current_user.id)
email_changed = false
if !params[:user][:email].nil?
if @user.email != params[:user][:email]
email_changed = true
else
params[:user].delete :email
end
end
password_changed = false
if !params[:user][:password].nil?
if !params[:user][:password].empty?
password_changed = true
else
params[:user].delete :password
params[:user].delete :password_confirmation
end
end
if email_changed || password_changed
successfully_updated = @user.update_with_password(account_update_params)
else
params[:user].delete :current_password
successfully_updated = @user.update_without_password(account_update_params)
end
if successfully_updated
if email_changed
unless @user.nil?
@user.update_attribute('email', params[:user][:email])
end
set_flash_message :notice, :update_needs_confirmation
else
set_flash_message :notice, :updated
end
# Sign in the user bypassing validation in case his password changed
sign_in @user, bypass: true
redirect_to after_update_path_for(@user)
else
flash[:alert] = 'Updating account failed. ' \
"#{@user.errors.full_messages.join('. ')}."
render 'edit'
end
super
end
protected
@ -67,12 +24,11 @@ class RegistrationsController < Devise::RegistrationsController
def configure_permitted_parameters
devise_parameter_sanitizer.for(:account_update) do |u|
u.
permit(:email, :password, :password_confirmation, :current_password, :name, :biography,
:nickname, :affiliation)
permit(:email, :password, :password_confirmation, :current_password, :username)
end
devise_parameter_sanitizer.for(:sign_up) do |u|
u.
permit(:email, :password, :password_confirmation, :name)
permit(:email, :password, :password_confirmation, :name, :username)
end
end
end

View file

@ -11,11 +11,13 @@ module Users
def handle(provider)
auth_hash = request.env['omniauth.auth']
uid = auth_hash[:uid]
openid = Openid.find_for_oauth(auth_hash) # Get or create openid
# If openid exists and is associated with a user, sign in with associated user,
# even if the email of the associated user and the email of the provided openid are different
unless (user = openid.user)
user = User.find_for_auth(auth_hash, current_user) # Get or create users
user.username = "#{uid}@#{provider}" if user.username.blank?
end
begin
@ -28,7 +30,8 @@ module Users
sign_in user
redirect_to root_path, notice: user.email + " signed in successfully with #{provider}"
rescue => e
redirect_back_or_to new_user_registration_path, alert: 'Failed' + e.message
flash[:error] = e.message
redirect_back_or_to new_user_registration_path
end
end
end

View file

@ -0,0 +1,30 @@
class UsersController < ApplicationController
before_filter :verify_user
load_and_authorize_resource
# GET /users/1
def show
@events = @user.events.where(state: :confirmed)
end
# GET /users/1/edit
def edit
end
# PATCH/PUT /users/1
def update
if @user.update(user_params)
redirect_to @user, notice: 'User was successfully updated.'
else
flash[:error] = "A error prohibited your Profile from being saved: #{@user.errors.full_messages.join('. ')}."
render :edit
end
end
private
# Only allow a trusted parameter "white list" through.
def user_params
params.require(:user).permit(:user_id, :name, :biography, :nickname, :affiliation)
end
end

View file

@ -123,6 +123,7 @@ class Ability
end
can :index, :schedule # show?
can :show, User
end
def signed_in(user)
@ -131,6 +132,9 @@ class Ability
# Conference Registration
can :manage, Registration, user_id: user.id
can :manage, User, id: user.id
can :show, User
## Proposals
# Users can manage their own proposals
can :manage, Event, id: user.events.pluck(:id)

View file

@ -1,3 +1,6 @@
class IChainRecordNotFound < StandardError
end
class User < ActiveRecord::Base
rolify
include Gravtastic
@ -8,16 +11,26 @@ class User < ActiveRecord::Base
# Include default devise modules. Others available are:
# :token_authenticatable, :confirmable,
# :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable, :confirmable,
:omniauthable, omniauth_providers: [:suse, :google, :facebook, :github]
devise_modules = []
if CONFIG['authentication']['ichain']['enabled']
devise_modules += [ :ichain_authenticatable, :ichain_registerable ]
else
devise_modules += [:database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable, :confirmable,
:omniauthable, omniauth_providers: [:suse, :google, :facebook, :github] ]
end
devise(*devise_modules)
has_and_belongs_to_many :roles
has_many :openids
attr_accessible :email, :password, :password_confirmation, :remember_me, :role_id, :role_ids,
:name, :email_public, :biography, :nickname, :affiliation, :is_admin,
:tshirt, :mobile, :volunteer_experience, :languages
:tshirt, :mobile, :volunteer_experience, :languages, :username, :login
attr_accessor :login
has_many :event_users, dependent: :destroy
has_many :events, -> { uniq }, through: :event_users
@ -29,7 +42,13 @@ class User < ActiveRecord::Base
has_many :subscriptions, dependent: :destroy
accepts_nested_attributes_for :roles
validates :name, presence: true
validates :email, presence: true
validates :username,
uniqueness: {
case_sensitive: false
},
presence: true
# Returns the ticket purchased ticket
# ====Returns
@ -38,6 +57,30 @@ class User < ActiveRecord::Base
ticket_purchases.where(ticket_id: id).first
end
def self.for_ichain_username(username, attributes)
user = find_by(username: username)
if user
user.update_attributes(email: attributes[:email])
else
begin
user = create(username: username, email: attributes[:email])
rescue ActiveRecord::RecordNotUnique
raise IChainRecordNotFound
end
end
user
end
def self.find_for_database_authentication(warden_conditions)
conditions = warden_conditions.dup
login = conditions.delete(:login)
if login
where(conditions).where(['lower(username) = :value OR lower(email) = :value', { value: login.downcase }]).first
else
where(conditions).first
end
end
# Searches for user based on email. Returns found user or new user.
# ====Returns
# * +User::ActiveRecord_Relation+ -> user

View file

@ -1,35 +1,25 @@
%h1 Edit your Account
.row
.col-md-12
= semantic_form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f|
= f.inputs name: 'Profile' do
= f.input :name, as: :string
= f.input :nickname, as: :string
= f.input :affiliation, as: :string,
hint: 'This could be a company, a user group, or nothing at all.'
= f.input :biography, input_html: {rows: 5,
'onkeyup' => "word_count(this, 'biography-count', 150)"}
You have used
%span#biography-count #{current_user.biography_word_count}
words. Biographies are limited to 150 words.
%br
%br
= f.inputs name: 'Account' do
= f.input :username, required: false, input_html: { autocomplete: 'off' }
= f.input :email, required: false, input_html: { autocomplete: 'off' }
= f.input :password, hint: "(Leave blank if you don't want to change it)", input_html: { autocomplete: 'off' }
= f.input :password_confirmation, input_html: { autocomplete: 'off' }
= f.inputs name: 'OpenID' do
%h4
Currently the following openIDs are associated with your account
- @openids.each do |openid|
%li= "#{openid.provider}:#{openid.email}"
%br
%h4
To add an openID with a different email address to your account, sign in with your
openID while logged in to OSEM
- if User.omniauth_providers.present?
%h4
To add an openID with a different email address to your account, sign in with your
openID while logged in to OSEM
#openidlinks
= render 'devise/shared/openid'
= f.inputs name: 'Account' do
= f.input :email, required: false, input_html: {autocomplete: "off"}
= f.input :password, hint: "(Leave blank if you don't want to change it)", input_html: {autocomplete: 'off'}
= f.input :password_confirmation, input_html: {autocomplete: 'off'}
= f.inputs name: 'Confirmation' do
= f.input :current_password, input_html: {autocomplete: 'off'},
hint: '(we need your current password to confirm password or email changes)'
= f.action :submit, as: :button, label: 'Update', button_html: {class: 'btn btn-primary'}
hint: '(we need your current password to confirm password, email or username changes)'
= f.action :submit, as: :button, label: 'Update Account', button_html: {class: 'btn btn-primary'}

View file

@ -6,6 +6,7 @@
Sign Up
.panel-body
= semantic_form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f|
= f.input :username, required: true
= f.input :email
= f.input :name, required: true
= f.input :password

View file

@ -6,7 +6,7 @@
Sign In
.panel-body
= semantic_form_for(resource, as: resource_name, url: session_path(resource_name)) do |f|
= f.input :email
= f.input :login
= f.input :password
- if devise_mapping.rememberable?
%p.text-right.small

View file

@ -16,7 +16,7 @@
%ul.nav.navbar-nav.navbar-right
%li.dropdown
%a.dropdown-toggle{"data-toggle" => "dropdown", :href => "#", id: "current-user-detail"}
- if not current_user.name.empty?
- if not current_user.name.blank?
#{current_user.name}
-else
#{current_user.email}
@ -26,13 +26,13 @@
= render 'layouts/user_menu'
- else
%ul.nav.navbar-nav.navbar-right
- if current_page?(new_registration_path('user'))
%li.active
= link_to(new_registration_path('user')) do
- if CONFIG['authentication']['ichain']['enabled']
%li{:class=> "#{active_nav_li(new_ichain_registration_path('user'))}"}
= link_to(new_ichain_registration_path('user')) do
%span.fa.fa-heart
Sign Up
- else
%li
%li{:class=> "#{active_nav_li(new_registration_path('user'))}"}
= link_to(new_registration_path('user')) do
%span.fa.fa-heart
Sign Up
@ -42,23 +42,29 @@
Sign In
%span.caret
.dropdown-menu{:style => "padding: 17px;"}
= form_tag user_session_path do
= text_field_tag 'user[email]', nil, id: 'user_email_dd'
= password_field_tag 'user[password]', nil, id: 'user_password_dd'
%p.text-right
%small
Remember me
= check_box_tag 'user[remember_me]'
%button.btn.btn-success.btn-block Sign in
- unless omniauth_configured.empty?
.divider
%h6.text-center
or
= render 'devise/shared/openid'
%p.text-right
%a.small{"data-toggle" => "collapse", "data-target" => "#navbar-devise-help"}
Need Help?
#navbar-devise-help.collapse
= link_to "Forgot your password?", new_password_path(User.new)
%li.hidden-lg
= link_to('Sign In', new_user_session_path)
- if CONFIG['authentication']['ichain']['enabled']
= form_tag new_user_ichain_session_path do
= text_field_tag 'username', nil, id: 'user_ichain_email_dd'
= password_field_tag 'password', nil, id: 'user_ichain_password_dd'
%button.btn.btn-success.btn-block Sign in
- else
= form_tag new_user_session_path do
= text_field_tag 'user[login]', nil, id: 'user_login_dd'
= password_field_tag 'user[password]', nil, id: 'user_password_dd'
%p.text-right
%small
Remember me
= check_box_tag 'user[remember_me]'
%button.btn.btn-success.btn-block Sign in
- unless omniauth_configured.empty?
.divider
%h6.text-center
or
= render 'devise/shared/openid'
%p.text-right
%a.small{"data-toggle" => "collapse", "data-target" => "#navbar-devise-help"}
Need Help?
#navbar-devise-help.collapse
= link_to "Forgot your password?", new_password_path(User.new)
%li.hidden-lg
= link_to('Sign In', new_user_session_path)

View file

@ -1,16 +1,26 @@
- unless CONFIG['authentication']['ichain']['enabled']
%li
= link_to(edit_user_registration_path) do
%span.fa.fa-wrench
Edit Account
%li
= link_to(edit_user_registration_path) do
%span.fa.fa-wrench
Edit Account
= link_to(edit_user_path(current_user.id)) do
%span.fa.fa-user
Edit Profil
-if @conference and @conference.id
%li
= link_to(conference_proposal_index_path(@conference.short_title)) do
%span.fa.fa-comment
My Submissions
%li
= link_to(destroy_user_session_path, :method=>'delete') do
%span.fa.fa-minus
Sign out
- if CONFIG['authentication']['ichain']['enabled']
= link_to(destroy_user_ichain_session_path, :method=>'delete') do
%span.fa.fa-minus
Sign out
- else
= link_to(destroy_user_session_path, :method=>'delete') do
%span.fa.fa-minus
Sign out
- if can? :index, Conference
%li.divider
%li

View file

@ -0,0 +1,17 @@
%h1 Edit your profile
.row
.col-md-12
= semantic_form_for(@user, url: user_path(@user.id)) do |f|
= f.inputs name: 'Profile' do
= f.input :name, as: :string
= f.input :nickname, as: :string
= f.input :affiliation, as: :string,
hint: 'This could be a company, a user group, or nothing at all.'
= f.input :biography, input_html: { rows: 5, 'onkeyup' => "word_count(this, 'biography-count', 150)" }
You have used
%span#bio_length
0
words. Biographies are limited to 150 words.
%br
%br
= f.action :submit, as: :button, label: 'Update', button_html: {class: 'btn btn-primary'}

View file

@ -0,0 +1,16 @@
%h1
= @user.name
%h4
= @user.biography
- if @user.events.confirmed.any?
%h3
= "#{@user.name} presents #{pluralize(@user.events.confirmed.count, 'Event')}:"
%ul.list-unstyled
- @user.events.confirmed.each do |event|
%li
%h4
= link_to event.title, conference_proposal_path(event.conference.short_title, event.id)
%strong
at
= event.conference.title

View file

@ -16,6 +16,11 @@ defaults: &defaults
speakerdeck: 'Speakerdeck',
instagram: 'Instagram' }
# If you want to use iChain to handle registration and authentication enable the next lines
authentication:
ichain:
enabled: false
development:
<<: *defaults

View file

@ -38,7 +38,7 @@ Devise.setup do |config|
# session. If you need permissions, you should implement that in a before filter.
# You can also supply a hash where the value is a boolean determining whether
# or not authentication should be aborted when the value is not present.
# config.authentication_keys = [ :email ]
config.authentication_keys = [ :login ]
# Configure parameters from the request object used for authentication. Each entry
# given should be a request method and it will automatically be passed to the
@ -244,4 +244,35 @@ Devise.setup do |config|
# When using omniauth, Devise cannot automatically set Omniauth path,
# so you need to do it manually. For the users scope, it would be:
# config.omniauth_path_prefix = "/my_engine/users/auth"
# You will always need to set this parameter.
config.ichain_base_url = "https://my.application.org"
# Paths (relative to ichain_base_url) used by your proxy
# config.ichain_login_path = "ICSLogin/"
# config.ichain_registration_path = "ICSLogin/auth-up/"
# config.ichain_logout_path = "cmd/ICSLogout/"
# The header used by your iChain proxy to pass the username.
# config.ichain_username_header = "HTTP_X_USERNAME"
# Additional parameters, beyond the username, provided by the iChain proxy.
# HTTP_X_EMAIL is expected by default. Set to {} if no additional attributes
# are configured in the proxy.
# config.ichain_attributes_header = {:email => "HTTP_X_EMAIL"}
# Configuration options for requests sent to the iChain proxy
# config.ichain_context = "default"
# config.ichain_proxypath = "reverse"
# Activate the test mode, useful when no real iChain is present, like in
# testing and development environments
# config.ichain_test_mode = true
# In test mode, you can skip asking for the user information by always
# forcing the following username. The user will be permanently signed in.
# config.ichain_force_test_username = "testuser"
# In test mode, force the following additional attributes
# config.ichain_force_test_attributes = {:email => "testuser@example.com"}
end

View file

@ -17,7 +17,7 @@ en:
unauthenticated: 'You need to sign in or sign up before continuing.'
unconfirmed: 'You have to confirm your account before continuing.'
locked: 'Your account is locked.'
invalid: 'Invalid email or password.'
invalid: 'Invalid login or password.'
invalid_token: 'Invalid authentication token.'
timeout: 'Your session expired, please sign in again to continue.'
inactive: 'Your account was not activated yet.'

View file

@ -1,8 +1,16 @@
Osem::Application.routes.draw do
devise_for :users, controllers: { registrations: :registrations,
omniauth_callbacks: 'users/omniauth_callbacks' },
path: 'accounts'
if CONFIG['authentication']['ichain']['enabled']
devise_for :users, controllers: { registrations: :registrations }
else
devise_for :users,
controllers: {
registrations: :registrations,
omniauth_callbacks: 'users/omniauth_callbacks' },
path: 'accounts'
end
resources :users, except: [:new, :index, :create, :destroy]
namespace :admin do
resources :users

View file

@ -0,0 +1,6 @@
class AddUsernameToUsers < ActiveRecord::Migration
def change
add_column :users, :username, :string
add_index :users, :username, unique: true
end
end

View file

@ -0,0 +1,18 @@
class GenerateUsername < ActiveRecord::Migration
class TempUser < ActiveRecord::Base
self.table_name = 'users'
attr_accessible :username, :email
end
def change
TempUser.all.each do |user|
if user.username.blank?
username = user.email.split('@')[0]
if TempUser.find_by(username: username)
username = username + user.id.to_s
end
user.update_attributes(username: username)
end
end
end
end

View file

@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20140825093132) do
ActiveRecord::Schema.define(version: 20141104131625) do
create_table "ahoy_events", force: true do |t|
t.uuid "visit_id"
@ -84,13 +84,13 @@ ActiveRecord::Schema.define(version: 20140825093132) do
end
create_table "conferences", force: true do |t|
t.string "guid", null: false
t.string "title", null: false
t.string "short_title", null: false
t.string "timezone", null: false
t.string "guid", null: false
t.string "title", null: false
t.string "short_title", null: false
t.string "timezone", null: false
t.string "html_export_path"
t.date "start_date", null: false
t.date "end_date", null: false
t.date "start_date", null: false
t.date "end_date", null: false
t.integer "venue_id"
t.datetime "created_at"
t.datetime "updated_at"
@ -98,11 +98,11 @@ ActiveRecord::Schema.define(version: 20140825093132) do
t.string "logo_content_type"
t.integer "logo_file_size"
t.datetime "logo_updated_at"
t.boolean "use_dietary_choices", default: false
t.boolean "use_dietary_choices", default: false
t.integer "revision"
t.boolean "use_vpositions", default: false
t.boolean "use_vdays", default: false
t.boolean "use_difficulty_levels", default: false
t.boolean "use_vpositions", default: false
t.boolean "use_vdays", default: false
t.boolean "use_difficulty_levels", default: false
t.boolean "use_volunteers"
t.string "color"
t.string "sponsor_email"
@ -504,11 +504,13 @@ ActiveRecord::Schema.define(version: 20140825093132) do
t.string "languages"
t.text "volunteer_experience"
t.boolean "is_admin", default: false
t.string "username"
end
add_index "users", ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
add_index "users", ["email"], name: "index_users_on_email", unique: true
add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
add_index "users", ["username"], name: "index_users_on_username", unique: true
create_table "vchoices", force: true do |t|
t.integer "vday_id"
@ -525,8 +527,8 @@ ActiveRecord::Schema.define(version: 20140825093132) do
create_table "venues", force: true do |t|
t.string "guid"
t.text "name", limit: 255
t.text "address", limit: 255
t.text "name", limit: 255
t.text "address", limit: 255
t.string "website"
t.text "description"
t.string "offline_map_url"

View file

@ -3,6 +3,7 @@ FactoryGirl.define do
factory :user do
sequence(:email) { |n| "example#{n}@example.com" }
sequence(:name) { |n| "name#{n}" }
sequence(:username) { |n| "username#{n}" }
password 'changeme'
password_confirmation 'changeme'
confirmed_at { Time.now }