From 024ecf534544641e03d51b2063de98c7535633dd Mon Sep 17 00:00:00 2001 From: Rob Kaufman Date: Tue, 8 Aug 2023 02:20:43 -0500 Subject: [PATCH] SSO Interface and Connection (#652) * working saml, cas, probably openid_connect. shibboleth gets to idp and logs in but looses session on the way back merge all the omniauth related code together solve saml metadata issue, put in saml redirect loading page, put in choose your sso page remove shibboleth since it can be done with saml and saml is more configurable * update gemfile order * Update app/models/identity_provider.rb Co-authored-by: Alisha Evans * remove old spec and leftovers --------- Co-authored-by: leaannbradford Co-authored-by: Alisha Evans --- .gitguardian.yaml | 5 + Gemfile | 201 +++++++----------- Gemfile.lock | 99 ++++++++- app/assets/javascripts/application.js | 2 - app/assets/javascripts/hyku/auth_provider.js | 28 --- app/assets/stylesheets/application.css | 1 + app/assets/stylesheets/single_signon.scss | 44 ++++ .../identity_providers_controller.rb | 98 +++++++++ app/controllers/single_signon_controller.rb | 9 + .../users/omniauth_callbacks_controller.rb | 40 ++++ app/models/concerns/account_settings.rb | 2 +- app/models/identity_provider.rb | 19 ++ app/models/user.rb | 15 +- app/uploaders/logo_uploader.rb | 18 ++ app/views/_user_util_links.html.erb | 2 +- .../omniauth_callbacks/complete.html.erb | 9 + app/views/devise/shared/_links.html.erb | 8 +- .../dashboard/sidebar/_configuration.html.erb | 4 + app/views/identity_providers/_form.html.erb | 59 +++++ app/views/identity_providers/edit.html.erb | 5 + app/views/identity_providers/index.html.erb | 37 ++++ app/views/identity_providers/new.html.erb | 5 + app/views/shared/_footer.html.erb | 2 +- app/views/single_signon/index.html.erb | 22 ++ config/application.rb | 7 + config/initializers/apartment.rb | 3 +- config/initializers/devise.rb | 18 +- config/initializers/is_it_working.rb | 4 - config/locales/en.yml | 14 +- config/locales/hyrax.en.yml | 2 +- config/routes.rb | 53 +++-- ..._print_pending_relationships.iiif_print.rb | 14 +- .../20230727180717_add_omniauth_to_users.rb | 6 + ...0230804073106_create_identity_providers.rb | 12 ++ db/schema.rb | 12 ++ docker-compose.yml | 4 + lib/omni_auth/strategies/saml_decorator.rb | 54 +++++ spec/factories/identity_providers.rb | 9 + spec/models/identity_provider_spec.rb | 28 +++ spec/requests/single_signon_request_spec.rb | 25 +++ spec/routing/auth_providers_routing_spec.rb | 29 --- 41 files changed, 797 insertions(+), 231 deletions(-) create mode 100644 .gitguardian.yaml delete mode 100644 app/assets/javascripts/hyku/auth_provider.js create mode 100644 app/assets/stylesheets/single_signon.scss create mode 100644 app/controllers/identity_providers_controller.rb create mode 100644 app/controllers/single_signon_controller.rb create mode 100644 app/controllers/users/omniauth_callbacks_controller.rb create mode 100644 app/models/identity_provider.rb create mode 100644 app/uploaders/logo_uploader.rb create mode 100644 app/views/devise/omniauth_callbacks/complete.html.erb create mode 100644 app/views/identity_providers/_form.html.erb create mode 100644 app/views/identity_providers/edit.html.erb create mode 100644 app/views/identity_providers/index.html.erb create mode 100644 app/views/identity_providers/new.html.erb create mode 100644 app/views/single_signon/index.html.erb delete mode 100644 config/initializers/is_it_working.rb create mode 100644 db/migrate/20230727180717_add_omniauth_to_users.rb create mode 100644 db/migrate/20230804073106_create_identity_providers.rb create mode 100644 lib/omni_auth/strategies/saml_decorator.rb create mode 100644 spec/factories/identity_providers.rb create mode 100644 spec/models/identity_provider_spec.rb create mode 100644 spec/requests/single_signon_request_spec.rb delete mode 100644 spec/routing/auth_providers_routing_spec.rb diff --git a/.gitguardian.yaml b/.gitguardian.yaml new file mode 100644 index 000000000..6169cdf7d --- /dev/null +++ b/.gitguardian.yaml @@ -0,0 +1,5 @@ +secret: + ignored-matches: + - match: 2ace7433e96955aeed1a310d7dcc61f8761d05fbff91b92d79d860e307d6ea6a + name: Generic High Entropy Secret - .env +version: 2 diff --git a/Gemfile b/Gemfile index c7a35e799..02d1b6ac2 100644 --- a/Gemfile +++ b/Gemfile @@ -3,143 +3,106 @@ source 'https://rubygems.org' # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' gem 'rails', '~> 5.2.5' -gem 'activerecord-nulldb-adapter' -# Use sqlite3 as the database for Active Record -gem 'pg' -# Use Puma as the app server -gem 'puma', '~> 4.3' -# Use SCSS for stylesheets -gem 'sass-rails', '~> 5.0' -# Use CoffeeScript for .coffee assets and views -gem 'coffee-rails', '~> 4.2' -# See https://github.com/rails/execjs#readme for more supported runtimes -# gem 'therubyracer', platforms: :ruby -# Use jquery as the JavaScript library -gem 'jquery-rails' -# Turbolinks makes following links in your web application faster. Read more: https://github.com/rails/turbolinks -gem 'turbolinks', '~> 5' -# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder -gem 'jbuilder', '~> 2.5' -# bundle exec rake doc:rails generates the API under doc/api. -# gem 'sdoc', '~> 0.4.0', group: :doc -# Use ActiveModel has_secure_password -# gem 'bcrypt', '~> 3.1.7' -gem 'active-fedora', '>= 11.1.4' -gem 'flutie' -# Use Capistrano for deployment -# gem 'capistrano-rails', group: :development -group :development, :test do - # Call 'byebug' anywhere in the code to stop execution and get a debugger console - gem 'byebug' - gem 'pry-byebug' - gem 'i18n-debug', require: false - gem 'i18n-tasks' - gem 'rspec' - gem 'rspec-rails', '>= 3.6.0' - gem 'coveralls', '~> 0.8', '>= 0.8.23', require: false - gem 'simplecov', require: false - gem 'fcrepo_wrapper', '~> 0.4' - gem 'solr_wrapper', '~> 2.0' - - gem 'rubocop', '~> 0.50', '<= 0.52.1' - gem 'rubocop-rspec', '~> 1.22', '<= 1.22.2' - # gem 'xray-rails' -end - -group :test do - gem 'capybara' - gem 'capybara-screenshot', '~> 1.0' - gem 'database_cleaner' - gem 'factory_bot_rails' - gem 'launchy' - # rack-test >= 0.71 does not work with older Capybara versions (< 2.17). See #214 for more details - gem 'rack-test', '0.7.0' - gem 'rails-controller-testing' - gem 'rspec-activemodel-mocks' - gem 'rspec-its' - gem 'rspec_junit_formatter' - gem 'rspec-retry' - gem 'semaphore_test_boosters' - gem 'selenium-webdriver', '3.142.7' - gem 'shoulda-matchers', '~> 4.0' - gem 'webdrivers', '~> 4.0' - gem 'webmock' -end -group :development do - # Access an IRB console on exception pages or by using <%= console %> in views - gem 'web-console', '>= 3.3.0' - gem 'listen', '>= 3.0.5', '< 3.2' - # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring - gem 'easy_translate' - gem 'scss_lint', require: false - gem 'spring', '~> 1.7' - gem 'spring-watcher-listen', '~> 2.0.0' -end - -gem 'bulkrax', '~> 5.0' +gem 'active-fedora', '>= 11.1.4' +gem 'active_elastic_job', github: 'active-elastic-job/active-elastic-job', ref: 'ec51c5d9dedc4a1b47f2db41f26d5fceb251e979', group: %i[aws] +gem 'activejob-scheduler', github: 'notch8/activejob-scheduler' +gem 'activerecord-nulldb-adapter' +gem 'addressable', '2.8.1' +gem 'apartment' +gem 'aws-sdk-sqs', group: %i[aws] gem 'blacklight', '~> 6.7' +gem 'blacklight_advanced_search' gem 'blacklight_oai_provider', '~> 6.1', '>= 6.1.1' - -gem 'hyrax', git: 'https://github.com/samvera/hyrax.git', branch: 'privkey_for_ga4_3.x' #'~> 3.6.0' -gem 'addressable', '2.8.1' +gem 'blacklight_range_limit' gem 'bolognese', '>= 1.9.10' -gem 'hyrax-doi', git: 'https://github.com/samvera-labs/hyrax-doi.git', branch: 'hyrax_upgrade' -gem 'hyrax-iiif_av', git: 'https://github.com/samvera-labs/hyrax-iiif_av.git', branch: 'main' -gem 'iiif_print', git: 'https://github.com/scientist-softserv/iiif_print.git', branch: 'main' -gem 'postrank-uri', '>= 1.0.24' -gem 'redlock', '>= 0.1.2', '< 2.0' # lock redlock per https://github.com/samvera/hyrax/pull/5961 -gem 'rsolr', '~> 2.0' - +gem 'bootstrap-datepicker-rails' +gem 'bulkrax', '~> 5.0' +gem 'byebug', group: %i[development test] +gem 'capybara', group: %i[test] +gem 'capybara-screenshot', '~> 1.0', group: %i[test] +gem 'carrierwave-aws', '~> 1.3', group: %i[aws test] +gem 'cocoon' +gem 'codemirror-rails' +gem 'coffee-rails', '~> 4.2' # Use CoffeeScript for .coffee assets and views +gem 'config', '~> 2.2', '>= 2.2.1' +gem 'coveralls', '~> 0.8', '>= 0.8.23', require: false, group: %i[development test] +gem 'database_cleaner', group: %i[test] gem 'devise' gem 'devise-guests', '~> 0.3' gem 'devise-i18n' gem 'devise_invitable', '~> 1.6' -gem 'apartment' -gem 'config', '~> 2.2', '>= 2.2.1' -gem 'is_it_working' -gem 'rolify' +gem 'dry-monads', '~> 1.4.0' # Locked because 1.5.0 was not compatible with Hyrax v.3.4.2 +gem 'easy_translate', group: %i[development] +gem 'factory_bot_rails', group: %i[test] +gem 'fcrepo_wrapper', '~> 0.4', group: %i[development test] gem 'flipflop', '~> 2.3' +gem 'flutie' +gem 'hyrax', github: 'samvera/hyrax', branch: 'privkey_for_ga4_3.x' #'~> 3.6.0' +gem 'hyrax-doi', github: 'samvera-labs/hyrax-doi', branch: 'main' +gem 'hyrax-iiif_av', github: 'samvera-labs/hyrax-iiif_av', branch: 'main' +gem 'i18n-debug', require: false, group: %i[development test] +gem 'i18n-tasks', group: %i[development test] +gem 'iiif_print', github: 'scientist-softserv/iiif_print', branch: 'main' +gem 'jbuilder', '~> 2.5' +gem 'jquery-rails' # Use jquery as the JavaScript library +gem 'launchy', group: %i[test] +gem 'listen', '>= 3.0.5', '< 3.2', group: %i[development] gem 'lograge' gem 'mods', '~> 2.4' -group :aws, :test do - gem 'carrierwave-aws', '~> 1.3' -end -group :aws do - gem 'active_elastic_job'#, git: 'https://github.com/active-elastic-job/active-elastic-job' - gem 'aws-sdk-sqs' -end -gem 'activejob-scheduler', git: 'https://github.com/notch8/activejob-scheduler.git' -gem 'bootstrap-datepicker-rails' -gem "cocoon" -gem 'codemirror-rails' gem 'negative_captcha' gem 'okcomputer' +gem 'omniauth-cas', github: 'stanhu/omniauth-cas', ref: '4211e6d05941b4a981f9a36b49ec166cecd0e271' +gem 'omniauth-multi-provider' +gem 'omniauth-rails_csrf_protection', '~> 1.0' +gem 'omniauth-saml', '~> 2.1' +gem 'omniauth_openid_connect' +gem 'order_already', '~> 0.3.1' gem 'parser', '~> 2.5.3' -gem 'rdf', '~> 3.1.15' # rdf 3.2.0 removed SerializedTransaction which ldp requires -gem 'riiif', '~> 1.1' -gem 'secure_headers' -gem "sentry-raven" # April ToDo: Need to take out once the transfer is complete to Sentry.io -gem 'sentry-ruby' -gem 'sentry-rails' -gem 'sidekiq' -gem 'terser' # to support the Safe Navigation / Optional Chaining operator (?.) and avoid uglifier precompile issue -gem 'tether-rails' +gem 'pg' +gem 'postrank-uri', '>= 1.0.24' gem 'progress_bar' -# Pronto adds comments to MRs in gitlab when rubocop offenses are made. gem 'pronto' gem 'pronto-brakeman', require: false gem 'pronto-flay', require: false gem 'pronto-rails_best_practices', require: false gem 'pronto-rails_schema', require: false gem 'pronto-rubocop', require: false - -gem "order_already", "~> 0.3.1" -gem "redcarpet" -# When first attempting to upgrade to Hyrax v3.4.2, this dry-monads gem was upgraded to v1.5.0. -# This version threw the following error: -# NameError: uninitialized constant Dry::Monads::Result::Transformer -# Locking it to v1.4.x does not throw an error. -gem 'dry-monads', '~> 1.4.0' -gem 'blacklight_range_limit' -gem "blacklight_advanced_search" +gem 'pry-byebug', group: %i[development test] +gem 'puma', '~> 4.3' # Use Puma as the app server +gem 'rack-test', '0.7.0', group: %i[test] # rack-test >= 0.71 does not work with older Capybara versions (< 2.17). See #214 for more details +gem 'rails-controller-testing', group: %i[test] +gem 'rdf', '~> 3.1.15' # rdf 3.2.0 removed SerializedTransaction which ldp requires +gem 'redcarpet' +gem 'redlock', '>= 0.1.2', '< 2.0' # lock redlock per https://github.com/samvera/hyrax/pull/5961 +gem 'riiif', '~> 1.1' +gem 'rolify' +gem 'rsolr', '~> 2.0' +gem 'rspec', group: %i[development test] +gem 'rspec-activemodel-mocks', group: %i[test] +gem 'rspec-its', group: %i[test] +gem 'rspec-rails', '>= 3.6.0', group: %i[development test] +gem 'rspec-retry', group: %i[test] +gem 'rspec_junit_formatter', group: %i[test] +gem 'rubocop', '~> 0.50', '<= 0.52.1', group: %i[development test] +gem 'rubocop-rspec', '~> 1.22', '<= 1.22.2', group: %i[development test] +gem 'sass-rails', '~> 5.0' # Use SCSS for stylesheets +gem 'scss_lint', require: false, group: %i[development] +gem 'secure_headers' +gem 'selenium-webdriver', '3.142.7', group: %i[test] +gem 'semaphore_test_boosters', group: %i[test] +gem 'sentry-rails' +gem 'sentry-raven' # April ToDo: Need to take out once the transfer is complete to Sentry.io +gem 'sentry-ruby' +gem 'shoulda-matchers', '~> 4.0', group: %i[test] +gem 'sidekiq' +gem 'simplecov', require: false, group: %i[development test] +gem 'solr_wrapper', '~> 2.0', group: %i[development test] +gem 'spring', '~> 1.7', group: %i[development] +gem 'spring-watcher-listen', '~> 2.0.0', group: %i[development] +gem 'terser' # to support the Safe Navigation / Optional Chaining operator (?.) and avoid uglifier precompile issue +gem 'tether-rails' +gem 'turbolinks', '~> 5' +gem 'web-console', '>= 3.3.0', group: %i[development] # <%= console %> in views +gem 'webdrivers', '~> 4.7.0', group: %i[test] +gem 'webmock', group: %i[test] diff --git a/Gemfile.lock b/Gemfile.lock index 24a19de9c..db7c47a95 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,3 +1,12 @@ +GIT + remote: https://github.com/active-elastic-job/active-elastic-job.git + revision: ec51c5d9dedc4a1b47f2db41f26d5fceb251e979 + ref: ec51c5d9dedc4a1b47f2db41f26d5fceb251e979 + specs: + active_elastic_job (2.0.1) + aws-sdk-sqs (~> 1) + rails (>= 4.2) + GIT remote: https://github.com/notch8/activejob-scheduler.git revision: 22850499dd0ab046eff2e1c113261782e7b8d8dc @@ -11,11 +20,11 @@ GIT GIT remote: https://github.com/samvera-labs/hyrax-doi.git - revision: f29b3ef86e7feb12509cf2c4608817bb275f097e - branch: hyrax_upgrade + revision: d494a50ef8ce3eae594c7ed7148c33b3c977d4a7 + branch: main specs: hyrax-doi (0.2.0) - bolognese (>= 1.8.6, < 1.10) + bolognese (~> 1.8, >= 1.8.6) flipflop (~> 2.3) hyrax (>= 2.9, < 4.0) rails (~> 5.2.4, >= 5.2.4.3) @@ -109,6 +118,16 @@ GIT rails (~> 5.0) rdf-vocab (~> 3.0) +GIT + remote: https://github.com/stanhu/omniauth-cas.git + revision: 4211e6d05941b4a981f9a36b49ec166cecd0e271 + ref: 4211e6d05941b4a981f9a36b49ec166cecd0e271 + specs: + omniauth-cas (2.0.0) + addressable (~> 2.3) + nokogiri (~> 1.5) + omniauth (>= 1.2, < 3) + GEM remote: https://rubygems.org/ specs: @@ -150,9 +169,6 @@ GEM activesupport (>= 3.0.0) rdf (>= 2.0.2, < 4.0) rdf-vocab (>= 2.0, < 4.0) - active_elastic_job (3.2.0) - aws-sdk-sqs (~> 1) - rails (>= 5.2.6, < 7.1) active_encode (0.8.2) rails sprockets (< 4) @@ -184,6 +200,7 @@ GEM tzinfo (~> 1.1) addressable (2.8.1) public_suffix (>= 2.0.2, < 6.0) + aes_key_wrap (1.1.0) almond-rails (0.3.0) rails (>= 4.2) amazing_print (1.5.0) @@ -194,6 +211,7 @@ GEM rack (>= 1.3.6) arel (9.0.0) ast (2.4.2) + attr_required (1.0.1) autoprefixer-rails (10.4.13.0) execjs (~> 2) awesome_nested_set (3.5.0) @@ -238,6 +256,7 @@ GEM smart_properties bibtex-ruby (6.0.0) latex-decode (~> 0.0) + bindata (2.4.15) bindex (0.8.1) blacklight (6.25.0) bootstrap-sass (~> 3.2) @@ -662,7 +681,6 @@ GEM json iiif_manifest (1.3.1) activesupport (>= 4) - is_it_working (1.1.0) iso-639 (0.3.5) iso8601 (0.9.1) jbuilder (2.11.5) @@ -682,6 +700,11 @@ GEM railties (>= 3.2.16) json (2.6.3) json-canonicalization (0.3.2) + json-jwt (1.15.3) + activesupport (>= 4.2) + aes_key_wrap + bindata + httpclient json-ld (3.1.10) htmlentities (~> 4.3) json-canonicalization (~> 0.2) @@ -869,6 +892,32 @@ GEM oj (3.13.21) oj_mimic_json (1.0.1) okcomputer (1.18.4) + omniauth (2.1.1) + hashie (>= 3.4.6) + rack (>= 2.2.3) + rack-protection + omniauth-multi-provider (0.4.0) + omniauth + omniauth-rails_csrf_protection (1.0.1) + actionpack (>= 4.2) + omniauth (~> 2.0) + omniauth-saml (2.1.0) + omniauth (~> 2.0) + ruby-saml (~> 1.12) + omniauth_openid_connect (0.6.1) + omniauth (>= 1.9, < 3) + openid_connect (~> 1.1) + openid_connect (1.4.2) + activemodel + attr_required (>= 1.0.0) + json-jwt (>= 1.15.0) + net-smtp + rack-oauth2 (~> 1.21) + swd (~> 1.3) + tzinfo + validate_email + validate_url + webfinger (~> 1.2) openseadragon (0.6.0) rails (> 3.2.0) optimist (3.0.1) @@ -937,6 +986,14 @@ GEM raabro (1.4.0) racc (1.7.1) rack (2.2.8) + rack-oauth2 (1.21.3) + activesupport + attr_required + httpclient + json-jwt (>= 1.11.0) + rack (>= 2.1.0) + rack-protection (3.1.0) + rack (~> 2.2, >= 2.2.4) rack-test (0.7.0) rack (>= 1.0, < 3) rails (5.2.8.1) @@ -1131,6 +1188,9 @@ GEM multipart-post oauth2 ruby-progressbar (1.13.0) + ruby-saml (1.15.0) + nokogiri (>= 1.13.10) + rexml ruby2_keywords (0.0.5) ruby_dep (1.5.0) ruby_parser (3.19.1) @@ -1246,6 +1306,10 @@ GEM activesupport (>= 5.2) sprockets (>= 3.0.0) ssrf_filter (1.0.8) + swd (1.3.0) + activesupport (>= 3) + attr_required (>= 0.0.5) + httpclient (>= 2.4) sxp (1.1.0) rdf (~> 3.1) sync (0.5.0) @@ -1286,6 +1350,12 @@ GEM unicode-types (1.8.0) unicode_utils (1.4.0) validatable (1.6.7) + validate_email (0.1.6) + activemodel (>= 3.0) + mail (>= 2.2.5) + validate_url (1.0.15) + activemodel (>= 3.0.0) + public_suffix valkyrie (2.2.0) activemodel activesupport @@ -1313,6 +1383,9 @@ GEM nokogiri (~> 1.6) rubyzip (>= 1.3.0) selenium-webdriver (> 3.141, < 5.0) + webfinger (1.2.0) + activesupport + httpclient (>= 2.4) webmock (3.18.1) addressable (>= 2.8.0) crack (>= 0.3.2) @@ -1323,7 +1396,7 @@ GEM websocket-extensions (0.1.5) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.6.11) + zeitwerk (2.6.8) PLATFORMS aarch64-linux-musl @@ -1331,7 +1404,7 @@ PLATFORMS DEPENDENCIES active-fedora (>= 11.1.4) - active_elastic_job + active_elastic_job! activejob-scheduler! activerecord-nulldb-adapter addressable (= 2.8.1) @@ -1370,7 +1443,6 @@ DEPENDENCIES i18n-debug i18n-tasks iiif_print! - is_it_working jbuilder (~> 2.5) jquery-rails launchy @@ -1379,6 +1451,11 @@ DEPENDENCIES mods (~> 2.4) negative_captcha okcomputer + omniauth-cas! + omniauth-multi-provider + omniauth-rails_csrf_protection (~> 1.0) + omniauth-saml (~> 2.1) + omniauth_openid_connect order_already (~> 0.3.1) parser (~> 2.5.3) pg @@ -1427,7 +1504,7 @@ DEPENDENCIES tether-rails turbolinks (~> 5) web-console (>= 3.3.0) - webdrivers (~> 4.0) + webdrivers (~> 4.7.0) webmock BUNDLED WITH diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 288ab4bcd..2837ce782 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -34,7 +34,6 @@ //= require hyku/admin/appearance/themes //= require hyku/groups/per_page //= require hyku/groups/add_member -//= require hyku/auth_provider //= require proprietor //= require bootstrap_carousel //= require bootstrap-datepicker @@ -55,4 +54,3 @@ //= require 'blacklight_range_limit' //= require blacklight_advanced_search - diff --git a/app/assets/javascripts/hyku/auth_provider.js b/app/assets/javascripts/hyku/auth_provider.js deleted file mode 100644 index ca88f6767..000000000 --- a/app/assets/javascripts/hyku/auth_provider.js +++ /dev/null @@ -1,28 +0,0 @@ -$(document).on('turbolinks:load ready', function() { - var provider = $('#auth_provider_provider').val(); - if (provider == 'saml') { - $('#saml-fields').show(); - $('#oidc-fields').hide(); - } else if (provider == 'oidc') { - $('#saml-fields').hide(); - $('#oidc-fields').show(); - } else { - $('#saml-fields').hide(); - $('#oidc-fields').hide(); - } - - $('body').on('change', '#auth_provider_provider', function(e) { - provider = $(this).val(); - if (provider == 'saml') { - $('#saml-fields').show(); - $('#oidc-fields').hide(); - } else if (provider == 'oidc') { - $('#saml-fields').hide(); - $('#oidc-fields').show(); - } else { - $('#saml-fields').hide(); - $('#oidc-fields').hide(); - } - }); -}); - diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index 089a7d3ec..e105d523f 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -17,6 +17,7 @@ *= require hyrax *= require dataTables/bootstrap/3/jquery.dataTables.bootstrap *= require bootstrap-datepicker + *= require single_signon *= require_self * diff --git a/app/assets/stylesheets/single_signon.scss b/app/assets/stylesheets/single_signon.scss new file mode 100644 index 000000000..b5cd1fd62 --- /dev/null +++ b/app/assets/stylesheets/single_signon.scss @@ -0,0 +1,44 @@ +// Place all the styles related to the SingleSignon controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: http://sass-lang.com/ + +.col-centered { + background-color: #ff0000; + margin: 80px auto; + text-align: center; +} + +.sso-button { + background-color: #ffffff; + border: 1px solid; + margin: 100px 0 200px; + min-height: 300px; + padding: 20px 50px; +} + +.sso-button-fake { + margin-top: 100px; +} + +.loader { + -webkit-animation: spin 2s linear infinite; // Safari + animation: spin 2s linear infinite; + border-radius: 50%; + border-top: 16px solid #0a1f61; + border: 16px solid #f3f3f3; + color: #f3f3f3; + font-size: 11px; + height: 120px; + margin: 55px auto 150px; + text-indent: -99999em; + width: 120px; +} +// Safari +@-webkit-keyframes spin { + 0% { -webkit-transform: rotate(0deg); } + 100% { -webkit-transform: rotate(360deg); } +} +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} diff --git a/app/controllers/identity_providers_controller.rb b/app/controllers/identity_providers_controller.rb new file mode 100644 index 000000000..28774a0bc --- /dev/null +++ b/app/controllers/identity_providers_controller.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +class IdentityProvidersController < ApplicationController + layout 'hyrax/dashboard' + + before_action :ensure_admin! + before_action :set_identity_provider, only: %i[edit update destroy] + + def index + @identity_providers = IdentityProvider.all + end + + # GET /identity_providers/new + def new + add_breadcrumbs + @identity_provider = IdentityProvider.new + end + + # GET /identity_providers/1/edit + def edit + add_breadcrumbs + end + + # POST /identity_providers or /identity_providers.json + def create + @identity_provider = IdentityProvider.new(identity_provider_params) + respond_to do |format| + if @identity_provider.save + format.html do + redirect_to edit_identity_provider_url(@identity_provider), + notice: "Auth provider was successfully created." + end + format.json { render :show, status: :created, location: @identity_provider } + else + format.html { render :new, status: :unprocessable_entity } + format.json { render json: @identity_provider.errors, status: :unprocessable_entity } + end + end + end + + # PATCH/PUT /identity_providers/1 or /identity_providers/1.json + def update + respond_to do |format| + if @identity_provider.update(identity_provider_params) + format.html do + redirect_to edit_identity_provider_url(@identity_provider), + notice: "Auth provider was successfully updated." + end + format.json { render :show, status: :ok, location: @identity_provider } + else + format.html { render :edit, status: :unprocessable_entity } + format.json { render json: @identity_provider.errors, status: :unprocessable_entity } + end + end + end + + # DELETE /identity_providers/1 or /identity_providers/1.json + def destroy + @identity_provider.destroy + respond_to do |format| + format.html { redirect_to new_identity_provider_url, notice: "Auth provider was successfully destroyed." } + format.json { head :no_content } + end + end + + def add_breadcrumbs + add_breadcrumb t(:'hyrax.controls.home'), root_path + add_breadcrumb t(:'hyrax.dashboard.breadcrumbs.admin'), hyrax.dashboard_path + add_breadcrumb t(:'hyrax.admin.sidebar.configuration'), '#' + add_breadcrumb t(:'hyrax.admin.sidebar.identity_provider'), request.path + end + + private + + # Use callbacks to share common setup or constraints between actions. + def set_identity_provider + @identity_provider = IdentityProvider.find(params[:id]) + end + + def ensure_admin! + authorize! :read, :admin_dashboard + end + + # Only allow a list of trusted parameters through. + def identity_provider_params + return @identity_provider_params if @identity_provider_params + @identity_provider_params = params.require(:identity_provider).permit( + :name, + :provider, + :options, + :logo_image, + :logo_image_text + ) + @identity_provider_params['options'].presence && + @identity_provider_params['options'] = JSON.parse(@identity_provider_params['options']) + @identity_provider_params + end +end diff --git a/app/controllers/single_signon_controller.rb b/app/controllers/single_signon_controller.rb new file mode 100644 index 000000000..a17818a56 --- /dev/null +++ b/app/controllers/single_signon_controller.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class SingleSignonController < DeviseController + def index + @identity_providers = IdentityProvider.all + render && return unless @identity_providers.count.zero? + redirect_to main_app.new_user_session_path + end +end diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb new file mode 100644 index 000000000..f5cff3c0f --- /dev/null +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module Users + class OmniauthCallbacksController < Devise::OmniauthCallbacksController + skip_before_action :verify_authenticity_token + + def callback + # Here you will need to implement your logic for processing the callback + # for example, finding or creating a user + @user = User.from_omniauth(request.env['omniauth.auth']) + + if @user.persisted? + # We need to render a loading page here just to set the sesion properly + # otherwise the logged in session gets lost during the redirect + if params[:action] == 'saml' + set_flash_message(:notice, :success, kind: params[:action]) if is_navigational_format? + sign_in @user, event: :authentication # this will throw if @user is not activated + render 'complete' + else + sign_in_and_redirect @user, event: :authentication # this will throw if @user is not activated + set_flash_message(:notice, :success, kind: params[:action]) if is_navigational_format? + end + else + session['devise.user_attributes'] = @user.attributes + redirect_to new_user_registration_url + end + end + alias cas callback + alias openid_connect callback + alias saml callback + + def passthru + render status: 404, plain: 'Not found. Authentication passthru.' + end + + # def failure + # #redirect_to root_path + # end + end +end diff --git a/app/models/concerns/account_settings.rb b/app/models/concerns/account_settings.rb index 511b38c01..8af171de8 100644 --- a/app/models/concerns/account_settings.rb +++ b/app/models/concerns/account_settings.rb @@ -16,7 +16,6 @@ module AccountSettings setting :allow_signup, type: 'boolean', default: true setting :allow_downloads, type: 'boolean', default: true - setting :auth_provider, type: 'string' setting :analytics_provider, type: 'string' setting :bulkrax_validations, type: 'boolean', disabled: true setting :cache_api, type: 'boolean', default: false @@ -24,6 +23,7 @@ module AccountSettings setting :contact_email_to, type: 'string', default: 'consortial-ir@palci.org' setting :doi_reader, type: 'boolean', default: false setting :doi_writer, type: 'boolean', default: false + setting :email_domain, type: 'string', default: 'example.com' setting :email_format, type: 'array' setting :email_subject_prefix, type: 'string' setting :enable_oai_metadata, type: 'string', disabled: true diff --git a/app/models/identity_provider.rb b/app/models/identity_provider.rb new file mode 100644 index 000000000..c9ba7148c --- /dev/null +++ b/app/models/identity_provider.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class IdentityProvider < ApplicationRecord + validates :name, presence: true + validates :provider, presence: true + + mount_uploader :logo_image, LogoUploader + + def parsed_options(rack_env = nil) + @parsed_options = options.with_indifferent_access + return @parsed_options unless provider == 'saml' + url = "#{rack_env['HTTP_X_FORWARDED_PROTO']}://#{rack_env['HTTP_HOST']}/users/auth/saml/#{id}/callback" + @parsed_options['assertion_consumer_service_url'] = url + return @parsed_options unless @parsed_options['idp_metadata_url'] + idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new + idp_metadata = idp_metadata_parser.parse_remote_to_hash(@parsed_options['idp_metadata_url']) + @parsed_options = idp_metadata.merge(@parsed_options) + end +end diff --git a/app/models/user.rb b/app/models/user.rb index ed7165aaf..3889ee494 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -15,7 +15,8 @@ class User < ApplicationRecord # Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable and :omniauthable devise :database_authenticatable, :invitable, :registerable, - :recoverable, :rememberable, :trackable, :validatable + :recoverable, :rememberable, :trackable, :validatable, + :omniauthable, omniauth_providers: %i[saml openid_connect cas] after_create :add_default_group_membership! @@ -30,6 +31,18 @@ def self.default_scope scope :registered, -> { for_repository.group(:id).where(guest: false) } + def self.from_omniauth(auth) + find_or_create_by(provider: auth.provider, uid: auth.uid) do |user| + user.email = auth&.info&.email || [auth.uid, '@', Site.instance.account.email_domain].join if user.email.blank? + user.password = Devise.friendly_token[0, 20] + user.display_name = auth&.info&.name # assuming the user model has a name + # user.image = auth.info.image # assuming the user model has an image + # If you are using confirmable and the provider(s) you use validate emails, + # uncomment the line below to skip the confirmation emails. + # user.skip_confirmation! + end + end + # Method added by Blacklight; Blacklight uses #to_s on your # user class to get a user-displayable login/identifier. def to_s diff --git a/app/uploaders/logo_uploader.rb b/app/uploaders/logo_uploader.rb new file mode 100644 index 000000000..25a56c3e0 --- /dev/null +++ b/app/uploaders/logo_uploader.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class LogoUploader < CarrierWave::Uploader::Base + include CarrierWave::MiniMagick + include CarrierWave::Compatibility::Paperclip + + version :medium do + process resize_to_fill: [300, 300] + end + + version :thumb do + process resize_to_fill: [100, 100] + end + + def extension_whitelist + %w[jpg jpeg png gif bmp tif tiff] + end +end diff --git a/app/views/_user_util_links.html.erb b/app/views/_user_util_links.html.erb index 898b98da5..b1add73fc 100644 --- a/app/views/_user_util_links.html.erb +++ b/app/views/_user_util_links.html.erb @@ -22,7 +22,7 @@ <% else %>
  • - <%= link_to main_app.new_user_session_path do %> + <%= link_to main_app.single_signon_index_path do %> <%= t("hyrax.toolbar.profile.login") %> <% end %>
  • diff --git a/app/views/devise/omniauth_callbacks/complete.html.erb b/app/views/devise/omniauth_callbacks/complete.html.erb new file mode 100644 index 000000000..87a3625dc --- /dev/null +++ b/app/views/devise/omniauth_callbacks/complete.html.erb @@ -0,0 +1,9 @@ +

    Thank you for signing in. Your dashboard is loading

    +
    Loading...
    + diff --git a/app/views/devise/shared/_links.html.erb b/app/views/devise/shared/_links.html.erb index 86c366f64..caeb8b523 100644 --- a/app/views/devise/shared/_links.html.erb +++ b/app/views/devise/shared/_links.html.erb @@ -19,7 +19,7 @@ <% end -%> <%- if devise_mapping.omniauthable? %> - <%- resource_class.omniauth_providers.each do |provider| %> - <%= link_to t('.sign_in_with_provider', provider: provider.to_s.titleize), omniauth_authorize_path(resource_name, provider) %>
    - <% end -%> -<% end -%> + <%- IdentityProvider.all.each do |ip| %> +

    <%= button_to t('.sign_in_with_provider', provider: ip.provider.to_s.titleize), omniauth_authorize_path(resource_name, ip.provider, ip.id), data: { turbo: false } %>

    + <% end %> +<% end %> diff --git a/app/views/hyrax/dashboard/sidebar/_configuration.html.erb b/app/views/hyrax/dashboard/sidebar/_configuration.html.erb index f66c7e8d0..71a5ed756 100644 --- a/app/views/hyrax/dashboard/sidebar/_configuration.html.erb +++ b/app/views/hyrax/dashboard/sidebar/_configuration.html.erb @@ -23,6 +23,10 @@ <% end %> <% end %> + <%= menu.nav_link(main_app.identity_providers_path) do %> + <%= t('hyrax.admin.sidebar.identity_providers') %> + <% end %> + <%= menu.nav_link(main_app.edit_site_labels_path) do %> <%= t('hyrax.admin.sidebar.labels') %> <% end %> diff --git a/app/views/identity_providers/_form.html.erb b/app/views/identity_providers/_form.html.erb new file mode 100644 index 000000000..fd2d41430 --- /dev/null +++ b/app/views/identity_providers/_form.html.erb @@ -0,0 +1,59 @@ +
    + <%= simple_form_for(@identity_provider) do |f| %> +
    + <% if @identity_provider.errors.any? %> +
    +

    <%= pluralize(@identity_provider.errors.count, "error") %> prohibited this authentication provider from being saved:

    +
      + <% @identity_provider.errors.messages.each do |key, messages| %> +
    • <%= key %> "<%= @identity_provider.errors.details[key].first[:value] %>" <%= messages.join(' and ') %>
    • + <% end %> +
    +
    + <% end %> + <%= f.input :name %> + <%= f.input :provider, + collection: Devise.omniauth_providers.map {|o| [o, o.upcase]}, + label_method: :second, + value_method: :first, + label: t('hyku.identity_provider.label.provider'), + required: true %> +

    Documentation for each identity provider type can be found in its associated adapter documentation.

    +
      +
    • <%= link_to 'SAML', 'https://github.com/omniauth/omniauth-saml' %>
    • +
    • <%= link_to 'CAS', 'https://github.com/dlindahl/omniauth-cas' %>
    • +
    • <%= link_to 'Openid Connect', 'https://github.com/omniauth/omniauth_openid_connect' %>
    • +
    + +

    We use an additional paramater for SAML - `idp_metadata_url`. If you provide that URL, it will be parsed as shown in <%= link_to 'the SAML docs', 'https://github.com/omniauth/omniauth-saml#idp-metadata' %>

    + <% if @identity_provider.new_record? %> +

    SAML assertion_consumer_service_url will be displayed after record is saved

    + <% else %> +

    These are the assertion consumer service urls or redirect urls that need to be allowed by your IdP. Do not specify the assertion_consumer_service_url in your options.

    +
      + <% @current_account.domain_names.each do |dn| %> +
    • <%= dn.cname %>/users/auth/saml/<%= @identity_provider.id %>/callback
    • + <% end %> +
    +

    Metadata is available <%= link_to 'here', "/users/auth/saml/#{@identity_provider.id}/metadata", data: { turbolinks: false } %>

    + <% end %> + + <%= f.input :options, input_html: {value: @identity_provider.options&.to_json } %> + + + <%# Upload Logo Image %> + <%= f.input :logo_image, as: :file, wrapper: :vertical_file_input, hint: t('hyrax.admin.appearances.show.forms.logo_image.hint') %> + <%= f.input :logo_image_text, as: :text %> + <%= image_tag f.object.logo_image.url(:medium), class: "img-responsive", alt: f.object.logo_image_text if f.object.logo_image? %> + +
    + + + <% end %> +
    diff --git a/app/views/identity_providers/edit.html.erb b/app/views/identity_providers/edit.html.erb new file mode 100644 index 000000000..3e463430c --- /dev/null +++ b/app/views/identity_providers/edit.html.erb @@ -0,0 +1,5 @@ +<% provide :page_header do %> +

    <%= t('hyku.identity_provider.header') %>

    +<% end %> + +<%= render 'form', identity_provider: @identity_provider %> diff --git a/app/views/identity_providers/index.html.erb b/app/views/identity_providers/index.html.erb new file mode 100644 index 000000000..594fe23d5 --- /dev/null +++ b/app/views/identity_providers/index.html.erb @@ -0,0 +1,37 @@ +<% content_for :page_header do %> +

    <%= t(:'hyrax.admin.sidebar.identity_providers_and_permissions') %>

    +<% end %> + +
    +
    +
    +
    + + + + + + + + + + + + <% @identity_providers.each do |u| %> + + + + + + + + <% end %> + +
    NameProviderUpdated AtLogo
    <%= u.name %><%= u.provider %><%= u.updated_at %><%= image_tag u.logo_image.url(:thumb), class: "img-responsive", alt: u.logo_image_text if u.logo_image? %><%= link_to t('.edit'), edit_identity_provider_path(u) %> | <%= link_to t('helpers.action.delete'), identity_provider_path(u), method: :delete, data: { confirm: t('.confirm_delete') } %>
    + <%= link_to new_identity_provider_path, class: 'btn btn-primary' do %> + <%= t('.create_new') %> + <% end %> +
    +
    +
    +
    diff --git a/app/views/identity_providers/new.html.erb b/app/views/identity_providers/new.html.erb new file mode 100644 index 000000000..3e463430c --- /dev/null +++ b/app/views/identity_providers/new.html.erb @@ -0,0 +1,5 @@ +<% provide :page_header do %> +

    <%= t('hyku.identity_provider.header') %>

    +<% end %> + +<%= render 'form', identity_provider: @identity_provider %> diff --git a/app/views/shared/_footer.html.erb b/app/views/shared/_footer.html.erb index 2c4cdf422..b05feab29 100644 --- a/app/views/shared/_footer.html.erb +++ b/app/views/shared/_footer.html.erb @@ -9,7 +9,7 @@ diff --git a/app/views/single_signon/index.html.erb b/app/views/single_signon/index.html.erb new file mode 100644 index 000000000..32d52b7b1 --- /dev/null +++ b/app/views/single_signon/index.html.erb @@ -0,0 +1,22 @@ +

    Select a Single Sign On Provider

    + +<%- if devise_mapping.omniauthable? %> + <%- @identity_providers.each do |ip| %> +
    + <%= button_to omniauth_authorize_path(resource_name, ip.provider, ip.id), form_class: 'button_to col-centered', class: 'sso-button' do %> +
    + <%= image_tag ip.logo_image.url(:medium), class: "img-responsive", alt: ip.logo_image_text if ip.logo_image? %> +
    + <%= t('.sign_in_with_provider', provider: ip.name) %>
    +
    + Sign In + <% end %> +
    + <% end -%> +<% end -%> + +<% if @identity_providers.count == 1 %> + +<% end %> diff --git a/config/application.rb b/config/application.rb index 99d7709a6..cd596d323 100644 --- a/config/application.rb +++ b/config/application.rb @@ -48,6 +48,13 @@ class Application < Rails::Application end end + config.to_prepare do + # Allows us to use decorator files in the app directory + Dir.glob(File.join(File.dirname(__FILE__), "../lib/**/*_decorator*.rb")).sort.each do |c| + Rails.configuration.cache_classes ? require(c) : load(c) + end + end + # OAI additions Dir.glob(File.join(File.dirname(__FILE__), "../lib/oai/**/*.rb")).sort.each do |c| Rails.configuration.cache_classes ? require(c) : load(c) diff --git a/config/initializers/apartment.rb b/config/initializers/apartment.rb index b0ea7cc78..b177fecef 100644 --- a/config/initializers/apartment.rb +++ b/config/initializers/apartment.rb @@ -53,6 +53,5 @@ end end - Rails.application.config.middleware.use AccountElevator - + Rails.application.config.middleware.insert_before Warden::Manager, AccountElevator end diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 296e7d928..6658b94b0 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -289,7 +289,23 @@ # ==> OmniAuth # Add a new OmniAuth provider. Check the wiki for more information on setting # up on your models and hooks. - # config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo' + # if statement allows loading the app to call the migration that creates the provider + # setup for multiprovider SAML options + dynamic_options_generator = lambda { |identity_provider_id, rack_env| + identity_provider = IdentityProvider.find(identity_provider_id) + identity_provider.parsed_options(rack_env) + } + identity_provider_id_regex = /\d+/ + + [:cas, :openid_connect, :saml].each do |provider| + path_prefix = "/users/auth/#{provider}" + handler = OmniAuth::MultiProvider::Handler.new(path_prefix: path_prefix, + identity_provider_id_regex: identity_provider_id_regex, + &dynamic_options_generator) + static_options = { path_prefix: path_prefix } + + config.omniauth provider, static_options.merge(handler.provider_options) + end # ==> Warden configuration # If you want to use other strategies, that are not supported by Devise, or diff --git a/config/initializers/is_it_working.rb b/config/initializers/is_it_working.rb deleted file mode 100644 index 4f17bcefc..000000000 --- a/config/initializers/is_it_working.rb +++ /dev/null @@ -1,4 +0,0 @@ -Rails.configuration.middleware.use(IsItWorking::Handler) do |h| - # Check the ActiveRecord database connection without spawning a new thread - h.check :active_record, async: false -end diff --git a/config/locales/en.yml b/config/locales/en.yml index c0bf56ec6..43c1d126a 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1,5 +1,8 @@ --- en: + single_signon: + index: + sign_in_with_provider: Sign in with %{provider} activerecord: attributes: site: @@ -137,13 +140,14 @@ en: user_reader: Can read any User in this tenant title: Administration work_types: Available Work Types - auth_provider: - header: Authentication Provider + identity_provider: + header: Authentication Identity Provider label: + name: Name or Description provider: Provider - client_id: Client ID - client_secret: Client Secret - idp_sso_service_url: 'IDP SSO Service URL' + optoins: Options + logo_image: Image for SSO Page + logo_image_alt_text: Alt Text for Image footer: admin_login: Administrator login proprietor: diff --git a/config/locales/hyrax.en.yml b/config/locales/hyrax.en.yml index 719e1aa79..1171027d6 100644 --- a/config/locales/hyrax.en.yml +++ b/config/locales/hyrax.en.yml @@ -209,7 +209,7 @@ en: sidebar: activity: Activity appearance: Appearance - auth_provider: Auth Provider + identity_provider: Identity Provider collection_types: Collection Types collections: Collections configuration: Configuration diff --git a/config/routes.rb b/config/routes.rb index c22586c95..1842314a5 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,10 +1,11 @@ +# frozen_string_literal: true + # OVERRIDE Hyrax 2.9.0 to add featured collection routes require 'sidekiq/web' -Rails.application.routes.draw do - - resources :auth_providers +Rails.application.routes.draw do # rubocop:disable Metrics/BlockLength + resources :identity_providers concern :iiif_search, BlacklightIiifSearch::Routes.new concern :range_searchable, BlacklightRangeLimit::Routes::RangeSearchable.new concern :oai_provider, BlacklightOaiProvider::Routes.new @@ -12,7 +13,7 @@ mount Hyrax::IiifAv::Engine, at: '/' mount Riiif::Engine => 'images', as: :riiif if Hyrax.config.iiif_image_server? - authenticate :user, lambda { |u| u.is_superadmin } do + authenticate :user, ->(u) { u.is_superadmin } do mount Sidekiq::Web => '/sidekiq' end @@ -55,23 +56,44 @@ mount BrowseEverything::Engine => '/browse' resource :site, only: [:update] do - resources :roles, only: [:index, :update] - resource :labels, only: [:edit, :update] + resources :roles, only: %i[index update] + resource :labels, only: %i[edit update] end root 'hyrax/homepage#index' + devise_for :users, skip: [:omniauth_callbacks], controllers: { invitations: 'hyku/invitations', + registrations: 'hyku/registrations', + omniauth_callbacks: 'users/omniauth_callbacks' } + as :user do + resources :single_signon, only: [:index] + + Devise.omniauth_providers.each do |provider| + path_prefix = '/users/auth' + match "#{path_prefix}/#{provider}/:id", + to: "users/omniauth_callbacks#passthru", + as: "user_#{provider}_omniauth_authorize", + via: OmniAuth.config.allowed_request_methods + + match "#{path_prefix}/#{provider}/:id/metadata", + to: "users/omniauth_callbacks#passthru", + as: "user_#{provider}_omniauth_metadata", + via: [:get] + + match "#{path_prefix}/#{provider}/:id/callback", + to: "users/omniauth_callbacks##{provider}", + as: "user_#{provider}_omniauth_callback", + via: [:get, :post] + end + end - devise_for :users, controllers: { invitations: 'hyku/invitations', registrations: 'hyku/registrations' } mount Qa::Engine => '/authorities' mount Blacklight::Engine => '/' mount BlacklightAdvancedSearch::Engine => '/' mount Hyrax::Engine, at: '/' - if ENV.fetch('HYKU_BULKRAX_ENABLED', 'true') == 'true' - mount Bulkrax::Engine, at: '/' - end + mount Bulkrax::Engine, at: '/' if ENV.fetch('HYKU_BULKRAX_ENABLED', 'true') == 'true' concern :searchable, Blacklight::Routes::Searchable.new concern :exportable, Blacklight::Routes::Exportable.new @@ -100,8 +122,8 @@ end namespace :admin do - resource :account, only: [:edit, :update] - resource :work_types, only: [:edit, :update] + resource :account, only: %i[edit update] + resource :work_types, only: %i[edit update] resources :users, only: [:destroy] resources :groups do member do @@ -118,7 +140,7 @@ # Generic collection routes resources :collections, only: [] do member do - resource :featured_collection, only: [:create, :destroy] + resource :featured_collection, only: %i[create destroy] end end resources :featured_collection_lists, path: 'featured_collections', only: :create @@ -127,6 +149,7 @@ get 'all_collections' => 'hyrax/homepage#all_collections', as: :all_collections # Upload a collection thumbnail - post "/dashboard/collections/:id/delete_uploaded_thumbnail", to: "hyrax/dashboard/collections#delete_uploaded_thumbnail", as: :delete_uploaded_thumbnail - + post "/dashboard/collections/:id/delete_uploaded_thumbnail", + to: "hyrax/dashboard/collections#delete_uploaded_thumbnail", + as: :delete_uploaded_thumbnail end diff --git a/db/migrate/20230131202855_create_iiif_print_pending_relationships.iiif_print.rb b/db/migrate/20230131202855_create_iiif_print_pending_relationships.iiif_print.rb index 9a8f29cb9..163917cb1 100644 --- a/db/migrate/20230131202855_create_iiif_print_pending_relationships.iiif_print.rb +++ b/db/migrate/20230131202855_create_iiif_print_pending_relationships.iiif_print.rb @@ -1,12 +1,14 @@ # This migration comes from iiif_print (originally 20230109000000) class CreateIiifPrintPendingRelationships < ActiveRecord::Migration[5.1] def change - create_table :iiif_print_pending_relationships do |t| - t.string :child_title, null: false - t.string :parent_id, null: false - t.string :child_order, null: false - t.timestamps + unless table_exists?(:iiif_print_pending_relationships) + create_table :iiif_print_pending_relationships do |t| + t.string :child_title, null: false + t.string :parent_id, null: false + t.string :child_order, null: false + t.timestamps + end + add_index :iiif_print_pending_relationships, :parent_id end - add_index :iiif_print_pending_relationships, :parent_id end end diff --git a/db/migrate/20230727180717_add_omniauth_to_users.rb b/db/migrate/20230727180717_add_omniauth_to_users.rb new file mode 100644 index 000000000..c17782ae4 --- /dev/null +++ b/db/migrate/20230727180717_add_omniauth_to_users.rb @@ -0,0 +1,6 @@ +class AddOmniauthToUsers < ActiveRecord::Migration[5.2] + def change + add_column :users, :provider, :string + add_column :users, :uid, :string + end +end diff --git a/db/migrate/20230804073106_create_identity_providers.rb b/db/migrate/20230804073106_create_identity_providers.rb new file mode 100644 index 000000000..0cc2dd5ba --- /dev/null +++ b/db/migrate/20230804073106_create_identity_providers.rb @@ -0,0 +1,12 @@ +class CreateIdentityProviders < ActiveRecord::Migration[5.2] + def change + create_table :identity_providers do |t| + t.string :name + t.string :provider + t.jsonb :options + t.string :logo_image + t.string :logo_image_text + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index b1af55c23..309efbf41 100755 --- a/db/schema.rb +++ b/db/schema.rb @@ -383,6 +383,16 @@ t.string "humanized_name" end + create_table "identity_providers", force: :cascade do |t| + t.string "name" + t.string "provider" + t.jsonb "options" + t.string "logo_image" + t.string "logo_image_text" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + create_table "iiif_print_derivative_attachments", id: :serial, force: :cascade do |t| t.string "fileset_id" t.string "path" @@ -856,6 +866,8 @@ t.integer "invited_by_id" t.string "invited_by_type" t.string "preferred_locale" + t.string "provider" + t.string "uid" t.index ["email"], name: "index_users_on_email", unique: true t.index ["invitation_token"], name: "index_users_on_invitation_token", unique: true t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true diff --git a/docker-compose.yml b/docker-compose.yml index 6f6e70985..bd20ada95 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -124,6 +124,8 @@ services: web: <<: *app + # Uncomment command to access container with out starting Rails. Useful for debugging + # command: sleep infinity environment: - VIRTUAL_PORT=3000 - VIRTUAL_HOST=.hyku.test @@ -146,6 +148,8 @@ services: condition: service_started initialize_app: condition: service_completed_successfully + # ports: + # - 3000:3000 expose: - 3000 diff --git a/lib/omni_auth/strategies/saml_decorator.rb b/lib/omni_auth/strategies/saml_decorator.rb new file mode 100644 index 000000000..f4f07ce93 --- /dev/null +++ b/lib/omni_auth/strategies/saml_decorator.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +# monkey patch to support metadata paths - hacked version of: +# https://github.com/salsify/omniauth-multi-provider/issues/4#issuecomment-366452170 +# +# This patches omn-auth-saml to ensure setup_phase is called at the beginning of other_phase +# (which is consistent with how it handles request_phase and callback_phase). +module OmniAuthSamlOtherPhaseSetupPatch + def on_auth_path? + # Override this to ensure initialization happens properly in OmniAuth::Strategies::SAML for "other" + # requests + current_path.start_with?(options.path_prefix) + end + + def on_other_path? + # Override this to ensure initialization happens properly in OmniAuth::Strategies::SAML for "other" + # requests + current_path.match(%r{/(?:metadata|spslo|slo)\z}) + end + + def other_phase + # Override the other_phase method to call setup_phase before checking to see if the request + # is on an "other" request path. This ensures omniauth-multi-provider has setup the path + # prefix properly for the given identity provider. By default omniauth won't call setup_phase until + # after checking the path. + @callback_path = nil + setup_phase if on_auth_path? && on_other_path? + super + end + + def request_path + super + @request_path = @request_path.gsub('saml/saml', 'saml') + end + + def callback_path + super + @callback_path = @callback_path.gsub('saml/saml', 'saml') + end + + def setup_path + super + @setup_path = @setup_path.gsub('saml/saml', 'saml') + end + + def setup_phase + # Make sure we only perform setup once since this method will be called twice during the other phase + return if @setup # TODO: always false due to the calling class being created anew each time? + super + @setup = true + end +end + +OmniAuth::Strategies::SAML.prepend(OmniAuthSamlOtherPhaseSetupPatch) diff --git a/spec/factories/identity_providers.rb b/spec/factories/identity_providers.rb new file mode 100644 index 000000000..ef0d6cb56 --- /dev/null +++ b/spec/factories/identity_providers.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :identity_provider, class: 'IdentityProvider' do + name { 'SAML Test' } + provider { 'saml' } + options { {} } + end +end diff --git a/spec/models/identity_provider_spec.rb b/spec/models/identity_provider_spec.rb new file mode 100644 index 000000000..d2a13fab6 --- /dev/null +++ b/spec/models/identity_provider_spec.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe IdentityProvider, type: :model do + subject do + described_class.new( + name: 'SAML Test', + provider: 'saml' + ) + end + + context 'attributes and validations' do + it 'is valid with valid attributes' do + expect(subject).to be_valid + end + + it 'is not valid without a name' do + subject.name = nil + expect(subject).not_to be_valid + end + + it 'is not valid without a provider' do + subject.provider = nil + expect(subject).not_to be_valid + end + end +end diff --git a/spec/requests/single_signon_request_spec.rb b/spec/requests/single_signon_request_spec.rb new file mode 100644 index 000000000..f03da32a8 --- /dev/null +++ b/spec/requests/single_signon_request_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe "SingleSignons", type: :request do + describe "GET single_signon#index" do + describe "with no IdentityProviders" do + it "redirects to sign in" do + get "/single_signon" + expect(response).to have_http_status(:redirect) + end + end + + describe "with an IdentityProvider" do + before do + IdentityProvider.create(name: 'fake', provider: 'saml') + end + + it "renders succes" do + get "/single_signon" + expect(response).to have_http_status(:success) + end + end + end +end diff --git a/spec/routing/auth_providers_routing_spec.rb b/spec/routing/auth_providers_routing_spec.rb deleted file mode 100644 index 9b64dd7e9..000000000 --- a/spec/routing/auth_providers_routing_spec.rb +++ /dev/null @@ -1,29 +0,0 @@ -require "rails_helper" - -RSpec.describe AuthProvidersController, type: :routing do - describe "routing" do - it "routes to #new" do - expect(get: "/auth_providers/new").to route_to("auth_providers#new") - end - - it "routes to #edit" do - expect(get: "/auth_providers/1/edit").to route_to("auth_providers#edit", id: "1") - end - - it "routes to #create" do - expect(post: "/auth_providers").to route_to("auth_providers#create") - end - - it "routes to #update via PUT" do - expect(put: "/auth_providers/1").to route_to("auth_providers#update", id: "1") - end - - it "routes to #update via PATCH" do - expect(patch: "/auth_providers/1").to route_to("auth_providers#update", id: "1") - end - - it "routes to #destroy" do - expect(delete: "/auth_providers/1").to route_to("auth_providers#destroy", id: "1") - end - end -end