Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
- Fix failing eslint workflow / upgrade `actions/checkout` & `actions/setup-node` to v3 [#3503](https://github.com/DMPRoadmap/roadmap/pull/3503)
- Fix rendering of `confirm_merge` partial [#3515](https://github.com/DMPRoadmap/roadmap/pull/3515)
- Remove Auto-Generated TinyMCE Skins and Add `public/tinymce/skins/` to `.gitignore` [#3466](https://github.com/DMPRoadmap/roadmap/pull/3466)
- Re-implement email confirmation [#3507](https://github.com/DMPRoadmap/roadmap/pull/3507)
- Update `spec/support/faker.rb` to use `I18n.default_locale` [#3507](https://github.com/DMPRoadmap/roadmap/pull/3507)

## v5.0.0

Expand Down
34 changes: 21 additions & 13 deletions app/controllers/sessions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

# Controller that handles user login and logout
class SessionsController < Devise::SessionsController
include EmailConfirmationHandler

def new
redirect_to(root_path)
end
Expand All @@ -11,28 +13,22 @@ def new
# rubocop:disable Metrics/AbcSize
def create
existing_user = User.find_by(email: params[:user][:email])
unless existing_user.nil?
if existing_user.present?

# (see app/models/concerns/email_confirmation_handler.rb)
return if confirmation_instructions_missing_and_handled?(existing_user)

# Until ORCID login is supported
unless session['devise.shibboleth_data'].nil?
args = {
identifier_scheme: IdentifierScheme.find_by(name: 'shibboleth'),
value: session['devise.shibboleth_data']['uid'],
identifiable: existing_user,
attrs: session['devise.shibboleth_data']
}
@ui = Identifier.new(args)
end
@ui = create_shibboleth_identifier(existing_user) unless session['devise.shibboleth_data'].nil?
session[:locale] = existing_user.locale unless existing_user.locale.nil?
# Method defined at controllers/application_controller.rb
set_locale
end

super do
if [email protected]? && @ui.save
# rubocop:disable Layout/LineLength
flash[:notice] = _('Your account has been successfully linked to your institutional credentials. You will now be able to sign in with them.')
# rubocop:enable Layout/LineLength
flash[:notice] = _('Your account has been successfully linked to your institutional credentials. ' \
'You will now be able to sign in with them.')
end
end
end
Expand All @@ -45,3 +41,15 @@ def destroy
set_locale
end
end

private

def create_shibboleth_identifier(user)
args = {
identifier_scheme: IdentifierScheme.find_by(name: 'shibboleth'),
value: session['devise.shibboleth_data']['uid'],
identifiable: user,
attrs: session['devise.shibboleth_data']
}
Identifier.new(args)
end
96 changes: 54 additions & 42 deletions app/controllers/users/omniauth_callbacks_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
module Users
# Controller that handles callbacks from OmniAuth integrations (e.g. Shibboleth and ORCID)
class OmniauthCallbacksController < Devise::OmniauthCallbacksController
include EmailConfirmationHandler
##
# Dynamically build a handler for each omniauth provider
# -------------------------------------------------------------
Expand All @@ -21,8 +22,6 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
#
# scheme - The IdentifierScheme for the provider
#
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
def handle_omniauth(scheme)
user = if request.env['omniauth.auth'].nil?
User.from_omniauth(request.env)
Expand All @@ -32,55 +31,68 @@ def handle_omniauth(scheme)

# If the user isn't logged in
if current_user.nil?
# If the uid didn't have a match in the system send them to register
if user.nil?
session["devise.#{scheme.name.downcase}_data"] = request.env['omniauth.auth']
redirect_to new_user_registration_url

# Otherwise sign them in
elsif scheme.name == 'shibboleth'
# Until ORCID becomes supported as a login method
set_flash_message(:notice, :success, kind: scheme.description) if is_navigational_format?
sign_in_and_redirect user, event: :authentication
else
flash[:notice] = _('Successfully signed in')
redirect_to new_user_registration_url
end

handle_omniauth_for_signed_out_user(user, scheme)
# The user is already logged in and just registering the uid with us
else
# If the user could not be found by that uid then attach it to their record
if user.nil?
if Identifier.create(identifier_scheme: scheme,
value: request.env['omniauth.auth'].uid,
attrs: request.env['omniauth.auth'],
identifiable: current_user)
flash[:notice] =
format(_('Your account has been successfully linked to %{scheme}.'),
scheme: scheme.description)
handle_omniauth_for_signed_in_user(user, scheme)
end
end

def failure
redirect_to root_path
end

else
flash[:alert] = format(_('Unable to link your account to %{scheme}.'),
scheme: scheme.description)
end
private

elsif user.id != current_user.id
# If a user was found but does NOT match the current user then the identifier has
# already been attached to another account (likely the user has 2 accounts)
# rubocop:disable Layout/LineLength
flash[:alert] = _("The current #{scheme.description} iD has been already linked to a user with email #{identifier.user.email}")
# rubocop:enable Layout/LineLength
# rubocop:disable Metrics/AbcSize
def handle_omniauth_for_signed_in_user(user, scheme)
# If the user could not be found by that uid then attach it to their record
if user.nil?
if Identifier.create(identifier_scheme: scheme,
value: request.env['omniauth.auth'].uid,
attrs: request.env['omniauth.auth'],
identifiable: current_user)
flash[:notice] = format(_('Your account has been successfully linked to %{scheme}.'),
scheme: scheme.description)

else
flash[:alert] = format(_('Unable to link your account to %{scheme}.'),
scheme: scheme.description)
end

# Redirect to the User Profile page
redirect_to edit_user_registration_path
elsif user.id != current_user.id
# If a user was found but does NOT match the current user then the identifier has
# already been attached to another account (likely the user has 2 accounts)
flash[:alert] = _("The current #{scheme.description} iD has been already linked " \
"to a user with email #{identifier.user.email}")
end

# Redirect to the User Profile page
redirect_to edit_user_registration_path
end
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
# rubocop:enable Metrics/AbcSize

def failure
redirect_to root_path
# rubocop:disable Metrics/AbcSize
def handle_omniauth_for_signed_out_user(user, scheme)
# If the uid didn't have a match in the system send them to register
if user.nil?
session["devise.#{scheme.name.downcase}_data"] = request.env['omniauth.auth']
redirect_to new_user_registration_url

# Otherwise sign them in
elsif scheme.name == 'shibboleth'
# Until ORCID becomes supported as a login method

# (see app/models/concerns/email_confirmation_handler.rb)
return if confirmation_instructions_missing_and_handled?(user)

set_flash_message(:notice, :success, kind: scheme.description) if is_navigational_format?
sign_in_and_redirect user, event: :authentication
else
flash[:notice] = _('Successfully signed in')
redirect_to new_user_registration_url
end
end
# rubocop:enable Metrics/AbcSize
end
end
29 changes: 29 additions & 0 deletions app/models/concerns/email_confirmation_handler.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# frozen_string_literal: true

# Some users in our db are both unconfirmed AND have no outstanding confirmation_token
# This is true for those users due to the following:
# - We haven't always used Devise's :confirmable module (it generates a confirmation_token when a user is created)
# - We have set `confirmed_at` and `confirmation_token` to nil via Rake tasks (lib/tasks/email_confirmation.rake)
# This concern is meant to streamline the confirmation process for those users
module EmailConfirmationHandler
extend ActiveSupport::Concern

def confirmation_instructions_missing_and_handled?(user)
# A user's "confirmation instructions are missing" if they're both unconfirmed and have no confirmation_token
return false if user_confirmed_or_has_confirmation_token?(user)

handle_missing_confirmation_instructions(user)
true
end

private

def user_confirmed_or_has_confirmation_token?(user)
user.confirmed? || user.confirmation_token.present?
end

def handle_missing_confirmation_instructions(user)
user.send_confirmation_instructions
redirect_to root_path, notice: I18n.t('devise.registrations.signed_up_but_unconfirmed')
end
end
4 changes: 2 additions & 2 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,9 @@ class User < ApplicationRecord
# Devise
# Include default devise modules. Others available are:
# :token_authenticatable, :confirmable,
# :lockable, :timeoutable and :omniauthable
# :lockable and :omniauthable
devise :invitable, :database_authenticatable, :registerable, :recoverable,
:rememberable, :trackable, :validatable, :omniauthable,
:rememberable, :trackable, :validatable, :omniauthable, :confirmable, :timeoutable,
omniauth_providers: %i[shibboleth orcid]

# default user language to the default language
Expand Down
1 change: 1 addition & 0 deletions config/environments/test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
# The :test delivery method accumulates sent emails in the
# ActionMailer::Base.deliveries array.
config.action_mailer.delivery_method = :test
config.action_mailer.default_options = { from: '[email protected]' }

# Print deprecation notices to the stderr.
config.active_support.deprecation = :stderr
Expand Down
2 changes: 1 addition & 1 deletion config/initializers/devise.rb
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@
# ==> Configuration for :timeoutable
# The time you want to timeout the user session without activity. After this
# time the user will be asked for credentials again. Default is 30 minutes.
config.timeout_in = 3.hours
config.timeout_in = 90.minutes

# If true, expires auth token on session timeout.
# config.expire_auth_token_on_timeout = false
Expand Down
3 changes: 2 additions & 1 deletion config/locales/translation.en-CA.yml
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,8 @@ en-CA:
not_found_in_database: Invalid %{authentication_keys} or password.
timeout: Your session expired. Please sign in again to continue.
unauthenticated: You need to sign in or sign up before continuing.
unconfirmed: You have to confirm your email address before continuing.
unconfirmed: You need to confirm your account before continuing. <a href="/users/confirmation/new"
class="a-orange">(Click to request a new confirmation email)</a>
invited:
mailer:
confirmation_instructions:
Expand Down
3 changes: 2 additions & 1 deletion config/locales/translation.en-GB.yml
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,8 @@ en-GB:
not_found_in_database: Invalid email or password.
timeout: Your session expired, please sign in again to continue.
unauthenticated: You need to sign in or sign up before continuing.
unconfirmed: You have to confirm your account before continuing.
unconfirmed: You have to confirm your account before continuing. <a href="/users/confirmation/new"
class="a-orange">(Click to request a new confirmation email)</a>
invited: You have a pending invitation, accept it to finish creating your account.
mailer:
confirmation_instructions:
Expand Down
3 changes: 2 additions & 1 deletion config/locales/translation.fr-CA.yml
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,8 @@ fr-CA:
not_found_in_database: "%{authentication_keys} ou mot de passe incorrect."
timeout: Votre session est expirée. Veuillez vous reconnecter pour continuer.
unauthenticated: Vous devez vous connecter ou vous inscrire pour continuer.
unconfirmed: Vous devez confirmer votre adresse courriel pour continuer.
unconfirmed: Vous devez confirmer votre compte avant de continuer. <a href="/users/confirmation/new"
class="a-orange">(cliquez pour demander un nouveau courriel de confirmation)</a>
invited: Vous avez une invitation en attente. Acceptez-la pour terminer la création
de votre compte.
mailer:
Expand Down
47 changes: 47 additions & 0 deletions lib/tasks/email_confirmation.rake
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# frozen_string_literal: true

namespace :email_confirmation do
desc 'Reset confirmation status for all users, excluding superusers'
task clear_all: :environment do
p '------------------------------------------------------------------------'
p 'Beginning task: Unconfirming all users except superusers'
p '------------------------------------------------------------------------'
unconfirm_all_users_except_superusers
p 'Task completed: Unconfirmed all users except superusers'
end

private

def unconfirm_all_users_except_superusers
p 'Updating :confirmable columns to nil for all users'
p '(i.e. Setting confirmed_at, confirmation_token, and confirmation_sent_at to nil for all users)'
p '------------------------------------------------------------------------'
set_confirmable_cols_to_nil_for_all_users
p '------------------------------------------------------------------------'
p 'Updating superusers so that they are not required to confirm their email addresses'
p '(i.e. Setting `confirmed_at = Time.current` for superusers)'
p '------------------------------------------------------------------------'
confirm_superusers
end

def set_confirmable_cols_to_nil_for_all_users
count = User.update_all(confirmed_at: nil, confirmation_token: nil, confirmation_sent_at: nil)
p ":confirmable columns updated to nil for #{count} users"
end

# Sets `confirmed_at` to `Time.current` for all superusers
def confirm_superusers
confirmed_at = Time.current
count = User.joins(:perms).where(perms: { id: super_admin_perm_ids })
.distinct
.update_all(confirmed_at: confirmed_at)
p "Updated confirmed_at = #{confirmed_at} for #{count} superuser(s)"
end

# Returns an array of all perm ids that are considered super admin perms
# (Based off of `def can_super_admin?` in `app/models/user.rb`
# i.e. `can_add_orgs? || can_grant_api_to_orgs? || can_change_org?` )
def super_admin_perm_ids
[Perm.add_orgs.id, Perm.grant_api.id, Perm.change_affiliation.id]
end
end
10 changes: 10 additions & 0 deletions spec/factories/identifier_schemes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,16 @@
context_count { 1 }
end

# Add a trait for Shibboleth
trait :shibboleth do
name { 'shibboleth' }
context { 11 }
description { 'Institutional Sign In (Shibboleth)' }
logo_url { nil }
identifier_prefix { nil }
active { true }
end

after(:create) do |identifier_scheme, evaluator|
(0..evaluator.context_count - 1).each do |idx|
identifier_scheme.update("#{identifier_scheme.all_context[idx]}": true)
Expand Down
10 changes: 10 additions & 0 deletions spec/factories/users.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
email { Faker::Internet.unique.email }
password { 'password' }
accept_terms { true }
confirmed_at { Time.current }

trait :org_admin do
after(:create) do |user, _evaluator|
Expand All @@ -81,5 +82,14 @@
end
end
end

trait :unconfirmed_and_no_confirmation_token do
confirmed_at { nil }
# When using :confirmable, a confirmation_token is generated at the time of user creation
# Setting it to nil allows us to duplicate the attributes of some existing users
after(:create) do |user|
user.update(confirmation_token: nil)
end
end
end
end
Loading