diff --git a/Gemfile b/Gemfile
index 5868b4d4c5..45fbe98b7c 100644
--- a/Gemfile
+++ b/Gemfile
@@ -64,6 +64,8 @@ gem "devise-authy", ">= 1.10.0"
gem "pundit", "~> 0.3"
gem "devise_zxcvbn", ">= 4.4.1"
gem "devise-security", "~> 0.18.0"
+gem "omniauth-rails_csrf_protection"
+gem "omniauth-oauth2"
# GOV.UK Notify support (for mailers)
gem "mail-notify", "~> 2.0"
diff --git a/Gemfile.lock b/Gemfile.lock
index 0eefc5ca02..a4de23a04f 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -352,6 +352,8 @@ GEM
money (6.16.0)
i18n (>= 0.6.4, <= 2)
multi_json (1.15.0)
+ multi_xml (0.7.1)
+ bigdecimal (~> 3.1)
multipart-post (2.3.0)
mustermann (3.0.0)
ruby2_keywords (~> 0.0.1)
@@ -379,6 +381,21 @@ GEM
racc (~> 1.4)
notifications-ruby-client (6.0.0)
jwt (>= 1.5, < 3)
+ oauth2 (1.4.11)
+ faraday (>= 0.17.3, < 3.0)
+ jwt (>= 1.0, < 3.0)
+ multi_json (~> 1.3)
+ multi_xml (~> 0.5)
+ rack (>= 1.2, < 4)
+ omniauth (1.9.2)
+ hashie (>= 3.4.6)
+ rack (>= 1.6.2, < 3)
+ omniauth-oauth2 (1.3.1)
+ oauth2 (~> 1.0)
+ omniauth (~> 1.2)
+ omniauth-rails_csrf_protection (0.1.2)
+ actionpack (>= 4.2)
+ omniauth (>= 1.3.1)
orm_adapter (0.5.0)
pagy (7.0.11)
paper_trail (12.2.0)
@@ -775,6 +792,8 @@ DEPENDENCIES
matrix
nilify_blanks
nokogiri
+ omniauth-oauth2
+ omniauth-rails_csrf_protection
paper_trail (~> 12.2.0)
paper_trail-association_tracking
pdf-inspector
diff --git a/app/controllers/admin/admins_controller.rb b/app/controllers/admin/admins_controller.rb
index 10cadf06f0..d1785e8ddd 100644
--- a/app/controllers/admin/admins_controller.rb
+++ b/app/controllers/admin/admins_controller.rb
@@ -1,5 +1,6 @@
-class Admin::AdminsController < Admin::UsersController
- before_action :find_resource, except: [:index, :new, :create, :login_as_assessor, :login_as_user]
+class Admin::AdminsController < Admin::BaseController
+ before_action :find_resource, except: [:index, :login_as_assessor, :login_as_user]
+
def index
params[:search] ||= AdminSearch::DEFAULT_SEARCH
params[:search].permit!
@@ -7,48 +8,7 @@ def index
@search = AdminSearch.new(Admin.all)
.search(params[:search])
@resources = @search.results.page(params[:page])
- end
-
- def new
- @resource = Admin.new
- authorize @resource, :create?
- end
-
- def create
- @resource = Admin.new(resource_params)
- @resource.skip_password_validation = true
- authorize @resource, :create?
-
- @resource.save
-
- render_flash_message_for(@resource, message: @resource.errors.none? ? nil : @resource.errors.messages.values.flatten.uniq.join("
"))
-
- location = @resource.persisted? ? admin_admins_path : nil
- respond_with :admin, @resource, location: location
- end
-
- def update
- authorize @resource, :update?
-
- if resource_params[:password].present?
- @resource.update(resource_params)
- else
- @resource.skip_password_validation = true
- @resource.update_without_password(resource_params)
- end
-
- render_flash_message_for(@resource, message: @resource.errors.none? ? nil : @resource.errors.messages.values.flatten.uniq.join("
"))
-
- respond_with :admin, @resource, location: admin_admins_path
- end
-
- def destroy
- authorize @resource, :destroy?
- @resource.soft_delete!
-
- render_flash_message_for(@resource, message: @resource.errors.none? ? nil : @resource.errors.messages.values.flatten.uniq.join("
"))
-
- respond_with :admin, @resource, location: admin_admins_path
+ render template: "admin/users/index"
end
# NOTE: debug abilities for Admin - BEGIN
@@ -74,13 +34,4 @@ def login_as_user
def find_resource
@resource = Admin.find(params[:id])
end
-
- def resource_params
- params.require(:admin)
- .permit(:email,
- :password,
- :password_confirmation,
- :first_name,
- :last_name)
- end
end
diff --git a/app/controllers/admins/confirmations_controller.rb b/app/controllers/admins/confirmations_controller.rb
index c54d0ad7db..f60c74d866 100644
--- a/app/controllers/admins/confirmations_controller.rb
+++ b/app/controllers/admins/confirmations_controller.rb
@@ -1,9 +1,2 @@
class Admins::ConfirmationsController < Devise::ConfirmationsController
- include PasswordSettable
-
- private
-
- def password_reset_path(token)
- edit_admin_password_path(reset_password_token: token, locals: { password_not_set: true })
- end
end
diff --git a/app/controllers/admins/omniauth_callbacks_controller.rb b/app/controllers/admins/omniauth_callbacks_controller.rb
new file mode 100644
index 0000000000..26eaa26e2b
--- /dev/null
+++ b/app/controllers/admins/omniauth_callbacks_controller.rb
@@ -0,0 +1,33 @@
+class Admins::OmniauthCallbacksController < Devise::OmniauthCallbacksController
+ skip_before_action :verify_authenticity_token, only: :developer
+
+ def dbt_staff_sso
+ FindAndUpdateWithAuth.new(auth: request.env["omniauth.auth"], model: Admin).run.tap do |result|
+ if result.success
+ sign_in_and_redirect result.user, event: :authentication
+ set_flash_message(:notice, :success, kind: "DBT Staff SSO")
+ else
+ set_flash_message(:notice, :error, kind: result.errors)
+ redirect_to new_admin_session_url
+ end
+ end
+ end
+
+ def developer
+ raise "developer strategy should only be used in development!" unless Rails.env.development?
+
+ FindAndUpdateWithAuth.new(auth: request.env["omniauth.auth"], model: Admin).run.tap do |result|
+ if result.success
+ sign_in_and_redirect result.user, event: :authentication
+ set_flash_message(:notice, :success, kind: "Developer SSO")
+ else
+ set_flash_message(:notice, :error, kind: result.errors)
+ redirect_to new_admin_session_url
+ end
+ end
+ end
+
+ def failure
+ redirect_to admin_root_path
+ end
+end
diff --git a/app/controllers/admins/sessions_controller.rb b/app/controllers/admins/sessions_controller.rb
new file mode 100644
index 0000000000..78ca0749de
--- /dev/null
+++ b/app/controllers/admins/sessions_controller.rb
@@ -0,0 +1,2 @@
+class Admins::SessionsController < Devise::SessionsController
+end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 84a1737eb1..17f5ac0eba 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -31,6 +31,14 @@ def after_sign_in_path_for(resource)
end
end
+ def after_sign_out_path_for(scope)
+ if scope == :admin
+ new_admin_session_path
+ else
+ super
+ end
+ end
+
def admin_in_read_only_mode?
@admin_in_read_only_mode ||= (admin_signed_in? || assessor_signed_in?) &&
session["warden.user.user.key"] &&
diff --git a/app/interactors/find_and_update_with_auth.rb b/app/interactors/find_and_update_with_auth.rb
new file mode 100644
index 0000000000..137257c678
--- /dev/null
+++ b/app/interactors/find_and_update_with_auth.rb
@@ -0,0 +1,52 @@
+class FindAndUpdateWithAuth
+ attr_reader :success, :errors, :user
+
+ def initialize(auth:, model:)
+ @model = model
+ @sso_uid = auth.uid
+ @sso_info = auth.info
+ @sso_provider = auth.provider
+ end
+
+ def run
+ @user = find_or_create_user
+ @success = @user.persisted?
+ @errors = @user.errors unless @success
+ self
+ end
+
+ private
+
+ # 1 - check if they have already set up sso; when they have update them to ensure we capture the current email/first_name/last_name
+ # 2 - check if the user already existed prior to sso; when they do update them with the auth details
+ # 3 - finally if they dont exist; create them from the auth details
+ def find_or_create_user
+ find_and_update_with_auth(sso_provider: @sso_provider, sso_uid: @sso_uid) || find_and_update_with_auth(email: @sso_info.email) || create_from_auth
+ end
+
+ def find_and_update_with_auth(conditions)
+ raise "conditions should not be nil: #{conditions}" if conditions.compact.empty?
+
+ existing = @model.find_by(**conditions)
+ return unless existing
+
+ @model.update(
+ existing.id,
+ sso_provider: @sso_provider,
+ sso_uid: @sso_uid,
+ email: @sso_info.email,
+ first_name: @sso_info.first_name,
+ last_name: @sso_info.last_name,
+ )
+ end
+
+ def create_from_auth
+ @model.create(
+ email: @sso_info.email,
+ first_name: @sso_info.first_name,
+ last_name: @sso_info.last_name,
+ sso_uid: @sso_uid,
+ sso_provider: @sso_provider,
+ )
+ end
+end
diff --git a/app/models/admin.rb b/app/models/admin.rb
index a5fd57acbf..f8471cb248 100644
--- a/app/models/admin.rb
+++ b/app/models/admin.rb
@@ -2,15 +2,11 @@ class Admin < ApplicationRecord
include PgSearch::Model
include AutosaveTokenGeneration
- # Include default devise modules. Others available are:
- # :confirmable, :lockable, :timeoutable and :omniauthable
-
- devise :authy_authenticatable, :database_authenticatable,
- :recoverable, :trackable, :validatable, :confirmable,
- :zxcvbnable, :lockable, :timeoutable, :session_limitable
- include PasswordValidator
+ devise :trackable, :timeoutable, :session_limitable
+ devise :omniauthable, omniauth_providers: %i[dbt_staff_sso developer]
validates :first_name, :last_name, presence: true
+ validates :email, format: { with: Devise.email_regexp }
has_many :form_answer_attachments, as: :attachable
diff --git a/app/views/admin/admins/_list.html.slim b/app/views/admin/admins/_list.html.slim
index e192d772d9..f822f1c466 100644
--- a/app/views/admin/admins/_list.html.slim
+++ b/app/views/admin/admins/_list.html.slim
@@ -10,7 +10,7 @@
th.sortable
= sort_link f, 'Signed in on', @search, :last_sign_in_at
th.sortable
- = sort_link f, "Confirmed on", @search, :confirmed_at
+ = sort_link f, 'Single sign on ID', @search, :sso_uid
tbody
- if AdminDecorator.decorate_collection(resources).none?
tr
@@ -30,10 +30,4 @@
= admin.formatted_last_sign_in_at_long
span.hidden-lg
= admin.formatted_last_sign_in_at_short
- td
- - if admin.confirmed_at.present?
- small.text-muted
- = l admin.confirmed_at, format: :date_at_time
- - else
- small.text-danger
- ' Not confirmed
+ td = admin.sso_uid
diff --git a/app/views/admin/users/index.html.slim b/app/views/admin/users/index.html.slim
index 8bcb1c0213..18ee035cfa 100644
--- a/app/views/admin/users/index.html.slim
+++ b/app/views/admin/users/index.html.slim
@@ -25,8 +25,9 @@
= link_to "Edit assessors’ access to the system", suspension_status_admin_assessors_path, class: "btn btn-default btn-md"
'
- = link_to public_send("new_admin_#{controller_name.singularize}_path"), class: 'new-user btn btn-secondary btn-md' do
- = "+ Add #{controller_name == "users" ? "applicant" : controller_name.singularize}"
+ - unless controller_name == "admins"
+ = link_to public_send("new_admin_#{controller_name.singularize}_path"), class: 'new-user btn btn-secondary btn-md' do
+ = "+ Add #{controller_name == "users" ? "applicant" : controller_name.singularize}"
.clear
- if @search.query?
diff --git a/app/views/admins/sessions/new.html.slim b/app/views/admins/sessions/new.html.slim
index 5e8eafbaa2..076ea57935 100644
--- a/app/views/admins/sessions/new.html.slim
+++ b/app/views/admins/sessions/new.html.slim
@@ -9,16 +9,15 @@
= resource.model_name
' - Sign in
- = simple_form_for(resource, as: resource_name, url: session_path(resource_name)) do |f|
+ - unless devise_mapping.omniauthable?
+ = simple_form_for(resource, as: resource_name, url: session_path(resource_name)) do |f|
+ .govuk-form-group
+ = f.input :email, required: false, autofocus: true, label: "Email", input_html: {class: "govuk-input--width-20"}
- .govuk-form-group
- = f.input :email, required: false, autofocus: true, label: "Email", input_html: {class: "govuk-input--width-20"}
+ .govuk-form-group
+ = f.input :password, required: false, label: "Password", input_html: {class: "govuk-input--width-10"}
- .govuk-form-group
- = f.input :password, required: false, label: "Password", input_html: {class: "govuk-input--width-10"}
-
- = f.input :remember_me, as: :boolean if devise_mapping.rememberable?
-
- = f.button :submit, "Sign in", class: "button large"
+ = f.input :remember_me, as: :boolean if devise_mapping.rememberable?
+ = f.button :submit, "Sign in", class: "button large"
= render "admins/shared/links"
diff --git a/app/views/admins/shared/_links.html.slim b/app/views/admins/shared/_links.html.slim
index afc7b25cf4..1ddafb8020 100644
--- a/app/views/admins/shared/_links.html.slim
+++ b/app/views/admins/shared/_links.html.slim
@@ -15,6 +15,9 @@
p
= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name), class: 'govuk-link'
- if devise_mapping.omniauthable?
- - resource_class.omniauth_providers.each do |provider|
- p
- = link_to "Sign in with #{provider.to_s.titleize}", omniauth_authorize_path(resource_name, provider), class: 'govuk-link'
+ - if Rails.env.development?
+ = link_to "Sign in with Developer", omniauth_authorize_path(resource_name, :developer), class: 'govuk-link', method: :post
+ - else
+ - resource_class.omniauth_providers.excluding(:developer).each do |provider|
+ p
+ = link_to "Sign in with #{provider.to_s.titleize}", omniauth_authorize_path(resource_name, provider), class: 'govuk-link', method: :post
diff --git a/app/views/layouts/application-admin.html.slim b/app/views/layouts/application-admin.html.slim
index 59a8f768ba..92ca4a84ff 100644
--- a/app/views/layouts/application-admin.html.slim
+++ b/app/views/layouts/application-admin.html.slim
@@ -91,10 +91,6 @@ html.no-js
strong = current_subject.decorate.full_name
br
small = current_subject.email
- li.divider
- - unless current_admin.authy_enabled
- li
- = link_to "Enable 2FA", admin_enable_authy_path
li
= button_to "Sign out",
destroy_admin_session_path,
@@ -110,10 +106,6 @@ html.no-js
strong = current_subject.decorate.full_name
br
small = current_subject.email
- li.divider
- - unless current_admin.authy_enabled
- li
- = link_to "Enable 2FA", admin_enable_authy_path
li
= link_to "Sign out",
destroy_admin_session_path,
diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb
index 5b426f8fab..811714828e 100644
--- a/config/initializers/devise.rb
+++ b/config/initializers/devise.rb
@@ -206,6 +206,13 @@
# manager.default_strategies(scope: :user).unshift :some_external_strategy
# end
+ config.omniauth :dbt_staff_sso,
+ ENV.fetch("DBT_STAFF_SSO_APP_ID", nil), # remove the nils once we have these set in ci etc
+ ENV.fetch("DBT_STAFF_SSO_APP_SECRET", nil),
+ info_fields: OmniAuth::Strategies::DbtStaffSso::INFO_FIELD_NAMES
+
+ config.omniauth :developer, fields: [:first_name, :last_name, :email] if Rails.env.development?
+
config.warden do |manager|
manager.failure_app = QaeFailureApp
end
diff --git a/config/routes.rb b/config/routes.rb
index dee13d7daf..5bc2de311a 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -15,14 +15,12 @@
unlocks: "users/unlocks",
}
- devise_for :admins, controllers: {
- confirmations: "admins/confirmations",
- devise_authy: "admin/devise_authy",
- }, path_names: {
- verify_authy: "/verify-token",
- enable_authy: "/enable-two-factor",
- verify_authy_installation: "/verify-installation",
- }
+ devise_for :admins, controllers: { omniauth_callbacks: "admins/omniauth_callbacks" }
+
+ devise_scope :admin do
+ get "admins/sign_in", to: "devise/sessions#new", as: :new_admin_session
+ delete "admins/sign_out", to: "devise/sessions#destroy", as: :destroy_admin_session
+ end
authenticate :admin do
mount Sidekiq::Web => "/sidekiq"
diff --git a/db/migrate/20241028142655_add_omniauth_to_admins.rb b/db/migrate/20241028142655_add_omniauth_to_admins.rb
new file mode 100644
index 0000000000..9883f32263
--- /dev/null
+++ b/db/migrate/20241028142655_add_omniauth_to_admins.rb
@@ -0,0 +1,6 @@
+class AddOmniauthToAdmins < ActiveRecord::Migration[7.0]
+ def change
+ add_column :admins, :sso_provider, :string
+ add_column :admins, :sso_uid, :string
+ end
+end
diff --git a/db/seeds.rb b/db/seeds.rb
index d8abf9c43a..409f09de2f 100644
--- a/db/seeds.rb
+++ b/db/seeds.rb
@@ -1,7 +1,6 @@
unless Admin.exists?
admin_args = {
email: "admin@example.com",
- password: "^#ur9EkLm@1W+OaDvg",
first_name: "First name",
last_name: "Last name",
confirmed_at: DateTime.now
diff --git a/db/structure.sql b/db/structure.sql
index 99590be2a8..069fe90c73 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -1,6 +1,7 @@
SET statement_timeout = 0;
SET lock_timeout = 0;
SET idle_in_transaction_session_timeout = 0;
+SET transaction_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SELECT pg_catalog.set_config('search_path', '', false);
@@ -9,6 +10,13 @@ SET xmloption = content;
SET client_min_messages = warning;
SET row_security = off;
+--
+-- Name: public; Type: SCHEMA; Schema: -; Owner: -
+--
+
+-- *not* creating schema, since initdb creates it
+
+
--
-- Name: citext; Type: EXTENSION; Schema: -; Owner: -
--
@@ -253,7 +261,9 @@ CREATE TABLE public.admins (
superadmin boolean DEFAULT false,
deleted boolean DEFAULT false,
autosave_token character varying,
- unique_session_id character varying
+ unique_session_id character varying,
+ sso_provider character varying,
+ sso_uid character varying
);
@@ -4389,5 +4399,4 @@ INSERT INTO "schema_migrations" (version) VALUES
('20240314105208'),
('20240501155226'),
('20241101151806');
-
-
+('20241102142655');
diff --git a/lib/omni_auth/strategies/dbt_staff_sso.rb b/lib/omni_auth/strategies/dbt_staff_sso.rb
new file mode 100644
index 0000000000..943f6d07d2
--- /dev/null
+++ b/lib/omni_auth/strategies/dbt_staff_sso.rb
@@ -0,0 +1,31 @@
+module OmniAuth
+ module Strategies
+ class DbtStaffSso < OmniAuth::Strategies::OAuth2
+ URL = ENV.fetch("DBT_STAFF_SSO_URL", nil) # remove the nils once we have these set in ci etc
+ INFO_PATH = ENV.fetch("DBT_STAFF_SSO_INFO_PATH", nil)
+ INFO_FIELD_NAMES = [
+ "first_name",
+ "last_name",
+ "email",
+ ]
+
+ option :name, :dbt_staff_sso
+ option :client_options, site: URL
+ option :pkce, true
+
+ uid do
+ raw_info["id"]
+ end
+
+ info do
+ INFO_FIELD_NAMES.index_with do |field_name|
+ raw_info[field_name]
+ end
+ end
+
+ def raw_info
+ @raw_info ||= access_token.get(INFO_PATH).parsed
+ end
+ end
+ end
+end
diff --git a/spec/acceptance/admin_sign_in.feature b/spec/acceptance/admin_sign_in.feature
deleted file mode 100644
index 03f9ed37bc..0000000000
--- a/spec/acceptance/admin_sign_in.feature
+++ /dev/null
@@ -1,7 +0,0 @@
-Feature: Admin Sign in
- Background:
- Given Admin user exists
-
- Scenario: I'm able to sign in
- When I sign in as admin
- Then I should see sign out link
diff --git a/spec/acceptance/steps/admin_sign_in_steps.rb b/spec/acceptance/steps/admin_sign_in_steps.rb
index 4ba54adcce..d6dec0718f 100644
--- a/spec/acceptance/steps/admin_sign_in_steps.rb
+++ b/spec/acceptance/steps/admin_sign_in_steps.rb
@@ -1,12 +1,9 @@
step "Admin user exists" do
- @admin = FactoryBot.create(:admin, password: "my98ssdkjv9823kds=2")
+ @admin = FactoryBot.create(:admin)
end
step "I sign in as admin" do
- visit "/admins/sign_in"
- fill_in "admin_email", with: @admin.email
- fill_in "admin_password", with: "my98ssdkjv9823kds=2"
- click_button "Sign in"
+ login_admin(@admin)
end
step "I should see sign out link" do
diff --git a/spec/controllers/admin/admins_controller_spec.rb b/spec/controllers/admin/admins_controller_spec.rb
index b1a2d1dbe9..4ec5d345b3 100644
--- a/spec/controllers/admin/admins_controller_spec.rb
+++ b/spec/controllers/admin/admins_controller_spec.rb
@@ -1,6 +1,6 @@
require "rails_helper"
-RSpec.describe Admin::AdminsController do
+describe Admin::AdminsController do
let!(:admin) { create(:admin, superadmin: true) }
before do
sign_in admin
@@ -14,44 +14,6 @@
end
end
- describe "GET new" do
- it "renders the new template" do
- get :new
- expect(response).to render_template("new")
- end
- end
-
- describe "POST create" do
- it "should create a resource" do
- post :create, params: { admin: FactoryBot.attributes_for(:admin) }
- expect(response).to redirect_to admin_admins_url
- expect(Admin.count).to eq 2
- end
- end
- describe "GET edit" do
- it "renders the template" do
- get :edit, params: { id: admin.id }
- expect(response).to render_template("edit")
- end
- end
-
- describe "PUT update" do
- it "should update a resource" do
- put :update, params: { id: admin.id, admin: { first_name: "changed first name" } }
- expect(response).to redirect_to admin_admins_url
- expect(Admin.first.first_name).to eq "changed first name"
- end
- end
-
- describe "Delete destroy" do
- it "should destroy a resource" do
- to_be_deleted = create(:admin)
- delete :destroy, params: { id: to_be_deleted.id }
- expect(response).to redirect_to admin_admins_url
- expect(Admin.count).to eq 1
- end
- end
-
describe "GET login_as_assessor" do
it "should login_as_assessor" do
assessor = create(:assessor)
diff --git a/spec/factories/admin_factory.rb b/spec/factories/admin_factory.rb
index be180cb490..bee5db3052 100644
--- a/spec/factories/admin_factory.rb
+++ b/spec/factories/admin_factory.rb
@@ -10,7 +10,6 @@
factory :admin do
first_name { "John" }
last_name { "Doe" }
- password { "my98ssdkjv9823kds=2" }
email
confirmed_at { Time.zone.now }
end
diff --git a/spec/features/admins/signing_in_with_single_sign_on_spec.rb b/spec/features/admins/signing_in_with_single_sign_on_spec.rb
new file mode 100644
index 0000000000..4f57ff181d
--- /dev/null
+++ b/spec/features/admins/signing_in_with_single_sign_on_spec.rb
@@ -0,0 +1,132 @@
+require "rails_helper"
+
+describe "Signing in with Single Sign on" do
+ let(:provider) { :dbt_staff_sso }
+ let(:uid) { "B0d$-Bu770N$" }
+
+ context "when the admin existed prior to SSO" do
+ let(:admin) { create(:admin) }
+
+ context "with the same details" do
+ before { stub_sso_request(email: admin.email, first_name: admin.first_name, last_name: admin.last_name, uid:, provider:) }
+
+ it "signs the admin in and updates the SSO fields" do
+ expect { login_admin(admin, stub_sso: false) }.not_to change { Admin.count }
+ expect_admin_to_be_signed_in
+ admin.reload
+ expect(admin.sso_uid).to eq(uid)
+ expect(admin.sso_provider).to eq(provider.to_s)
+ end
+ end
+
+ context "when the first_name has changed" do
+ let(:new_first_name) { "Bob" }
+
+ before { stub_sso_request(email: admin.email, first_name: new_first_name, last_name: admin.last_name, uid:, provider:) }
+
+ it "signs the admin in and updates the SSO fields" do
+ expect { login_admin(admin, stub_sso: false) }.not_to change { Admin.count }
+ expect_admin_to_be_signed_in
+ admin.reload
+ expect(admin.sso_uid).to eq(uid)
+ expect(admin.sso_provider).to eq(provider.to_s)
+ expect(admin.first_name).to eq(new_first_name)
+ end
+ end
+
+ context "when the last_name has changed" do
+ let(:new_last_name) { "Buttons" }
+
+ before { stub_sso_request(email: admin.email, first_name: admin.first_name, last_name: new_last_name, uid:, provider:) }
+
+ it "signs the admin in, updates the SSO fields and the last_name" do
+ expect { login_admin(admin, stub_sso: false) }.not_to change { Admin.count }
+ expect_admin_to_be_signed_in
+ admin.reload
+ expect(admin.sso_uid).to eq(uid)
+ expect(admin.sso_provider).to eq(provider.to_s)
+ expect(admin.last_name).to eq(new_last_name)
+ end
+ end
+ end
+
+ context "when the admin has previously signed in with SSO" do
+ let(:admin) { create(:admin, sso_uid: uid, sso_provider: provider) }
+
+ context "with the same details" do
+ before { stub_sso_request(email: admin.email, first_name: admin.first_name, last_name: admin.last_name, uid: admin.sso_uid, provider: admin.sso_provider) }
+
+ it "signs the admin in" do
+ expect { login_admin(admin, stub_sso: false) }.not_to change { Admin.count }
+ expect_admin_to_be_signed_in
+ end
+ end
+
+ context "when the email has changed" do
+ let(:new_email) { "bob@buttons.com" }
+
+ before { stub_sso_request(email: new_email, first_name: admin.first_name, last_name: admin.last_name, uid: admin.sso_uid, provider: provider) }
+
+ it "signs the admin in and updates the email" do
+ expect { login_admin(admin, stub_sso: false) }.not_to change { Admin.count }
+ expect_admin_to_be_signed_in
+ admin.reload
+ expect(admin.email).to eq(new_email)
+ end
+ end
+
+ context "when the first_name has changed" do
+ let(:new_first_name) { "Bob" }
+
+ before { stub_sso_request(email: admin.email, first_name: new_first_name, last_name: admin.last_name, uid: admin.sso_uid, provider: provider) }
+
+ it "signs the admin in and updates the first_name" do
+ expect { login_admin(admin, stub_sso: false) }.not_to change { Admin.count }
+ expect_admin_to_be_signed_in
+ admin.reload
+ expect(admin.first_name).to eq(new_first_name)
+ end
+ end
+
+ context "when the last_name has changed" do
+ let(:new_last_name) { "Buttons" }
+
+ before { stub_sso_request(email: admin.email, first_name: admin.first_name, last_name: new_last_name, uid: admin.sso_uid, provider: provider) }
+
+ it "signs the admin in and updates the last_name" do
+ expect { login_admin(admin, stub_sso: false) }.not_to change { Admin.count }
+ expect_admin_to_be_signed_in
+ admin.reload
+ expect(admin.last_name).to eq(new_last_name)
+ end
+ end
+ end
+
+ context "when the admin is using the platform for the first time" do
+ let(:email) { "bob@buttons.com" }
+ let(:first_name) { "Bob" }
+ let(:last_name) { "Buttons" }
+
+ before do
+ stub_sso_request(email:, first_name:, last_name:, uid:, provider:)
+ visit "/admins/sign_in"
+ end
+
+ it "signs the admin in and creates a new admin record" do
+ expect { click_link "Sign in with Dbt Staff Sso" }.to change { Admin.count }.from(0).to(1)
+ expect_admin_to_be_signed_in
+
+ admin = Admin.last
+ expect(admin.email).to eq(email)
+ expect(admin.first_name).to eq(first_name)
+ expect(admin.last_name).to eq(last_name)
+ expect(admin.sso_uid).to eq(uid)
+ expect(admin.sso_provider).to eq(provider.to_s)
+ end
+ end
+
+ def expect_admin_to_be_signed_in
+ expect(page).to have_content("Successfully authenticated from DBT Staff SSO account.")
+ expect(page).to have_selector(:link_or_button, "Sign out")
+ end
+end
diff --git a/spec/models/admin_spec.rb b/spec/models/admin_spec.rb
index b0c7dac295..a568161c45 100644
--- a/spec/models/admin_spec.rb
+++ b/spec/models/admin_spec.rb
@@ -2,17 +2,11 @@
require "rails_helper"
-RSpec.describe Admin, type: :model do
+describe Admin, type: :model do
describe "#create" do
- it "creates an autosave token for an admin" do
- admin = Admin.create!(
- email: "john@example.com",
- first_name: "John",
- last_name: "Smith",
- password: "^#ur9EkLm@1W+OaDvg",
- password_confirmation: "^#ur9EkLm@1W+OaDvg",
- )
+ let(:admin) { Admin.create!(email: "john@example.com", first_name: "John", last_name: "Smith") }
+ it "creates an autosave token for an admin" do
expect(admin.autosave_token).not_to be nil
end
end
@@ -30,16 +24,10 @@
end
describe "soft_delete!" do
+ let(:admin) { create(:admin) }
+
it "should set deleted" do
- admin = create(:admin)
- admin.soft_delete!
- expect(admin.deleted.present?).to be_truthy
+ expect { admin.soft_delete! }.to change { admin.deleted.present? }.from(false).to(true)
end
end
-
- context "devise mailers" do
- let(:user) { create(:admin) }
-
- include_context "devise mailers instructions"
- end
end
diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb
index c780c9636a..673b493298 100644
--- a/spec/rails_helper.rb
+++ b/spec/rails_helper.rb
@@ -115,6 +115,8 @@ def matches?(source_file)
config.include(Shoulda::Matchers::ActiveModel, type: :model)
config.include(Shoulda::Matchers::ActiveRecord, type: :model)
end
+
+ OmniAuth.config.test_mode = true
end
RSpec::Matchers.define_negated_matcher :not_change, :change
diff --git a/spec/support/user_step_definitions.rb b/spec/support/user_step_definitions.rb
index b6272e282d..aa8de6a2f7 100644
--- a/spec/support/user_step_definitions.rb
+++ b/spec/support/user_step_definitions.rb
@@ -1,8 +1,28 @@
module UserStepDefinitions
- def login_admin(admin)
+ def login_admin(admin, stub_sso: true)
+ if stub_sso
+ stub_sso_request(
+ email: admin.email,
+ first_name: admin.first_name,
+ last_name: admin.last_name,
+ provider: :dbt_staff_sso,
+ )
+ end
+
visit "/admins/sign_in"
- fill_in "admin_email", with: admin.email
- fill_in "admin_password", with: "my98ssdkjv9823kds=2"
- click_button "Sign in"
+ click_link "Sign in with Dbt Staff Sso"
end
+
+ def stub_sso_request(email:, first_name:, last_name:, provider:, uid: SecureRandom.uuid, auth: OmniAuth)
+ auth.config.mock_auth[provider] = AuthStruct.new(
+ uid,
+ provider,
+ InfoStruct.new(email, first_name, last_name),
+ )
+ end
+
+ private
+
+ AuthStruct = Struct.new(:uid, :provider, :info)
+ InfoStruct = Struct.new(:email, :first_name, :last_name)
end