Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 !@ui.nil? && @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
2 changes: 1 addition & 1 deletion app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ class User < ApplicationRecord
# :token_authenticatable, :confirmable,
# :lockable, :timeoutable and :omniauthable
devise :invitable, :database_authenticatable, :registerable, :recoverable,
:rememberable, :trackable, :validatable, :omniauthable,
:rememberable, :trackable, :validatable, :omniauthable, :confirmable,
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: 'noreply@example.org' }

# Print deprecation notices to the stderr.
config.active_support.deprecation = :stderr
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