diff --git a/Gemfile b/Gemfile index e2300cf..9b79150 100644 --- a/Gemfile +++ b/Gemfile @@ -16,6 +16,14 @@ gem 'mutex_m' # Deals with serialization deprecation in Rails 7.1 gem 'globalize', '~> 7.0' +# Intergates UC_SSO gem +gem 'uc_sso', git: 'git@git.uc.edu:UC-Libraries/uc_sso.git' +# gem 'uc_sso', git: 'https://git.uc.edu/UC-Libraries/uc_sso.git', branch: 'main' +gem 'omniauth-openid' +gem 'omniauth-shibboleth' +# gem 'rails-controller-testing' +gem 'show_me_the_cookies' + # Needed for ruby 3.3.3 upgrade gem 'net-pop', '~> 0.1.2' gem 'net-protocol', '>= 0' diff --git a/Gemfile.lock b/Gemfile.lock index bdc7daa..059b4d6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,3 +1,13 @@ +GIT + remote: git@git.uc.edu:UC-Libraries/uc_sso.git + revision: e782a7693f336c9005721b9c870535b187c22725 + specs: + uc_sso (0.1.0) + devise + omniauth + omniauth-shibboleth + rails (>= 6.0) + GEM remote: https://rubygems.org/ specs: @@ -76,9 +86,7 @@ GEM public_suffix (>= 2.0.2, < 7.0) airbrussh (1.5.3) sshkit (>= 1.6.1, != 1.7.0) - ast (2.4.2) - autoprefixer-rails (10.4.19.0) - execjs (~> 2) + ast (2.4.3) base64 (0.2.0) bcrypt (3.1.20) benchmark (0.4.0) @@ -86,18 +94,17 @@ GEM bindex (0.8.1) bootsnap (1.18.4) msgpack (~> 1.2) - bootstrap (5.3.3) - autoprefixer-rails (>= 9.1.0) + bootstrap (5.3.5) popper_js (>= 2.11.8, < 3) bootstrap-datepicker-rails (1.10.0.1) railties (>= 3.0) - brakeman (7.0.0) + brakeman (7.0.2) racc builder (3.3.0) bundler-audit (0.9.2) bundler (>= 1.2.0, < 3) thor (~> 1.0) - byebug (11.1.3) + byebug (12.0.0) capistrano (3.19.2) airbrussh (>= 1.0.0) i18n @@ -129,9 +136,9 @@ GEM execjs coffee-script-source (1.12.2) concurrent-ruby (1.3.5) - connection_pool (2.5.0) + connection_pool (2.5.3) crass (1.0.6) - csv (3.3.2) + csv (3.3.4) date (3.4.1) devise (4.9.4) bcrypt (~> 3.0) @@ -139,11 +146,11 @@ GEM railties (>= 4.1.0) responders warden (~> 1.2.3) - diff-lcs (1.6.0) + diff-lcs (1.6.2) docile (1.4.1) - dotenv (3.1.7) - dotenv-rails (3.1.7) - dotenv (= 3.1.7) + dotenv (3.1.8) + dotenv-rails (3.1.8) + dotenv (= 3.1.8) railties (>= 6.1) drb (2.2.1) erubi (1.13.1) @@ -153,12 +160,12 @@ GEM factory_bot_rails (6.4.4) factory_bot (~> 6.5) railties (>= 5.0.0) - ffi (1.17.1-aarch64-linux-gnu) - ffi (1.17.1-arm-linux-gnu) - ffi (1.17.1-arm64-darwin) - ffi (1.17.1-x86-linux-gnu) - ffi (1.17.1-x86_64-darwin) - ffi (1.17.1-x86_64-linux-gnu) + ffi (1.17.2-aarch64-linux-gnu) + ffi (1.17.2-arm-linux-gnu) + ffi (1.17.2-arm64-darwin) + ffi (1.17.2-x86-linux-gnu) + ffi (1.17.2-x86_64-darwin) + ffi (1.17.2-x86_64-linux-gnu) globalid (1.2.1) activesupport (>= 6.1) globalize (7.0.0) @@ -167,12 +174,13 @@ GEM activesupport (>= 7.0, < 8.1) request_store (~> 1.0) gritter (1.2.0) - groupdate (6.5.1) - activesupport (>= 7) + groupdate (6.6.0) + activesupport (>= 7.1) + hashie (5.0.0) i18n (1.14.7) concurrent-ruby (~> 1.0) io-console (0.8.0) - irb (1.15.1) + irb (1.15.2) pp (>= 0.6.0) rdoc (>= 4.0.0) reline (>= 0.4.2) @@ -183,14 +191,14 @@ GEM rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) - json (2.10.1) - language_server-protocol (3.17.0.4) + json (2.12.0) + language_server-protocol (3.17.0.5) lint_roller (1.1.0) listen (3.9.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) - logger (1.6.6) - loofah (2.24.0) + logger (1.7.0) + loofah (2.24.1) crass (~> 1.0.2) nokogiri (>= 1.12.0) mail (2.8.1) @@ -201,11 +209,11 @@ GEM marcel (1.0.4) matrix (0.4.2) mini_mime (1.1.5) - minitest (5.25.4) + minitest (5.25.5) msgpack (1.8.0) mutex_m (0.3.0) mysql2 (0.5.6) - net-imap (0.5.6) + net-imap (0.5.8) date net-protocol net-pop (0.1.2) @@ -232,10 +240,19 @@ GEM racc (~> 1.4) nokogiri (1.16.7-x86_64-linux) racc (~> 1.4) + omniauth (2.1.3) + hashie (>= 3.4.6) + rack (>= 2.2.3) + rack-protection + omniauth-openid (2.0.1) + omniauth (>= 1.0, < 3.0) + rack-openid (~> 1.4.0) + omniauth-shibboleth (1.3.0) + omniauth (>= 1.0.0) orm_adapter (0.5.0) ostruct (0.6.1) - parallel (1.26.3) - parser (3.3.7.1) + parallel (1.27.0) + parser (3.3.8.0) ast (~> 2.4.1) racc petergate (3.0.0) @@ -244,14 +261,21 @@ GEM pp (0.6.2) prettyprint prettyprint (0.2.0) - psych (5.2.3) + prism (1.4.0) + psych (5.2.6) date stringio - public_suffix (6.0.1) + public_suffix (6.0.2) puma (6.6.0) nio4r (~> 2.0) racc (1.8.1) - rack (2.2.12) + rack (2.2.14) + rack-openid (1.4.2) + rack (>= 1.1.0) + ruby-openid (>= 2.1.8) + rack-protection (3.2.0) + base64 (>= 0.1.0) + rack (~> 2.2, >= 2.2.4) rack-session (1.0.2) rack (< 3) rack-test (2.2.0) @@ -298,11 +322,11 @@ GEM rb-inotify (0.11.1) ffi (~> 1.0) rb-readline (0.5.5) - rdoc (6.12.0) + rdoc (6.13.1) psych (>= 4.0.0) recaptcha (5.19.0) regexp_parser (2.10.0) - reline (0.6.0) + reline (0.6.1) io-console (~> 0.5) request_store (1.7.0) rack (>= 1.4) @@ -312,10 +336,10 @@ GEM rexml (3.4.1) rspec-core (3.13.3) rspec-support (~> 3.13.0) - rspec-expectations (3.13.3) + rspec-expectations (3.13.4) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-mocks (3.13.2) + rspec-mocks (3.13.4) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) rspec-rails (6.1.5) @@ -326,10 +350,10 @@ GEM rspec-expectations (~> 3.13) rspec-mocks (~> 3.13) rspec-support (~> 3.13) - rspec-support (3.13.2) + rspec-support (3.13.3) rspec_junit_formatter (0.6.0) rspec-core (>= 2, < 4, != 2.12.0) - rubocop (1.73.2) + rubocop (1.75.5) json (~> 2.3) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.1.0) @@ -337,20 +361,22 @@ GEM parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 2.9.3, < 3.0) - rubocop-ast (>= 1.38.0, < 2.0) + rubocop-ast (>= 1.44.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 4.0) - rubocop-ast (1.38.1) - parser (>= 3.3.1.0) - rubocop-rails (2.30.3) + rubocop-ast (1.44.1) + parser (>= 3.3.7.2) + prism (~> 1.4) + rubocop-rails (2.31.0) activesupport (>= 4.2.0) lint_roller (~> 1.1) rack (>= 1.1) - rubocop (>= 1.72.1, < 2.0) + rubocop (>= 1.75.0, < 2.0) rubocop-ast (>= 1.38.0, < 2.0) - rubocop-rspec (3.5.0) + rubocop-rspec (3.6.0) lint_roller (~> 1.1) rubocop (~> 1.72, >= 1.72.1) + ruby-openid (2.9.2) ruby-progressbar (1.13.0) rubyzip (2.4.1) sass (3.7.4) @@ -380,6 +406,8 @@ GEM websocket (~> 1.0) shoulda-matchers (4.5.1) activesupport (>= 4.2.0) + show_me_the_cookies (6.0.0) + capybara (>= 2, < 4) simplecov (0.22.0) docile (~> 1.1) simplecov-html (~> 0.11) @@ -408,7 +436,7 @@ GEM net-sftp (>= 2.1.2) net-ssh (>= 2.8.0) ostruct - stringio (3.1.5) + stringio (3.1.7) thor (1.3.2) tilt (2.6.0) timeout (0.4.3) @@ -479,6 +507,8 @@ DEPENDENCIES net-pop (~> 0.1.2) net-protocol nokogiri (= 1.16.7) + omniauth-openid + omniauth-shibboleth petergate puma (>= 6.4.3) rails (~> 7.2.2.1) @@ -495,11 +525,13 @@ DEPENDENCIES sassc-rails (~> 2.1) selenium-webdriver (~> 4.18.1) shoulda-matchers (~> 4.0) + show_me_the_cookies simplecov simplecov-lcov sqlite3 (~> 1.4) turbolinks (~> 5) tzinfo-data + uc_sso! uglifier (~> 4.2, >= 4.2.1) web-console (>= 3.3.0) diff --git a/app/controllers/callbacks_controller.rb b/app/controllers/callbacks_controller.rb new file mode 100644 index 0000000..8ba6abd --- /dev/null +++ b/app/controllers/callbacks_controller.rb @@ -0,0 +1,106 @@ +# app/controllers/omniauth_callbacks_controller.rb +class CallbacksController < Devise::OmniauthCallbacksController +# include UcSso::ControllerHooks + +def shibboleth + auth = request.env['omniauth.auth'] + Rails.logger.info "omniauth.auth: #{auth.inspect}" + @user = User.from_omniauth(auth) + + Rails.logger.info ">>> HEADERS <<<" + request.env.each do |k, v| + Rails.logger.info "#{k}: #{v}" if k.to_s.start_with?('HTTP_') || k =~ /shib|eppn|mail/i + end + + + if @user.persisted? + sign_in_and_redirect @user, event: :authentication + set_flash_message(:notice, :success, kind: "Shibboleth") if is_navigational_format? + else + session["devise.shibboleth_data"] = auth.except("extra") + redirect_to new_user_registration_url, alert: "There was a problem signing you in through Shibboleth." + end +end + +def passthru + request.env.select { |k, _| k.to_s.include?('HTTP_') || k =~ /shib/i } + Rails.logger.info ">>> HIT PASSTHRU <<<" + Rails.logger.info ">>> HEADERS <<<" + request.env.each do |k, v| + Rails.logger.info "#{k}: #{v}" if k.to_s.start_with?('HTTP_') || k =~ /shib|eppn|mail/i + end + + +# render plain: "You're not authenticated via Shibboleth.", status: 404 + redirect_to user_shibboleth_omniauth_callback_path +end + + private + + def retrieve_shibboleth_attributes + @omni = request.env["omniauth.auth"] + @email = use_uid_if_email_is_blank + end + + def create_or_update_user + unless user_exists? + create_user + send_welcome_email + end + update_user_shibboleth_attributes if user_has_never_logged_in? + update_user_shibboleth_perishable_attributes + end + + def sign_in_shibboleth_user + sign_in_and_redirect @user, event: :authentication # this will throw if @user is not activated + cookies[:login_type] = { + value: "shibboleth", + secure: Rails.env.production? + } + flash[:notice] = "You are now signed in as #{@user.name} (#{@user.email})" + end + + def use_uid_if_email_is_blank + # If user has no email address use their sixplus2@uc.edu instead + # Some test accounts on QA/dev don't have email addresses + return @omni.extra.raw_info.mail if defined?(@omni.extra.raw_info.mail) && @omni.extra.raw_info.mail.present? + @omni.uid + end + + def user_exists? + @user = User.where(provider: @omni['provider'], uid: @omni['uid']).first + end + + def user_has_never_logged_in? + @user.sign_in_count.zero? + end + + def create_user + @user = User.create provider: @omni.provider, + uid: @omni.uid, + email: @email, + password: Devise.friendly_token[0, 20], + profile_update_not_required: false + end + + def update_user_shibboleth_attributes + @user.title = @omni.extra.raw_info.title + @user.telephone = @omni.extra.raw_info.telephoneNumber + @user.first_name = @omni.extra.raw_info.givenName + @user.last_name = @omni.extra.raw_info.sn + @user.save + end + + def update_user_shibboleth_perishable_attributes + @user.uc_affiliation = @omni.extra.raw_info.uceduPrimaryAffiliation + @user.ucdepartment = @omni.extra.raw_info.ou + @user.save + end + + def send_welcome_email + WelcomeMailer.welcome_email(@user).deliver + end + + +end + diff --git a/app/models/user.rb b/app/models/user.rb index 52aa61f..924705a 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -8,12 +8,14 @@ class User < ApplicationRecord ## The multiple option can be set to true if you need users to have multiple roles. ## petergate(roles: %i[root_admin owner viewer manager], multiple: false) ## ############################################################################################ - validates :first_name, :last_name, :email, presence: true + validates :first_name, :last_name, :email, :provider, :uid, :display_name, presence: true validate :allow_uc_domains + # Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable - devise :database_authenticatable, :registerable, - :recoverable, :rememberable, :validatable, password_length: 10..128 + devise :database_authenticatable, :registerable, :omniauthable, + :recoverable, :rememberable, :validatable, + omniauth_providers: [:shibboleth], password_length: 10..128 after_create :send_admin_mail diff --git a/config/deploy/qa.rb b/config/deploy/qa.rb index 63cc16f..af2349b 100644 --- a/config/deploy/qa.rb +++ b/config/deploy/qa.rb @@ -1,16 +1,21 @@ # frozen_string_literal: true set :rails_env, :production +set :branch, 'integration/uc_sso' set :bundle_without, %w[development test].join(' ') -set :branch, 'qa' set :default_env, path: '$PATH:/usr/local/bin' set :bundle_path, -> { shared_path.join('vendor/bundle') } append :linked_dirs, 'tmp', 'log' -ask(:username, nil) -ask(:password, nil, echo: false) -server 'libappstest.libraries.uc.edu', user: fetch(:username), password: fetch(:password), - port: 22, roles: %i[web app db] +server 'libappstest.libraries.uc.edu', + user: 'apache', # or whatever user owns the app + roles: %i[web app db], + port: 22 set :deploy_to, '/opt/webapps/application_portfolio' + +# Optional: forward SSH agent if using deploy keys from your local machine +set :ssh_options, forward_agent: true + after 'deploy:updating', 'ruby_update_check' after 'deploy:updating', 'init_qp' before 'deploy:cleanup', 'start_qp' + diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index b8981fc..8daefc8 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -262,6 +262,36 @@ # up on your models and hooks. # config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo' + config.omniauth :shibboleth, + shib_session_id_field: "Shib-Session-ID", + shib_application_id_field: "Shib-Application-ID", + uid_field: "eppn", + name_field: "displayName", + debug: false, + extra_fields: [ + :cn, + :eppn, + :givenName, + :ou, + :'persistent-id', + :sn, + :street, + :title, + :uceduAffiliation, + :uceduPrimaryAffiliation, + :uceduUCID, + :mail, + :affiliation, + :remoteuser, + :telephoneNumber, + :uceduAcademicProgram, + :uceduFERPACode, + :uceduPrimaryCollege, + :uceduSISPersonID + ] + + + # ==> Warden configuration # If you want to use other strategies, that are not supported by Devise, or # change the failure app, you can configure them inside the config.warden block. diff --git a/config/initializers/uc_sso.rb b/config/initializers/uc_sso.rb new file mode 100644 index 0000000..e9fe8ef --- /dev/null +++ b/config/initializers/uc_sso.rb @@ -0,0 +1,19 @@ +# config/initializers/uc_sso.rb + +require 'uc_sso' +require 'uc_sso/controller_hooks' + +UcSso.configure do |config| + # Shibboleth UID field to match user by (typically "eppn") + config.middleware_options[:uid_field] = 'eppn' + + # Mapping of info fields returned from Shibboleth + config.middleware_options[:info_fields] = { + email: 'mail', + name: 'displayName' + } + + Rails.logger.info ">> UcSso.methods: #{UcSso.methods}" + +end + diff --git a/config/routes.rb b/config/routes.rb index 340f12a..d9994e7 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -7,7 +7,13 @@ get 'seed/new', to: 'file_uploads#new', as: 'file_uploads_new' get 'seed/create', to: 'file_uploads#create', as: 'file_uploads_create' - devise_for :users + # devise_for :users + devise_for :users, controllers: { omniauth_callbacks: 'callbacks' } +# devise_for :users, controllers: { omniauth_callbacks: 'callbacks', registrations: "registrations" } +# devise_for :users, controllers: { omniauth_callbacks: 'users/omniauth_callbacks' } +# devise_for :users, controllers: { omniauth_callbacks: 'users/omniauth_callbacks' }, path: 'application_portfolio/users' + + resources :vendor_records resources :software_types diff --git a/spec/controllers/callbacks_controller_spec.rb b/spec/controllers/callbacks_controller_spec.rb new file mode 100644 index 0000000..92883ac --- /dev/null +++ b/spec/controllers/callbacks_controller_spec.rb @@ -0,0 +1,209 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe CallbacksController do + describe 'omniauth-orcid' do + let(:uid) { 'sixplus2@test.com' } + let(:provider) { :orcid } + + before do + @request.env["devise.mapping"] = Devise.mappings[:user] + omniauth_hash_orcid = { "provider": "orcid", + "uid": "0000-0003-2012-0010", + "info": { + "name": "John Smith", + "email": uid + }, + "credentials": { + "token": "e82938fa-a287-42cf-a2ce-f48ef68c9a35", + "refresh_token": "f94c58dd-b452-44f4-8863-0bf8486a0071", + "expires_at": 1_979_903_874, + "expires": true + }, + "extra": { + } } + OmniAuth.config.add_mock(provider, omniauth_hash_orcid) + request.env["omniauth.auth"] = OmniAuth.config.mock_auth[provider] + end + + context 'with a user who is already logged in' do + let(:user) { FactoryBot.create(:user) } + + before do + allow(controller).to receive(:current_user) { user } + end + + it 'redirects to home path with success notice' do + get provider + expect(response).to redirect_to(root_path) + expect(flash[:notice]).to match(/You have successfully connected with your ORCID record/) + end + end + end + + describe 'omniauth-shibboleth' do + let(:uid) { 'sixplus2@test.com' } + let(:provider) { :shibboleth } + + before do + @request.env["devise.mapping"] = Devise.mappings[:user] + omniauth_hash = { provider: 'shibboleth', + uid: uid, + extra: { + raw_info: { + mail: uid, + title: 'title', + telephoneNumber: '123-456-7890', + givenName: 'Fake', + sn: 'User', + uceduPrimaryAffiliation: 'staff', + ou: 'department' + } + } } + OmniAuth.config.add_mock(provider, omniauth_hash) + request.env["omniauth.auth"] = OmniAuth.config.mock_auth[provider] + end + + context 'with a user who is already logged in' do + let(:user) { FactoryBot.create(:user) } + + before do + controller.stub(:current_user).and_return(user) + end + it 'redirects to the dashboard' do + get provider + expect(response).to redirect_to(Hyrax::Engine.routes.url_helpers.dashboard_path) + end + end + + shared_examples 'Shibboleth login' do + it 'assigns the user and redirects' do + get provider + expect(flash[:notice]).to match(/You are now signed in as */) + expect(cookies[:login_type]).not_to eq(nil) + expect(assigns(:user).email).to eq(email) + expect(assigns(:user).provider).to eq('shibboleth') + expect(assigns(:user).uid).to eq(request.env["omniauth.auth"]["uid"]) + expect(assigns(:user).profile_update_not_required).to eq(false) + expect(response).to be_redirect + end + end + + context 'with a brand new user' do + let(:email) { uid } + + it_behaves_like 'Shibboleth login' + + it 'updates the shibboleth attributes' do + get provider + expect(assigns(:user).title).to eq(request.env["omniauth.auth"]["extra"]["raw_info"]["title"]) + expect(assigns(:user).telephone).to eq(request.env["omniauth.auth"]["extra"]["raw_info"]["telephoneNumber"]) + expect(assigns(:user).first_name).to eq(request.env["omniauth.auth"]["extra"]["raw_info"]["givenName"]) + expect(assigns(:user).last_name).to eq(request.env["omniauth.auth"]["extra"]["raw_info"]["sn"]) + expect(assigns(:user).uc_affiliation).to eq(request.env["omniauth.auth"]["extra"]["raw_info"]["uceduPrimaryAffiliation"]) + expect(assigns(:user).ucdepartment).to eq(request.env["omniauth.auth"]["extra"]["raw_info"]["ou"]) + end + + it 'sends a welcome email' do + ActionMailer::Base.deliveries = [] + get provider + expect(ActionMailer::Base.deliveries.count).to eq(1) + end + end + + context 'with a brand new user when Shibboleth email is not defined' do + before do + omniauth_hash = { provider: 'shibboleth', + uid: uid, + extra: { + raw_info: { + title: 'title', + telephoneNumber: '123-456-7890', + givenName: 'Fake', + sn: 'User', + uceduPrimaryAffiliation: 'staff', + ou: 'department' + } + } } + OmniAuth.config.add_mock(provider, omniauth_hash) + request.env["omniauth.auth"] = OmniAuth.config.mock_auth[provider] + end + let(:email) { uid } + + it_behaves_like 'Shibboleth login' + end + + context 'with a brand new user when Shibboleth email is blank' do + before do + omniauth_hash = { provider: 'shibboleth', + uid: uid, + extra: { + raw_info: { + mail: '', + title: 'title', + telephoneNumber: '123-456-7890', + givenName: 'Fake', + sn: 'User', + uceduPrimaryAffiliation: 'staff', + ou: 'department' + } + } } + OmniAuth.config.add_mock(provider, omniauth_hash) + request.env["omniauth.auth"] = OmniAuth.config.mock_auth[provider] + end + let(:email) { uid } + + it_behaves_like 'Shibboleth login' + end + + context 'with a registered user who has previously logged in' do + let!(:user) { FactoryBot.create(:shibboleth_user, count: 1, profile_update_not_required: false) } + let(:email) { user.email } + + it_behaves_like 'Shibboleth login' + end + + context 'with a registered user who has never logged in' do + let!(:user) { FactoryBot.create(:shibboleth_user, count: 0, profile_update_not_required: false) } + let(:email) { user.email } + + it_behaves_like 'Shibboleth login' + + it 'updates the shibboleth attributes' do + get provider + expect(assigns(:user).title).to eq(request.env["omniauth.auth"]["extra"]["raw_info"]["title"]) + expect(assigns(:user).telephone).to eq(request.env["omniauth.auth"]["extra"]["raw_info"]["telephoneNumber"]) + expect(assigns(:user).first_name).to eq(request.env["omniauth.auth"]["extra"]["raw_info"]["givenName"]) + expect(assigns(:user).last_name).to eq(request.env["omniauth.auth"]["extra"]["raw_info"]["sn"]) + expect(assigns(:user).uc_affiliation).to eq(request.env["omniauth.auth"]["extra"]["raw_info"]["uceduPrimaryAffiliation"]) + expect(assigns(:user).ucdepartment).to eq(request.env["omniauth.auth"]["extra"]["raw_info"]["ou"]) + end + end + + context 'with a registered user who has previously logged in and has updated shibboleth data' do + before do + omniauth_hash = { provider: 'shibboleth', + uid: uid, + extra: { + raw_info: { + uceduPrimaryAffiliation: 'Second Affiliation', + ou: 'Second Department' + } + } } + OmniAuth.config.add_mock(provider, omniauth_hash) + request.env["omniauth.auth"] = OmniAuth.config.mock_auth[provider] + end + + let!(:user) { FactoryBot.create(:shibboleth_user, count: 1, uc_affiliation: "First Affiliation", ucdepartment: "First Department") } + let!(:email) { user.email } + + it 'has the correct metadata' do + get provider + user = User.find(1) + expect(user["uc_affiliation"]).to eq "Second Affiliation" + expect(user["ucdepartment"]).to eq "Second Department" + end + end + end +end diff --git a/spec/factories/users.rb b/spec/factories/users.rb index 3e2e1d5..2d501a9 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -36,4 +36,20 @@ password_confirmation { 'random1234' } roles { 'owner' } end + + factory :shibboleth_user, class: 'User' do + transient do + count { 1 } + person_pid { nil } + end + email { 'sixplus2@test.com' } + password { '12345678' } + first_name { 'Fake' } + last_name { 'User' } + password_confirmation { '12345678' } + sign_in_count { count.to_s } + provider { 'shibboleth' } + uid { 'sixplus2@test.com' } + end + end diff --git a/spec/features/users/uc_shibboleth_spec.rb b/spec/features/users/uc_shibboleth_spec.rb new file mode 100644 index 0000000..ba7b0f4 --- /dev/null +++ b/spec/features/users/uc_shibboleth_spec.rb @@ -0,0 +1,159 @@ + # frozen_string_literal: true + + require 'rails_helper' + + describe 'UC account workflow', type: :feature do + let(:user) { FactoryBot.create(:user) } + let(:password) { FactoryBot.attributes_for(:user).fetch(:password) } + let(:locale) { 'en' } + + describe 'overridden devise password reset page' do + context 'with a uc.edu email address' do + email_address = 'fake.user@uc.edu' + it 'rejects password reset for @.uc.edu user' do + visit new_user_password_path + fill_in('user[email]', with: email_address) + click_on('Send me reset password instructions') + expect(page).to have_content('You cannot reset passwords for @uc.edu accounts. Use your UC Central Login instead') + end + end + + context 'with a non uc.edu email address' do + it 'allows a password reset' do + visit new_user_password_path + fill_in('user[email]', with: user.email) + click_on('Send me reset password instructions') + expect(page).to have_content('You will receive an email with instructions on how to reset your password in a few minutes.') + end + end + + context 'with an invalid email address' do + email_address = 'fake.user@mail.edu' + it 'allows a password reset' do + visit new_user_password_path + fill_in('user[email]', with: email_address) + click_on('Send me reset password instructions') + expect(page).to have_content('Email not found') + end + end + end + + describe 'overridden devise password reset page' do + it 'shows a Central Login option with shibboleth enabled' do + AUTH_CONFIG['shibboleth_enabled'] = true + visit new_user_password_path + expect(page).to have_content('Central Login form') + end + + it 'does not show a Central Login option with shibboleth disabled' do + AUTH_CONFIG['shibboleth_enabled'] = false + visit new_user_password_path + skip "this string displays without regard to shibboleth status" + expect(page).not_to have_content('Central Login form') # This string appears in the help text on the page + end + + it 'does not display the Shared links at the bottom' do + visit new_user_password_path + expect(page).not_to have_link('Sign in', href: '/users/sign_in') + expect(page).not_to have_link('Sign up', href: '/users/sign_up') + end + end + + describe 'overridden devise registration page' do + it 'shows a sign up form if signups are enabled' do + AUTH_CONFIG['signups_enabled'] = true + visit new_user_registration_path + expect(page).to have_field('user[email]') + end + + it 'shows a request link of signups are disabled' do + AUTH_CONFIG['signups_enabled'] = false + visit new_user_registration_path + expect(page).to have_link('use the contact page', href: contact_path(locale: locale)) + end + end + + describe 'overridden devise sign-in page' do + it 'shows a shibboleth login link if shibboleth is enabled' do + AUTH_CONFIG['shibboleth_enabled'] = true + visit new_user_session_path + expect(page).to have_link('Central Login form', href: user_shibboleth_omniauth_authorize_path(locale: locale)) + end + + it 'does not show a shibboleth login link if shibboleth is disabled' do + AUTH_CONFIG['shibboleth_enabled'] = false + visit new_user_session_path + expect(page).not_to have_link('Central Login form', href: user_shibboleth_omniauth_authorize_path(locale: locale)) + end + + it 'shows a signup link if signups are enabled' do + AUTH_CONFIG['signups_enabled'] = true + visit new_user_session_path + expect(page).to have_link('Sign up', href: new_user_registration_path(locale: locale)) + end + + it 'does not show signup link if signups are disabled' do + AUTH_CONFIG['signups_enabled'] = false + visit new_user_session_path + expect(page).not_to have_link('Sign up', href: new_user_registration_path(locale: locale)) + end + end + + describe 'shibboleth login page' do + context 'when shibboleth is enabled' do + before do + AUTH_CONFIG['shibboleth_enabled'] = true + visit login_path + end + + it 'shows a shibboleth login link and local login link' do + expect(page).to have_link('UC Central Login username', href: 'https://www.uc.edu/distance/Student_Orientation/One_Stop_Student_Resources/central-log-in-.html') + expect(page).to have_link('log in using a local account', href: new_user_session_path + '?locale=en') + end + end + + context 'when shibboleth is not enabled' do + before do + AUTH_CONFIG['shibboleth_enabled'] = false + visit login_path + end + + it 'shows the local log in page' do + expect(page).to have_field('user[email]') + end + end + end + + describe 'shibboleth password management' do + it 'hides the password change fields for shibboleth users' do + login_as(user) + user.provider = 'shibboleth' + visit hyrax.edit_dashboard_profile_path(user) + expect(page).not_to have_field('user[password]') + expect(page).not_to have_field('user[password_confirmation]') + end + end + + describe 'home page login button' do + it 'shows the correct login link' do + visit root_path + expect(page).to have_link('Login', href: login_path + '?locale=en') + end + end + + describe 'a user using a UC Shibboleth login' do + it "redirects to the UC Shibboleth logout page after logout" do + create_cookie('login_type', 'shibboleth') + visit('/users/sign_out') + expect(page).to have_content("You have been logged out of the University of Cincinnati's Login Service") + end + end + + describe 'a user using a local login' do + it "redirects to the home page after logout" do + create_cookie('login_type', 'local') + visit('/users/sign_out') + expect(page).to have_title("Scholar@UC") + end + end + end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 4d1e28b..4f23889 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -72,6 +72,9 @@ # config.filter_gems_from_backtrace("gem name") config.include Devise::Test::ControllerHelpers, type: :view config.include Devise::Test::ControllerHelpers, type: :controller + # Allow cookies to be set in feature tests (for UC Shibboleth testing) + config.include ShowMeTheCookies, type: :feature + end Shoulda::Matchers.configure do |config| diff --git a/spec/support/test_routes.rb b/spec/support/test_routes.rb new file mode 100644 index 0000000..019658b --- /dev/null +++ b/spec/support/test_routes.rb @@ -0,0 +1,13 @@ + # frozen_string_literal: true + + class ShibbolethLogoutController < ApplicationController + def show + render plain: "You have been logged out of the University of Cincinnati's Login Service" + end + end + + test_routes = proc do + get '/Shibboleth.sso/Logout' => 'shibboleth_logout#show' + end + + Rails.application.routes.send :eval_block, test_routes