mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-19 05:09:38 +02:00
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 <zach@maybe.co>
This commit is contained in:
parent
4a3685f503
commit
c7c281073f
29 changed files with 477 additions and 16 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -44,7 +44,6 @@
|
|||
|
||||
# Ignore VS Code
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/extensions.json
|
||||
!.vscode/*.code-snippets
|
||||
|
||||
|
|
6
.vscode/settings.json
vendored
6
.vscode/settings.json
vendored
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"[javascript]": {
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "biomejs.biome",
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 }
|
||||
|
|
21
app/controllers/concerns/impersonatable.rb
Normal file
21
app/controllers/concerns/impersonatable.rb
Normal file
|
@ -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
|
58
app/controllers/impersonation_sessions_controller.rb
Normal file
58
app/controllers/impersonation_sessions_controller.rb
Normal file
|
@ -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
|
2
app/helpers/impersonation_sessions_helper.rb
Normal file
2
app/helpers/impersonation_sessions_helper.rb
Normal file
|
@ -0,0 +1,2 @@
|
|||
module ImpersonationSessionsHelper
|
||||
end
|
|
@ -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
|
||||
|
|
39
app/models/impersonation_session.rb
Normal file
39
app/models/impersonation_session.rb
Normal file
|
@ -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
|
3
app/models/impersonation_session_log.rb
Normal file
3
app/models/impersonation_session_log.rb
Normal file
|
@ -0,0 +1,3 @@
|
|||
class ImpersonationSessionLog < ApplicationRecord
|
||||
belongs_to :impersonation_session
|
||||
end
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
21
app/views/impersonation_sessions/_approval_bar.html.erb
Normal file
21
app/views/impersonation_sessions/_approval_bar.html.erb
Normal file
|
@ -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 %>
|
||||
|
||||
<div class="sticky top-0 left-0 w-full bg-black flex items-center justify-between font-mono">
|
||||
<div class="flex items-center bg-red-600 px-6 py-4">
|
||||
<%= lucide_icon "alert-triangle", class: "w-6 h-6 text-white mr-2" %>
|
||||
<span class="text-white font-semibold uppercase">Access <%= in_progress_session.present? ? "Session" : "Request" %></span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2 px-2 py-2 text-white gap-4">
|
||||
<% if pending_session.present? %>
|
||||
<p class="text-xs max-w-3xl text-right">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.</p>
|
||||
<%= 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? %>
|
||||
<p class="text-xs max-w-3xl text-right">Someone from the Maybe Finance team is currently viewing your data. You may end the session at any time.</p>
|
||||
<%= 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 %>
|
||||
<p class="text-xs max-w-3xl text-right text-red-500">Something went wrong. Please contact us.</p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
35
app/views/impersonation_sessions/_super_admin_bar.html.erb
Normal file
35
app/views/impersonation_sessions/_super_admin_bar.html.erb
Normal file
|
@ -0,0 +1,35 @@
|
|||
<div class="sticky top-0 left-0 w-full bg-black flex items-center justify-between font-mono">
|
||||
<div class="flex items-center bg-red-600 px-6 py-4">
|
||||
<%= lucide_icon "alert-triangle", class: "w-6 h-6 text-white mr-2" %>
|
||||
<span class="text-white font-semibold uppercase">Super Admin</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2 px-2 py-2 text-white">
|
||||
<% if Current.session.active_impersonator_session.present? %>
|
||||
<div class="flex items-center space-x-3 bg-gray-800 border border-gray-700 rounded-md pl-3">
|
||||
<div class="text-sm">
|
||||
Impersonating: <span class="font-semibold text-red-400"><%= Current.impersonated_user.email %></span>
|
||||
</div>
|
||||
<%= 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" %>
|
||||
</div>
|
||||
<% 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 %>
|
||||
</div>
|
||||
</div>
|
|
@ -25,6 +25,9 @@
|
|||
</head>
|
||||
|
||||
<body class="h-full">
|
||||
<%= 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? %>
|
||||
|
||||
<div class="fixed z-50 space-y-1 top-6 right-10">
|
||||
<div id="notification-tray">
|
||||
<%= render_flash_notifications %>
|
||||
|
|
|
@ -2,4 +2,4 @@
|
|||
<div class="text-white font-normal">
|
||||
<%= tooltip_text %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
15
config/locales/views/impersonation_sessions/en.yml
Normal file
15
config/locales/views/impersonation_sessions/en.yml
Normal file
|
@ -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"
|
|
@ -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"
|
||||
|
||||
|
|
23
db/migrate/20241009214601_add_super_admin_to_users.rb
Normal file
23
db/migrate/20241009214601_add_super_admin_to_users.rb
Normal file
|
@ -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
|
12
db/migrate/20241017162347_create_impersonation_sessions.rb
Normal file
12
db/migrate/20241017162347_create_impersonation_sessions.rb
Normal file
|
@ -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
|
|
@ -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
|
6
db/schema.rb
generated
6
db/schema.rb
generated
|
@ -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"
|
||||
|
|
112
test/controllers/impersonation_sessions_controller_test.rb
Normal file
112
test/controllers/impersonation_sessions_controller_test.rb
Normal file
|
@ -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
|
11
test/fixtures/impersonation_session_logs.yml
vendored
Normal file
11
test/fixtures/impersonation_session_logs.yml
vendored
Normal file
|
@ -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
|
4
test/fixtures/impersonation_sessions.yml
vendored
Normal file
4
test/fixtures/impersonation_sessions.yml
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
in_progress:
|
||||
impersonator: maybe_support_staff
|
||||
impersonated: family_member
|
||||
status: in_progress
|
8
test/fixtures/users.yml
vendored
8
test/fixtures/users.yml
vendored
|
@ -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
|
||||
|
|
7
test/models/impersonation_session_log_test.rb
Normal file
7
test/models/impersonation_session_log_test.rb
Normal file
|
@ -0,0 +1,7 @@
|
|||
require "test_helper"
|
||||
|
||||
class ImpersonationSessionLogTest < ActiveSupport::TestCase
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
end
|
40
test/models/impersonation_session_test.rb
Normal file
40
test/models/impersonation_session_test.rb
Normal file
|
@ -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
|
Loading…
Add table
Add a link
Reference in a new issue