Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge integration into deployment-portage For Upcoming Release #948

Closed
wants to merge 66 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
ed53842
Merge pull request #913 from portagenetwork/deployment-portage
aaronskiba Sep 23, 2024
9ded631
Disable email in forms & remove from update_params
aaronskiba Sep 23, 2024
2486220
Update layout of "Edit Profile" contents
aaronskiba Sep 25, 2024
c445c1f
Fix test / update SSO button name within test
aaronskiba Sep 25, 2024
bdbc3ac
Merge pull request #918 from portagenetwork/aaron/fix-button-name-for…
aaronskiba Oct 3, 2024
725a13f
Add `:confirmable` to included devise modules
aaronskiba Sep 25, 2024
c4eecda
Replace .yml value from DMPOnline to DMP Assistant
aaronskiba Sep 26, 2024
6c41bd7
TEMP: customise devise.failure.unconfirmed values
aaronskiba Sep 26, 2024
6087aee
Improve UX flow for existing unconfirmed users
aaronskiba Oct 2, 2024
465d671
Handle confirmation for SSO created users
aaronskiba Oct 2, 2024
965c336
Re-add SSO linking capability for signed-out users
aaronskiba Oct 3, 2024
1caf1f7
Refactor / Make Rubocop happy
aaronskiba Oct 3, 2024
bf54e2f
Adapt existing tests to `:confirmable` re-addition
aaronskiba Oct 7, 2024
c68f713
Reset `:confirmable` cols for all non-superusers
aaronskiba Oct 7, 2024
fb883ec
Reuse `signed_up_but_unconfirmed` flash message
aaronskiba Oct 7, 2024
c996104
`.downcase` SSO email in `find_or_initialize_by()`
aaronskiba Oct 10, 2024
ade1189
Refactor `create_from_provider_data`
aaronskiba Oct 10, 2024
e395945
Update CHANGELOG.md
aaronskiba Oct 10, 2024
ab1b121
Refactor `create_from_provider_data`
aaronskiba Oct 10, 2024
8ef17ee
Merge pull request #924 from portagenetwork/aaron/issues/900
aaronskiba Oct 15, 2024
a2f607b
Enable custom confirmation UX flow for SSO sign in
aaronskiba Oct 15, 2024
414e376
Refactor: Move logic to `EmailConfirmationHandler`
aaronskiba Oct 15, 2024
a25a66a
Refactor / Make rubocop happy
aaronskiba Oct 16, 2024
a77e5fe
Add test for SSO linking of signed-out users
aaronskiba Oct 17, 2024
90f65ba
Add tests for custom email confirmation UX flow
aaronskiba Oct 9, 2024
d0749fb
Test custom I18n strings / override default_locale
aaronskiba Oct 17, 2024
89f32d0
Refactor EmailConfirmationHandler
aaronskiba Oct 17, 2024
da9fc60
Update `Time.now` to `Time.now.utc`
aaronskiba Oct 22, 2024
96a2af0
Update CHANGELOG.md
aaronskiba Oct 23, 2024
dabf2f0
Merge branch 'integration' into aaron/issues/916
aaronskiba Oct 23, 2024
fe5da38
Merge pull request #917 from portagenetwork/aaron/issues/916
aaronskiba Oct 24, 2024
296a73f
Address `OmniAuth.config.mock_auth` overriding
aaronskiba Oct 3, 2024
a891e59
Update and re-enable openid_connect features tests
aaronskiba Oct 9, 2024
b4cbeb5
Address `User.from_omniauth` overriding
aaronskiba Oct 20, 2024
cb9b6ad
Refactor: Create `:openid_connect` trait
aaronskiba Oct 22, 2024
c542d79
Refactor/Cleanup
aaronskiba Oct 22, 2024
b78fbf6
Update CHANGELOG.md
aaronskiba Oct 23, 2024
7d64c24
Merge pull request #922 from portagenetwork/aaron/issues/921
aaronskiba Oct 24, 2024
fd07ea1
Merge branch 'integration' into aaron/add-email-confirmation
aaronskiba Oct 24, 2024
a4e5dca
Lower `psych` version to enable .yml syncing
aaronskiba Oct 28, 2024
7c2cf88
`translation:sync` w/ prod key && `!disable_yaml`
aaronskiba Oct 28, 2024
ea71a22
Revert "Lower `psych` version to enable .yml syncing"
aaronskiba Oct 28, 2024
f600ba0
Remove unused config/locales/ files
aaronskiba Oct 28, 2024
bae62e9
Remove unused locale/ files
aaronskiba Oct 28, 2024
c87ae55
Fix tests: Set `default_locale` to 'en'
aaronskiba Oct 28, 2024
e289fba
Add `scope :search` to ResearchOutput model
aaronskiba Oct 29, 2024
3c5a478
Create `paginable/research_outputs_controller.rb`
aaronskiba Oct 29, 2024
b5d95fb
Update CHANGELOG.md
aaronskiba Oct 30, 2024
8cf8bab
Merge pull request #937 from portagenetwork/aaron/locales-cleanup
aaronskiba Oct 30, 2024
42f9067
Update CHANGELOG.md
aaronskiba Oct 30, 2024
c1b2411
Update CHANGELOG.md (and fix comment)
aaronskiba Oct 24, 2024
7ae2c3b
Add rake task for openid_connect / CILogon cleanup
aaronskiba Nov 1, 2024
7f158a7
Make Rubocop happy
aaronskiba Nov 4, 2024
ca1b58a
Merge branch 'integration' into aaron/add-email-confirmation
aaronskiba Nov 4, 2024
09e731e
Add finalised email confirmation translations
aaronskiba Nov 4, 2024
339a694
Prevent db sync from test, development, & uat
aaronskiba Oct 23, 2024
a47df30
Remove old commented-out code
aaronskiba Oct 23, 2024
9779999
Update CHANGELOG.md
aaronskiba Nov 5, 2024
c6134ae
Update test to match updated translation
aaronskiba Nov 6, 2024
0a2b126
Update CHANGELOG.md
aaronskiba Nov 6, 2024
8670db7
Merge pull request #945 from portagenetwork/aaron/issues/507
aaronskiba Nov 6, 2024
344b823
Merge pull request #944 from portagenetwork/aaron/issues/912
aaronskiba Nov 6, 2024
1ea6214
Merge pull request #923 from portagenetwork/aaron/add-email-confirmation
aaronskiba Nov 6, 2024
cffb456
Change`research_outputs` to local variable
aaronskiba Nov 12, 2024
2aed896
Refactor / add comment
aaronskiba Nov 12, 2024
cb988d7
Merge pull request #938 from portagenetwork/aaron/issues/935
aaronskiba Nov 18, 2024
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
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,33 @@
# Changelog

## [Unreleased]

### Added

- Add rake task for openid_connect / CILogon cleanup [#944](https://github.com/portagenetwork/roadmap/pull/944)

### Changed

- Email Confirmation Changes [#923](https://github.com/portagenetwork/roadmap/pull/923)

- Disable Updating of User Emails [#917](https://github.com/portagenetwork/roadmap/pull/917)

- Apply `translation:sync` to `yaml` Files and Remove Unused `locale/` + `locales/` Files [#937](https://github.com/portagenetwork/roadmap/pull/937)

### Fixed

- Fix User Lookup Via SSO Email: Make Query Case-Insensitive [#924](https://github.com/portagenetwork/roadmap/pull/924)

- Fixes to CILogon / `openid_connect` Tests [#922](https://github.com/portagenetwork/roadmap/pull/922)

- Fix Paginating, Sorting, and Searching Issues Within "Research Outputs" Tab [#938](https://github.com/portagenetwork/roadmap/pull/938)

## [4.1.1+portage-4.2.2] - 2024-09-18

### Changed

- Prevent Uploading of DB Fields to translation.io Within Test, Development, and UAT Environments [#945](https://github.com/portagenetwork/roadmap/pull/945)

- Update Handling of SSO Linking [#907](https://github.com/portagenetwork/roadmap/pull/907)

- Updated SSO button string [portagenetwork/roadmap#903](https://github.com/portagenetwork/roadmap/issues/903)
Expand Down
31 changes: 31 additions & 0 deletions app/controllers/concerns/email_confirmation_handler.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# frozen_string_literal: true

# EmailConfirmationHandler

# 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
# This concern is meant to improve 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
27 changes: 27 additions & 0 deletions app/controllers/paginable/research_outputs_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# frozen_string_literal: true

module Paginable
# Controller for paginating/sorting/searching the research_outputs table
class ResearchOutputsController < ApplicationController
include Paginable

after_action :verify_authorized

# GET /paginable/plans/:plan_id/research_outputs
def index
@plan = Plan.find_by(id: params[:plan_id])
# Same assignment as app/controllers/research_outputs_controller.rb
research_outputs = ResearchOutput.includes(:repositories).where(plan_id: @plan.id)
# Same authorize handling as app/controllers/research_outputs_controller.rb
# `|| ResearchOutput.new(plan_id: @plan.id)` prevents Pundit::NotDefinedError when a direct
# GET /paginable/plans/:id/research_outputs request is made on a plan with 0 research_outputs
authorize(research_outputs.first || ResearchOutput.new(plan_id: @plan.id))
paginable_renderise(
partial: 'index',
scope: research_outputs,
query_params: { sort_field: 'research_outputs.title' },
format: :json
)
end
end
end
56 changes: 8 additions & 48 deletions app/controllers/registrations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ def update
if params[:skip_personal_details] == 'true'
do_update_password(current_user, update_params)
else
do_update(needs_password?(current_user))
do_update
end
else
render(file: File.join(Rails.root, 'public/403.html'), status: 403, layout: false)
Expand All @@ -154,27 +154,15 @@ def update

private

# check if we need password to update user data
# ie if password or email was changed
# extend this as needed
def needs_password?(user)
user.email != update_params[:email] || update_params[:password].present?
end

# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
# rubocop:disable Style/OptionalBooleanParameter
def do_update(require_password = true, confirm = false)
def do_update(confirm = false)
restrict_orgs = Rails.configuration.x.application.restrict_orgs
mandatory_params = true
# added to by below, overwritten otherwise
message = _('Save Unsuccessful. ')
# ensure that the required fields are present

if update_params[:email].blank?
message += _('Please enter an email address. ')
mandatory_params &&= false
end
if update_params[:firstname].blank?
message += _('Please enter a First name. ')
mandatory_params &&= false
Expand All @@ -194,39 +182,11 @@ def do_update(require_password = true, confirm = false)
attrs = update_params
attrs = handle_org(attrs: attrs)

# user is changing email or password
if require_password
# if user is changing email
if current_user.email == attrs[:email]
# remove the current_password because its not actuallyt part of the User record
attrs.delete(:current_password)

# This case is never reached since this method when called with
# require_password = true is because the email changed.
# The case for password changed goes to do_update_password instead
successfully_updated = current_user.update_without_password(attrs)
elsif attrs[:password].blank?
# password needs to be present
message = _('Please enter your password to change email address.')
successfully_updated = false
elsif current_user.valid_password?(attrs[:current_password])
successfully_updated = current_user.update_with_password(attrs)
# rubocop:disable Metrics/BlockNesting
unless successfully_updated
message = _("Save unsuccessful. \
That email address is already registered. \
You must enter a unique email address.")
end
# rubocop:enable Metrics/BlockNesting
else
message = _('Invalid password')
end
else
# password not required
# remove the current_password because its not actuallyt part of the User record
attrs.delete(:current_password)
successfully_updated = current_user.update_without_password(attrs)
end
# password not required
# remove the current_password because its not actuallyt part of the User record
attrs.delete(:current_password)
successfully_updated = current_user.update_without_password(attrs)

else
successfully_updated = false
end
Expand Down Expand Up @@ -296,7 +256,7 @@ def sign_up_params
end

def update_params
params.require(:user).permit(:email, :firstname, :org_id, :language_id,
params.require(:user).permit(:firstname, :org_id, :language_id,
:current_password, :password, :password_confirmation,
:surname, :department_id, :org_id,
:org_name, :org_crosswalk)
Expand Down
5 changes: 5 additions & 0 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 @@ -13,6 +15,9 @@ def create
existing_user = User.find_by(email: params[:user][:email])
unless existing_user.nil?

# See app/controllers/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 = {
Expand Down
3 changes: 1 addition & 2 deletions app/controllers/super_admin/users_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,7 @@ def archive
private

def user_params
params.require(:user).permit(:email,
:firstname,
params.require(:user).permit(:firstname,
:surname,
:org_id, :org_name, :org_crosswalk,
:department_id,
Expand Down
52 changes: 32 additions & 20 deletions app/controllers/users/omniauth_callbacks_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
module Users
# Controller that handles callbacks from OmniAuth integrations (e.g. Shibboleth and ORCID)
class OmniauthCallbacksController < Devise::OmniauthCallbacksController
# This is for the OpenidConnect CILogon
include EmailConfirmationHandler

# rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
# This is for the OpenidConnect CILogon
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
def openid_connect
# First or create
auth = request.env['omniauth.auth']
Expand All @@ -23,23 +24,7 @@ def openid_connect
identifier_scheme = IdentifierScheme.find_by(name: auth.provider)

if current_user.nil? # if user is not signed in (They clicked the SSO sign in button)
if user.nil? # If an entry does not exist in the identifiers table for the chosen SSO account
user = User.create_from_provider_data(auth)
if user.nil? # if a user was NOT created (a match was found for User.find_by(email: auth.info.email)
# Do not link SSO credentials for the signed out, existing user
flash[:alert] = _('That email appears to be associated with an existing account.<br>' \
'Sign into your existing account, and you can link that ' \
"account with SSO from the 'Edit Profile' page.")
redirect_to root_path
return
end
# A new user was created, link the SSO credentials (we can do this for a newly created user)
user.identifiers << Identifier.create(identifier_scheme: identifier_scheme,
value: auth.uid,
attrs: auth,
identifiable: user)
end
sign_in_and_redirect user, event: :authentication
handle_openid_connect_for_signed_out_user(user, auth, identifier_scheme)
elsif user.nil?
# we need to link
current_user.identifiers << Identifier.create(identifier_scheme: identifier_scheme,
Expand All @@ -58,7 +43,7 @@ def openid_connect
redirect_to edit_user_registration_path
end
end
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength

def orcid
handle_omniauth(IdentifierScheme.for_authentication.find_by(name: 'orcid'))
Expand Down Expand Up @@ -140,5 +125,32 @@ def handle_omniauth(scheme)
def failure
redirect_to root_path
end

private

def handle_openid_connect_for_signed_out_user(user, auth, identifier_scheme)
# user.nil? is true if the chosen CILogon email is not currently linked to an existing user account
user = handle_new_sso_email_for_signed_out_user(auth, identifier_scheme) if user.nil?
# See app/controllers/concerns/email_confirmation_handler.rb
return if confirmation_instructions_missing_and_handled?(user)

sign_in_and_redirect user, event: :authentication
end

# This method is executed when a user performs the following two steps:
# 1) clicks "Sign in with institutional or social ID"
# 2) Within CILogon, selects an email that is not currently linked to an existing user account
def handle_new_sso_email_for_signed_out_user(auth, identifier_scheme)
# Find or create the user with user.email == email selected via SSO
user = User.find_or_create_from_provider_data(auth)
if user.confirmed?
# Only link the SSO email if user.email is confirmed
user.identifiers << Identifier.create(identifier_scheme: identifier_scheme,
value: auth.uid,
attrs: auth,
identifiable: user)
end
user
end
end
end
9 changes: 9 additions & 0 deletions app/models/research_output.rb
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,15 @@ class ResearchOutput < ApplicationRecord
# Ensure presence of the :output_type_description if the user selected 'other'
validates_presence_of :output_type_description, if: -> { other? }, message: PRESENCE_MESSAGE

# ==========
# = Scopes =
# ==========

scope :search, lambda { |term|
search_pattern = "%#{term}%"
where('lower(title) LIKE lower(?)', search_pattern)
}

# ====================
# = Instance methods =
# ====================
Expand Down
18 changes: 10 additions & 8 deletions 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 openid_connect]

##
Expand Down Expand Up @@ -185,19 +185,21 @@ def self.from_omniauth(auth)
##
# Handle user creation from provider
# rubocop:disable Metrics/AbcSize
def self.create_from_provider_data(provider_data)
user = User.find_or_initialize_by(email: provider_data.info.email)
def self.find_or_create_from_provider_data(provider_data)
user = User.find_or_initialize_by(email: provider_data.info.email.downcase)

return unless user.new_record?
return user unless user.new_record?

user.update!(
firstname: provider_data.info&.first_name.present? ? provider_data.info.first_name : _('First name'),
surname: provider_data.info&.last_name.present? ? provider_data.info.last_name : _('Last name'),
email: provider_data.info.email,
firstname: provider_data.info&.first_name.presence || _('First name'),
surname: provider_data.info&.last_name.presence || _('Last name'),
# We don't know which organization to setup so we will use other
org: Org.find_by(is_other: true),
accept_terms: true,
password: Devise.friendly_token[0, 20]
password: Devise.friendly_token[0, 20],
# provider_data.info.email comes from CILogon sign-in, which requires email confirmation
# It follows that we can set `confirmed_at: Time.now.utc` and bypass Devise's email confirmation step
confirmed_at: Time.now.utc
)
user
end
Expand Down
14 changes: 7 additions & 7 deletions app/views/devise/registrations/_personal_details.html.erb
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
<%= form_for(resource, namespace: current_user.id, as: resource_name, url: registration_path(resource_name), html: {method: :put, id: 'personal_details_registration_form' }) do |f| %>
<p class="form-control-static">
<%= sanitize _("Please note that your email address is used as your username. If you change this, remember to use your new email address on sign in.") %>
</p>

<p class="form-control-static"><%= _('You can edit any of the details below.') %></p>
<%= hidden_field_tag :unlink_flag, "false", id: 'unlink_flag' %>

<div class="form-group col-xs-8">
<%= f.label(:email, _('Email'), class: 'control-label') %>
<%= f.email_field(:email, class: "form-control", "aria-required": true, value: @user.email) %>
<%= f.email_field(:email, class: "form-control", "aria-required": true, value: @user.email, "disabled": true) %>
<%= hidden_field_tag :original_email, @user.email %>
</div>

<div class="form-group col-xs-12">
<p class="form-control-static"><%= _('You can edit any of the details below.') %></p>
</div>

<%= hidden_field_tag :unlink_flag, "false", id: 'unlink_flag' %>

<div class="form-group col-xs-8">
<%= f.label(:firstname, _('First name'), class: 'control-label') %>
<%= f.text_field(:firstname, class: "form-control", "aria-required": true, value: @user.firstname) %>
Expand Down
2 changes: 1 addition & 1 deletion app/views/super_admin/users/edit.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<%= form_for(@user, namespace: :superadmin, as: :user, url: super_admin_user_path(@user), html: {method: :put, id: 'super_admin_user_edit' }) do |f| %>
<div class="form-group col-xs-12">
<%= f.label(:email, _('Email'), class: 'control-label') %>
<%= f.email_field(:email, class: "form-control", "aria-required": true) %>
<%= f.email_field(:email, class: "form-control", "aria-required": true, "disabled": true) %>
</div>

<div class="form-group col-xs-12">
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
Loading
Loading