From c7c281073f8a5b20b6a125d539f5f537e280d0a7 Mon Sep 17 00:00:00 2001 From: Josh Pigford Date: Fri, 18 Oct 2024 11:26:58 -0500 Subject: [PATCH] Impersonation (#1325) * Initial impersonation * Impersonation audit * Keep super admin separate * Remove vscode settings * Comment cleanup * Comment out impersonation fixtures for now * Remove unused controlelr * Add impersonation testing (#1326) * Add impersonation testing * Remove unused method * Update schema.rb * Update brakeman --------- Co-authored-by: Zach Gollwitzer --- .gitignore | 1 - .vscode/settings.json | 6 - Gemfile.lock | 2 +- app/controllers/application_controller.rb | 2 +- app/controllers/concerns/authentication.rb | 6 +- app/controllers/concerns/impersonatable.rb | 21 ++++ .../impersonation_sessions_controller.rb | 58 +++++++++ app/helpers/impersonation_sessions_helper.rb | 2 + app/models/current.rb | 16 ++- app/models/impersonation_session.rb | 39 ++++++ app/models/impersonation_session_log.rb | 3 + app/models/session.rb | 4 + app/models/user.rb | 9 +- .../_approval_bar.html.erb | 21 ++++ .../_super_admin_bar.html.erb | 35 ++++++ app/views/layouts/application.html.erb | 3 + app/views/shared/_text_tooltip.erb | 2 +- .../views/impersonation_sessions/en.yml | 15 +++ config/routes.rb | 11 ++ ...20241009214601_add_super_admin_to_users.rb | 23 ++++ ...017162347_create_impersonation_sessions.rb | 12 ++ ...62536_create_impersonation_session_logs.rb | 14 +++ db/schema.rb | 6 +- .../impersonation_sessions_controller_test.rb | 112 ++++++++++++++++++ test/fixtures/impersonation_session_logs.yml | 11 ++ test/fixtures/impersonation_sessions.yml | 4 + test/fixtures/users.yml | 8 ++ test/models/impersonation_session_log_test.rb | 7 ++ test/models/impersonation_session_test.rb | 40 +++++++ 29 files changed, 477 insertions(+), 16 deletions(-) delete mode 100644 .vscode/settings.json create mode 100644 app/controllers/concerns/impersonatable.rb create mode 100644 app/controllers/impersonation_sessions_controller.rb create mode 100644 app/helpers/impersonation_sessions_helper.rb create mode 100644 app/models/impersonation_session.rb create mode 100644 app/models/impersonation_session_log.rb create mode 100644 app/views/impersonation_sessions/_approval_bar.html.erb create mode 100644 app/views/impersonation_sessions/_super_admin_bar.html.erb create mode 100644 config/locales/views/impersonation_sessions/en.yml create mode 100644 db/migrate/20241009214601_add_super_admin_to_users.rb create mode 100644 db/migrate/20241017162347_create_impersonation_sessions.rb create mode 100644 db/migrate/20241017162536_create_impersonation_session_logs.rb create mode 100644 test/controllers/impersonation_sessions_controller_test.rb create mode 100644 test/fixtures/impersonation_session_logs.yml create mode 100644 test/fixtures/impersonation_sessions.yml create mode 100644 test/models/impersonation_session_log_test.rb create mode 100644 test/models/impersonation_session_test.rb diff --git a/.gitignore b/.gitignore index a630037b..3e1cbccd 100644 --- a/.gitignore +++ b/.gitignore @@ -44,7 +44,6 @@ # Ignore VS Code .vscode/* -!.vscode/settings.json !.vscode/extensions.json !.vscode/*.code-snippets diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index f94a8b2b..00000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "[javascript]": { - "editor.formatOnSave": true, - "editor.defaultFormatter": "biomejs.biome", - } -} \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock index 21810fe3..51ea10a8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -110,7 +110,7 @@ GEM bindex (0.8.1) bootsnap (1.18.4) msgpack (~> 1.2) - brakeman (6.2.1) + brakeman (6.2.2) racc builder (3.3.0) capybara (3.40.0) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index c5145b17..b6fa8e0c 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,5 +1,5 @@ class ApplicationController < ActionController::Base - include Localize, AutoSync, Authentication, Invitable, SelfHostable, StoreLocation + include Localize, AutoSync, Authentication, Invitable, SelfHostable, StoreLocation, Impersonatable include Pagy::Backend private diff --git a/app/controllers/concerns/authentication.rb b/app/controllers/concerns/authentication.rb index cd210cca..a66d1b3f 100644 --- a/app/controllers/concerns/authentication.rb +++ b/app/controllers/concerns/authentication.rb @@ -14,7 +14,7 @@ module Authentication private def authenticate_user! - if session_record = Session.find_by_id(cookies.signed[:session_token]) + if session_record = find_session_by_cookie Current.session = session_record else if self_hosted_first_login? @@ -25,6 +25,10 @@ module Authentication end end + def find_session_by_cookie + Session.find_by(id: cookies.signed[:session_token]) + end + def create_session_for(user) session = user.sessions.create! cookies.signed.permanent[:session_token] = { value: session.id, httponly: true } diff --git a/app/controllers/concerns/impersonatable.rb b/app/controllers/concerns/impersonatable.rb new file mode 100644 index 00000000..857b68c4 --- /dev/null +++ b/app/controllers/concerns/impersonatable.rb @@ -0,0 +1,21 @@ +module Impersonatable + extend ActiveSupport::Concern + + included do + after_action :create_impersonation_session_log + end + + private + def create_impersonation_session_log + return unless Current.session&.active_impersonator_session.present? + + Current.session.active_impersonator_session.logs.create!( + controller: controller_name, + action: action_name, + path: request.fullpath, + method: request.method, + ip_address: request.ip, + user_agent: request.user_agent + ) + end +end diff --git a/app/controllers/impersonation_sessions_controller.rb b/app/controllers/impersonation_sessions_controller.rb new file mode 100644 index 00000000..1a8c5db7 --- /dev/null +++ b/app/controllers/impersonation_sessions_controller.rb @@ -0,0 +1,58 @@ +class ImpersonationSessionsController < ApplicationController + before_action :require_super_admin!, only: [ :create, :join, :leave ] + before_action :set_impersonation_session, only: [ :approve, :reject, :complete ] + + def create + Current.true_user.request_impersonation_for(session_params[:impersonated_id]) + redirect_to root_path, notice: t(".success") + end + + def join + @impersonation_session = Current.true_user.impersonator_support_sessions.find_by(id: params[:impersonation_session_id]) + Current.session.update!(active_impersonator_session: @impersonation_session) + redirect_to root_path, notice: t(".success") + end + + def leave + Current.session.update!(active_impersonator_session: nil) + redirect_to root_path, notice: t(".success") + end + + def approve + raise_unauthorized! unless @impersonation_session.impersonated == Current.true_user + + @impersonation_session.approve! + redirect_to root_path, notice: t(".success") + end + + def reject + raise_unauthorized! unless @impersonation_session.impersonated == Current.true_user + + @impersonation_session.reject! + redirect_to root_path, notice: t(".success") + end + + def complete + @impersonation_session.complete! + redirect_to root_path, notice: t(".success") + end + + private + def session_params + params.require(:impersonation_session).permit(:impersonated_id) + end + + def set_impersonation_session + @impersonation_session = + Current.true_user.impersonated_support_sessions.find_by(id: params[:id]) || + Current.true_user.impersonator_support_sessions.find_by(id: params[:id]) + end + + def require_super_admin! + raise_unauthorized! unless Current.true_user&.super_admin? + end + + def raise_unauthorized! + raise ActionController::RoutingError.new("Not Found") + end +end diff --git a/app/helpers/impersonation_sessions_helper.rb b/app/helpers/impersonation_sessions_helper.rb new file mode 100644 index 00000000..f955b896 --- /dev/null +++ b/app/helpers/impersonation_sessions_helper.rb @@ -0,0 +1,2 @@ +module ImpersonationSessionsHelper +end diff --git a/app/models/current.rb b/app/models/current.rb index d12fc0a9..86b9e97a 100644 --- a/app/models/current.rb +++ b/app/models/current.rb @@ -1,7 +1,19 @@ class Current < ActiveSupport::CurrentAttributes - attribute :session attribute :user_agent, :ip_address - delegate :user, to: :session, allow_nil: true + attribute :session + delegate :family, to: :user, allow_nil: true + + def user + impersonated_user || session&.user + end + + def impersonated_user + session&.active_impersonator_session&.impersonated + end + + def true_user + session&.user + end end diff --git a/app/models/impersonation_session.rb b/app/models/impersonation_session.rb new file mode 100644 index 00000000..0e0fd582 --- /dev/null +++ b/app/models/impersonation_session.rb @@ -0,0 +1,39 @@ +class ImpersonationSession < ApplicationRecord + belongs_to :impersonator, class_name: "User" + belongs_to :impersonated, class_name: "User" + + has_many :logs, class_name: "ImpersonationSessionLog" + + enum :status, { pending: "pending", in_progress: "in_progress", complete: "complete", rejected: "rejected" } + + scope :initiated, -> { where(status: [ :pending, :in_progress ]) } + + validate :impersonator_is_super_admin + validate :impersonated_is_not_super_admin + validate :impersonator_different_from_impersonated + + def approve! + update! status: :in_progress + end + + def reject! + update! status: :rejected + end + + def complete! + update! status: :complete + end + + private + def impersonator_is_super_admin + errors.add(:impersonator, "must be a super admin to impersonate") unless impersonator.super_admin? + end + + def impersonated_is_not_super_admin + errors.add(:impersonated, "cannot be a super admin") if impersonated.super_admin? + end + + def impersonator_different_from_impersonated + errors.add(:impersonator, "cannot be the same as the impersonated user") if impersonator == impersonated + end +end diff --git a/app/models/impersonation_session_log.rb b/app/models/impersonation_session_log.rb new file mode 100644 index 00000000..d7fa5ac3 --- /dev/null +++ b/app/models/impersonation_session_log.rb @@ -0,0 +1,3 @@ +class ImpersonationSessionLog < ApplicationRecord + belongs_to :impersonation_session +end diff --git a/app/models/session.rb b/app/models/session.rb index 8e94aa81..ce26938a 100644 --- a/app/models/session.rb +++ b/app/models/session.rb @@ -1,5 +1,9 @@ class Session < ApplicationRecord belongs_to :user + belongs_to :active_impersonator_session, + -> { where(status: :in_progress) }, + class_name: "ImpersonationSession", + optional: true before_create do self.user_agent = Current.user_agent diff --git a/app/models/user.rb b/app/models/user.rb index fe2a72c9..789e39df 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -3,6 +3,8 @@ class User < ApplicationRecord belongs_to :family has_many :sessions, dependent: :destroy + has_many :impersonator_support_sessions, class_name: "ImpersonationSession", foreign_key: :impersonator_id, dependent: :destroy + has_many :impersonated_support_sessions, class_name: "ImpersonationSession", foreign_key: :impersonated_id, dependent: :destroy accepts_nested_attributes_for :family validates :email, presence: true, uniqueness: true @@ -11,7 +13,7 @@ class User < ApplicationRecord normalizes :first_name, :last_name, with: ->(value) { value.strip.presence } - enum :role, { member: "member", admin: "admin" }, validate: true + enum :role, { member: "member", admin: "admin", super_admin: "super_admin" }, validate: true has_one_attached :profile_image do |attachable| attachable.variant :thumbnail, resize_to_fill: [ 300, 300 ] @@ -23,6 +25,11 @@ class User < ApplicationRecord password_salt&.last(10) end + def request_impersonation_for(user_id) + impersonated = User.find(user_id) + impersonator_support_sessions.create!(impersonated: impersonated) + end + def display_name [ first_name, last_name ].compact.join(" ").presence || email end diff --git a/app/views/impersonation_sessions/_approval_bar.html.erb b/app/views/impersonation_sessions/_approval_bar.html.erb new file mode 100644 index 00000000..2bc83087 --- /dev/null +++ b/app/views/impersonation_sessions/_approval_bar.html.erb @@ -0,0 +1,21 @@ +<% pending_session = Current.true_user.impersonated_support_sessions.pending.first %> +<% in_progress_session = Current.true_user.impersonated_support_sessions.in_progress.first %> + +
+
+ <%= lucide_icon "alert-triangle", class: "w-6 h-6 text-white mr-2" %> + Access <%= in_progress_session.present? ? "Session" : "Request" %> +
+
+ <% if pending_session.present? %> +

Maybe support staff has requested access to your account (likely to help you with a support request). If you approve the request, all activity they take will be logged for security and audit purposes.

+ <%= button_to "Approve", approve_impersonation_session_path(pending_session), method: :put, class: "inline-flex items-center px-3 py-1.5 border border-transparent text-sm font-medium rounded-md text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500" %> + <%= button_to "Reject", reject_impersonation_session_path(pending_session), method: :put, class: "inline-flex items-center px-3 py-1.5 border border-transparent text-sm font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500" %> + <% elsif in_progress_session.present? %> +

Someone from the Maybe Finance team is currently viewing your data. You may end the session at any time.

+ <%= button_to "End Session", complete_impersonation_session_path(in_progress_session), method: :put, class: "inline-flex items-center px-3 py-1.5 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" %> + <% else %> +

Something went wrong. Please contact us.

+ <% end %> +
+
diff --git a/app/views/impersonation_sessions/_super_admin_bar.html.erb b/app/views/impersonation_sessions/_super_admin_bar.html.erb new file mode 100644 index 00000000..fddb270b --- /dev/null +++ b/app/views/impersonation_sessions/_super_admin_bar.html.erb @@ -0,0 +1,35 @@ +
+
+ <%= lucide_icon "alert-triangle", class: "w-6 h-6 text-white mr-2" %> + Super Admin +
+
+ <% if Current.session.active_impersonator_session.present? %> +
+
+ Impersonating: <%= Current.impersonated_user.email %> +
+ <%= button_to "Leave", leave_impersonation_sessions_path, method: :delete, class: "items-center px-3 py-1.5 border border-transparent text-sm font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500" %> + <%= button_to "Terminate", complete_impersonation_session_path(Current.session.active_impersonator_session), method: :put, class: "items-center px-3 py-1.5 border border-transparent text-sm font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500" %> +
+ <% else %> + <% if Current.true_user.impersonator_support_sessions.in_progress.any? %> + <%= form_with url: join_impersonation_sessions_path, class: "flex items-center space-x-2 mr-4" do |f| %> + <%= f.select :impersonation_session_id, + Current.true_user.impersonator_support_sessions.in_progress.map { |session| + ["#{session.impersonated.email} (#{session.status})", session.id] + }, + { prompt: "Join a session" }, + { class: "rounded-md text-sm border-0 focus:ring-0 ring-0 text-black font-mono" } %> + <%= f.submit "Join", + class: "inline-flex items-center px-3 py-1.5 border border-transparent text-sm font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500" %> + <% end %> + <% end %> + + <%= form_with model: ImpersonationSession.new, class: "flex items-center space-x-2" do |f| %> + <%= f.text_field :impersonated_id, class: "rounded-md text-sm border-0 focus:ring-0 ring-0 text-black font-mono w-96", placeholder: "UUID", autocomplete: "off" %> + <%= f.submit "Request Impersonation", class: "inline-flex items-center px-3 py-1.5 border border-transparent text-sm font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500" %> + <% end %> + <% end %> +
+
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index e7712e13..386e839d 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -25,6 +25,9 @@ + <%= render "impersonation_sessions/super_admin_bar" if Current.true_user&.super_admin? %> + <%= render "impersonation_sessions/approval_bar" if Current.true_user&.impersonated_support_sessions&.initiated&.any? %> +
<%= render_flash_notifications %> diff --git a/app/views/shared/_text_tooltip.erb b/app/views/shared/_text_tooltip.erb index 03de7e48..53f93dd3 100644 --- a/app/views/shared/_text_tooltip.erb +++ b/app/views/shared/_text_tooltip.erb @@ -2,4 +2,4 @@
<%= tooltip_text %>
-
\ No newline at end of file +
diff --git a/config/locales/views/impersonation_sessions/en.yml b/config/locales/views/impersonation_sessions/en.yml new file mode 100644 index 00000000..be18173f --- /dev/null +++ b/config/locales/views/impersonation_sessions/en.yml @@ -0,0 +1,15 @@ +--- +en: + impersonation_sessions: + create: + success: "Request sent to user. Waiting for approval." + join: + success: "Joined session" + leave: + success: "Left session" + approve: + success: "Request approved" + reject: + success: "Request rejected" + complete: + success: "Session completed" \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index 9a84ce22..2cfa2b2d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -110,6 +110,17 @@ Rails.application.routes.draw do resources :currencies, only: %i[show] + resources :impersonation_sessions, only: [ :create ] do + post :join, on: :collection + delete :leave, on: :collection + + member do + put :approve + put :reject + put :complete + end + end + # Stripe webhook endpoint post "webhooks/stripe", to: "webhooks#stripe" diff --git a/db/migrate/20241009214601_add_super_admin_to_users.rb b/db/migrate/20241009214601_add_super_admin_to_users.rb new file mode 100644 index 00000000..cc1fadb4 --- /dev/null +++ b/db/migrate/20241009214601_add_super_admin_to_users.rb @@ -0,0 +1,23 @@ +class AddSuperAdminToUsers < ActiveRecord::Migration[7.2] + def change + reversible do |dir| + dir.up do + change_column :users, :role, :string, default: 'member' + + execute <<-SQL + DROP TYPE user_role; + SQL + end + + dir.down do + execute <<-SQL + CREATE TYPE user_role AS ENUM ('admin', 'member'); + SQL + + change_column_default :users, :role, nil + change_column :users, :role, :user_role, using: 'role::user_role' + change_column_default :users, :role, 'member' + end + end + end +end diff --git a/db/migrate/20241017162347_create_impersonation_sessions.rb b/db/migrate/20241017162347_create_impersonation_sessions.rb new file mode 100644 index 00000000..1e7cf38e --- /dev/null +++ b/db/migrate/20241017162347_create_impersonation_sessions.rb @@ -0,0 +1,12 @@ +class CreateImpersonationSessions < ActiveRecord::Migration[7.2] + def change + create_table :impersonation_sessions, id: :uuid do |t| + t.references :impersonator, null: false, foreign_key: { to_table: :users }, type: :uuid + t.references :impersonated, null: false, foreign_key: { to_table: :users }, type: :uuid + t.string :status, null: false, default: 'pending' + t.timestamps + end + + add_reference :sessions, :active_impersonator_session, type: :uuid, foreign_key: { to_table: :impersonation_sessions } + end +end diff --git a/db/migrate/20241017162536_create_impersonation_session_logs.rb b/db/migrate/20241017162536_create_impersonation_session_logs.rb new file mode 100644 index 00000000..ebf68d91 --- /dev/null +++ b/db/migrate/20241017162536_create_impersonation_session_logs.rb @@ -0,0 +1,14 @@ +class CreateImpersonationSessionLogs < ActiveRecord::Migration[7.2] + def change + create_table :impersonation_session_logs, id: :uuid do |t| + t.references :impersonation_session, type: :uuid, foreign_key: true, null: false + t.string :controller + t.string :action + t.text :path + t.string :method + t.string :ip_address + t.text :user_agent + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 49e43264..c5a313ea 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -19,7 +19,7 @@ ActiveRecord::Schema[7.2].define(version: 2024_10_17_204250) do # Note that some types may not work with other database engines. Be careful if changing database. create_enum "account_status", ["ok", "syncing", "error"] create_enum "import_status", ["pending", "importing", "complete", "failed"] - create_enum "user_role", ["admin", "member"] + create_enum "user_role", ["admin", "member", "super_admin"] create_table "account_balances", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| t.uuid "account_id", null: false @@ -493,6 +493,8 @@ ActiveRecord::Schema[7.2].define(version: 2024_10_17_204250) do t.string "ip_address" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.uuid "active_impersonator_session_id" + t.index ["active_impersonator_session_id"], name: "index_sessions_on_active_impersonator_session_id" t.index ["user_id"], name: "index_sessions_on_user_id" end @@ -535,7 +537,6 @@ ActiveRecord::Schema[7.2].define(version: 2024_10_17_204250) do t.string "last_alerted_upgrade_commit_sha" t.enum "role", default: "member", null: false, enum_type: "user_role" t.boolean "active", default: true, null: false - t.boolean "super_admin", default: false t.index ["email"], name: "index_users_on_email", unique: true t.index ["family_id"], name: "index_users_on_family_id" end @@ -573,6 +574,7 @@ ActiveRecord::Schema[7.2].define(version: 2024_10_17_204250) do add_foreign_key "imports", "families" add_foreign_key "institutions", "families" add_foreign_key "merchants", "families" + add_foreign_key "sessions", "impersonation_sessions", column: "active_impersonator_session_id" add_foreign_key "sessions", "users" add_foreign_key "taggings", "tags" add_foreign_key "tags", "families" diff --git a/test/controllers/impersonation_sessions_controller_test.rb b/test/controllers/impersonation_sessions_controller_test.rb new file mode 100644 index 00000000..65fe4bf5 --- /dev/null +++ b/test/controllers/impersonation_sessions_controller_test.rb @@ -0,0 +1,112 @@ +require "test_helper" + +class ImpersonationSessionsControllerTest < ActionDispatch::IntegrationTest + test "impersonation session logs all activity for auditing" do + sign_in impersonator = users(:maybe_support_staff) + impersonated = users(:family_member) + + impersonator_session = impersonation_sessions(:in_progress) + + post join_impersonation_sessions_path, params: { impersonation_session_id: impersonator_session.id } + + assert_difference "impersonator_session.logs.count", 2 do + get root_path + get account_path(impersonated.family.accounts.first) + end + end + + test "super admin can request an impersonation session" do + sign_in users(:maybe_support_staff) + + post impersonation_sessions_path, params: { impersonation_session: { impersonated_id: users(:family_member).id } } + + assert_equal "Request sent to user. Waiting for approval.", flash[:notice] + assert_redirected_to root_path + end + + test "super admin can join and leave an in progress impersonation session" do + sign_in super_admin = users(:maybe_support_staff) + + impersonator_session = impersonation_sessions(:in_progress) + + super_admin_session = super_admin.sessions.order(created_at: :desc).first + + assert_nil super_admin_session.active_impersonator_session + + # Joining the session + post join_impersonation_sessions_path, params: { impersonation_session_id: impersonator_session.id } + assert_equal impersonator_session, super_admin_session.reload.active_impersonator_session + assert_equal "Joined session", flash[:notice] + assert_redirected_to root_path + + follow_redirect! + + # Leaving the session + delete leave_impersonation_sessions_path + assert_nil super_admin_session.reload.active_impersonator_session + assert_equal "Left session", flash[:notice] + assert_redirected_to root_path + + # Impersonation session still in progress because nobody has ended it yet + assert_equal "in_progress", impersonator_session.reload.status + end + + test "super admin can complete an impersonation session" do + sign_in super_admin = users(:maybe_support_staff) + + impersonator_session = impersonation_sessions(:in_progress) + + put complete_impersonation_session_path(impersonator_session) + + assert_equal "Session completed", flash[:notice] + assert_nil super_admin.sessions.order(created_at: :desc).first.active_impersonator_session + assert_equal "complete", impersonator_session.reload.status + assert_redirected_to root_path + end + + test "regular user can complete an impersonation session" do + sign_in regular_user = users(:family_member) + + impersonator_session = impersonation_sessions(:in_progress) + + put complete_impersonation_session_path(impersonator_session) + + assert_equal "Session completed", flash[:notice] + assert_equal "complete", impersonator_session.reload.status + assert_redirected_to root_path + end + + test "super admin cannot accept an impersonation session" do + sign_in super_admin = users(:maybe_support_staff) + + impersonator_session = impersonation_sessions(:in_progress) + + put approve_impersonation_session_path(impersonator_session) + + assert_response :not_found + end + + test "regular user can accept an impersonation session" do + sign_in regular_user = users(:family_member) + + impersonator_session = impersonation_sessions(:in_progress) + + put approve_impersonation_session_path(impersonator_session) + + assert_equal "Request approved", flash[:notice] + assert_equal "in_progress", impersonator_session.reload.status + assert_redirected_to root_path + end + + test "regular user can reject an impersonation session" do + sign_in regular_user = users(:family_member) + + impersonator_session = impersonation_sessions(:in_progress) + + put reject_impersonation_session_path(impersonator_session) + + assert_equal "Request rejected", flash[:notice] + assert_equal "rejected", impersonator_session.reload.status + assert_redirected_to root_path + end +end diff --git a/test/fixtures/impersonation_session_logs.yml b/test/fixtures/impersonation_session_logs.yml new file mode 100644 index 00000000..e11ea93e --- /dev/null +++ b/test/fixtures/impersonation_session_logs.yml @@ -0,0 +1,11 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +# This model initially had no columns defined. If you add columns to the +# model remove the "{}" from the fixture names and add the columns immediately +# below each fixture, per the syntax in the comments below +# +#one: {} +# column: value +# +#two: {} +# column: value diff --git a/test/fixtures/impersonation_sessions.yml b/test/fixtures/impersonation_sessions.yml new file mode 100644 index 00000000..becd9e7c --- /dev/null +++ b/test/fixtures/impersonation_sessions.yml @@ -0,0 +1,4 @@ +in_progress: + impersonator: maybe_support_staff + impersonated: family_member + status: in_progress diff --git a/test/fixtures/users.yml b/test/fixtures/users.yml index 3c9aa4ed..b717c9cb 100644 --- a/test/fixtures/users.yml +++ b/test/fixtures/users.yml @@ -5,6 +5,14 @@ empty: email: user1@email.com password_digest: <%= BCrypt::Password.create('password') %> +maybe_support_staff: + family: empty + first_name: Support + last_name: Admin + email: support@maybe.co + password_digest: <%= BCrypt::Password.create('password') %> + role: super_admin + family_admin: family: dylan_family first_name: Bob diff --git a/test/models/impersonation_session_log_test.rb b/test/models/impersonation_session_log_test.rb new file mode 100644 index 00000000..f620ebb1 --- /dev/null +++ b/test/models/impersonation_session_log_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class ImpersonationSessionLogTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/models/impersonation_session_test.rb b/test/models/impersonation_session_test.rb new file mode 100644 index 00000000..1946e6da --- /dev/null +++ b/test/models/impersonation_session_test.rb @@ -0,0 +1,40 @@ +require "test_helper" + +class ImpersonationSessionTest < ActiveSupport::TestCase + test "only super admin can impersonate" do + regular_user = users(:family_member) + + assert_not regular_user.super_admin? + + assert_raises(ActiveRecord::RecordInvalid) do + ImpersonationSession.create!( + impersonator: regular_user, + impersonated: users(:maybe_support_staff) + ) + end + end + + test "super admin cannot be impersonated" do + super_admin = users(:maybe_support_staff) + + assert super_admin.super_admin? + + assert_raises(ActiveRecord::RecordInvalid) do + ImpersonationSession.create!( + impersonator: users(:family_member), + impersonated: super_admin + ) + end + end + + test "impersonation session must have different impersonator and impersonated" do + super_admin = users(:maybe_support_staff) + + assert_raises(ActiveRecord::RecordInvalid) do + ImpersonationSession.create!( + impersonator: super_admin, + impersonated: super_admin + ) + end + end +end