feat: setup invite to handle SAML enabled account [CW-5613] (#12439)

This commit is contained in:
Shivam Mishra
2025-09-17 19:33:38 +05:30
committed by GitHub
parent de4430ea5d
commit 7bc7ae5bc4
5 changed files with 349 additions and 0 deletions

View File

@@ -52,3 +52,5 @@ class AgentBuilder
}.compact))
end
end
AgentBuilder.prepend_mod_with('AgentBuilder')

View File

@@ -0,0 +1,13 @@
module Enterprise::AgentBuilder
def perform
super.tap do |user|
convert_to_saml_provider(user) if user.persisted? && account.saml_enabled?
end
end
private
def convert_to_saml_provider(user)
user.update!(provider: 'saml') unless user.provider == 'saml'
end
end

View File

@@ -0,0 +1,45 @@
<p>Hi <%= @resource.name %>,</p>
<% account_user = @resource&.account_users&.first %>
<% is_saml_account = account_user&.account&.saml_enabled? %>
<% if account_user&.inviter.present? && @resource.unconfirmed_email.blank? %>
<% if is_saml_account %>
<p><%= account_user.inviter.name %>, with <%= account_user.account.name %>, has invited you to access <%= global_config['BRAND_NAME'] || 'Chatwoot' %> via Single Sign-On (SSO).</p>
<p>Your organization uses SSO for secure authentication. You will not need a password to access your account.</p>
<% else %>
<p><%= account_user.inviter.name %>, with <%= account_user.account.name %>, has invited you to try out <%= global_config['BRAND_NAME'] || 'Chatwoot' %>.</p>
<% end %>
<% end %>
<% if @resource.confirmed? %>
<p>You can login to your <%= global_config['BRAND_NAME'] || 'Chatwoot' %> account through the link below:</p>
<% else %>
<% if account_user&.inviter.blank? %>
<p>
Welcome to <%= global_config['BRAND_NAME'] || 'Chatwoot' %>! We have a suite of powerful tools ready for you to explore. Before that we quickly need to verify your email address to know it's really you.
</p>
<% end %>
<% unless is_saml_account %>
<p>Please take a moment and click the link below and activate your account.</p>
<% end %>
<% end %>
<% if @resource.unconfirmed_email.present? %>
<p><%= link_to 'Confirm my account', frontend_url('auth/confirmation', confirmation_token: @token) %></p>
<% elsif @resource.confirmed? %>
<% if is_saml_account %>
<p>You can now access your account by logging in through your organization's SSO portal.</p>
<% else %>
<p><%= link_to 'Login to my account', frontend_url('auth/sign_in') %></p>
<% end %>
<% elsif account_user&.inviter.present? %>
<% if is_saml_account %>
<p>You can access your account by logging in through your organization's SSO portal.</p>
<% else %>
<p><%= link_to 'Confirm my account', frontend_url('auth/password/edit', reset_password_token: @resource.send(:set_reset_password_token)) %></p>
<% end %>
<% else %>
<p><%= link_to 'Confirm my account', frontend_url('auth/confirmation', confirmation_token: @token) %></p>
<% end %>

View File

@@ -0,0 +1,139 @@
require 'rails_helper'
RSpec.describe AgentBuilder do
let(:email) { 'agent@example.com' }
let(:name) { 'Test Agent' }
let(:account) { create(:account) }
let!(:inviter) { create(:user, account: account, role: 'administrator') }
let(:builder) do
described_class.new(
email: email,
name: name,
account: account,
inviter: inviter
)
end
describe '#perform with SAML enabled' do
let(:saml_settings) do
create(:account_saml_settings, account: account)
end
before { saml_settings }
context 'when user does not exist' do
it 'creates a new user with SAML provider' do
expect { builder.perform }.to change(User, :count).by(1)
user = User.from_email(email)
expect(user.provider).to eq('saml')
end
it 'creates user with correct attributes' do
user = builder.perform
expect(user.email).to eq(email)
expect(user.name).to eq(name)
expect(user.provider).to eq('saml')
expect(user.encrypted_password).to be_present
end
it 'adds user to the account with correct role' do
user = builder.perform
account_user = AccountUser.find_by(user: user, account: account)
expect(account_user).to be_present
expect(account_user.role).to eq('agent')
expect(account_user.inviter).to eq(inviter)
end
end
context 'when user already exists with email provider' do
let!(:existing_user) { create(:user, email: email, provider: 'email') }
it 'does not create a new user' do
expect { builder.perform }.not_to change(User, :count)
end
it 'converts existing user to SAML provider' do
expect(existing_user.provider).to eq('email')
builder.perform
expect(existing_user.reload.provider).to eq('saml')
end
it 'adds existing user to the account' do
user = builder.perform
account_user = AccountUser.find_by(user: user, account: account)
expect(account_user).to be_present
expect(account_user.inviter).to eq(inviter)
end
end
context 'when user already exists with SAML provider' do
let!(:existing_user) { create(:user, email: email, provider: 'saml') }
it 'does not change the provider' do
expect { builder.perform }.not_to(change { existing_user.reload.provider })
end
it 'still adds user to the account' do
user = builder.perform
account_user = AccountUser.find_by(user: user, account: account)
expect(account_user).to be_present
end
end
end
describe '#perform without SAML' do
context 'when user does not exist' do
it 'creates a new user with email provider (default behavior)' do
expect { builder.perform }.to change(User, :count).by(1)
user = User.from_email(email)
expect(user.provider).to eq('email')
end
end
context 'when user already exists' do
let!(:existing_user) { create(:user, email: email, provider: 'email') }
it 'does not change the existing user provider' do
expect { builder.perform }.not_to(change { existing_user.reload.provider })
end
end
end
describe '#perform with different account configurations' do
context 'when account has no SAML settings' do
# No saml_settings created for this account
it 'treats account as non-SAML enabled' do
user = builder.perform
expect(user.provider).to eq('email')
end
end
context 'when SAML settings are deleted after user creation' do
let(:saml_settings) do
create(:account_saml_settings, account: account)
end
let(:existing_user) { create(:user, email: email, provider: 'saml') }
before do
saml_settings
existing_user
end
it 'does not affect existing SAML users when adding to account' do
saml_settings.destroy!
user = builder.perform
expect(user.provider).to eq('saml') # Unchanged
end
end
end
end

View File

@@ -0,0 +1,150 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'Devise::Mailer' do
describe 'confirmation_instructions with Enterprise features' do
let(:account) { create(:account) }
let!(:confirmable_user) { create(:user, inviter: inviter_val, account: account) }
let(:inviter_val) { nil }
let(:mail) { Devise::Mailer.confirmation_instructions(confirmable_user.reload, nil, {}) }
before do
confirmable_user.update!(confirmed_at: nil)
confirmable_user.send(:generate_confirmation_token)
end
context 'with SAML enabled account' do
let(:saml_settings) { create(:account_saml_settings, account: account) }
before { saml_settings }
context 'when user has no inviter' do
it 'shows standard welcome message without SSO references' do
expect(mail.body).to match('We have a suite of powerful tools ready for you to explore.')
expect(mail.body).not_to match('via Single Sign-On')
end
it 'does not show activation instructions for SAML accounts' do
expect(mail.body).not_to match('Please take a moment and click the link below and activate your account')
end
it 'shows confirmation link' do
expect(mail.body).to include("app/auth/confirmation?confirmation_token=#{confirmable_user.confirmation_token}")
end
end
context 'when user has inviter and SAML is enabled' do
let(:inviter_val) { create(:user, :administrator, skip_confirmation: true, account: account) }
it 'mentions SSO invitation' do
expect(mail.body).to match(
"#{CGI.escapeHTML(inviter_val.name)}, with #{CGI.escapeHTML(account.name)}, has invited you to access.*via Single Sign-On \\(SSO\\)"
)
end
it 'explains SSO authentication' do
expect(mail.body).to match('Your organization uses SSO for secure authentication')
expect(mail.body).to match('You will not need a password to access your account')
end
it 'does not show standard invitation message' do
expect(mail.body).not_to match('has invited you to try out')
end
it 'directs to SSO portal instead of password reset' do
expect(mail.body).to match('You can access your account by logging in through your organization\'s SSO portal')
expect(mail.body).not_to include('app/auth/password/edit')
end
end
context 'when user is already confirmed and has inviter' do
let(:inviter_val) { create(:user, :administrator, skip_confirmation: true, account: account) }
before do
confirmable_user.confirm
end
it 'shows SSO login instructions' do
expect(mail.body).to match('You can now access your account by logging in through your organization\'s SSO portal')
expect(mail.body).not_to include('/auth/sign_in')
end
end
context 'when user updates email on SAML account' do
let(:inviter_val) { create(:user, :administrator, skip_confirmation: true, account: account) }
before do
confirmable_user.update!(email: 'updated@example.com')
end
it 'still shows confirmation link for email verification' do
expect(mail.body).to include('app/auth/confirmation?confirmation_token')
expect(confirmable_user.unconfirmed_email.blank?).to be false
end
end
context 'when user is already confirmed with no inviter' do
before do
confirmable_user.confirm
end
it 'shows SSO login instructions instead of regular login' do
expect(mail.body).to match('You can now access your account by logging in through your organization\'s SSO portal')
expect(mail.body).not_to include('/auth/sign_in')
end
end
end
context 'when account does not have SAML enabled' do
context 'when user has inviter' do
let(:inviter_val) { create(:user, :administrator, skip_confirmation: true, account: account) }
it 'shows standard invitation without SSO references' do
expect(mail.body).to match('has invited you to try out Chatwoot')
expect(mail.body).not_to match('via Single Sign-On')
expect(mail.body).not_to match('SSO portal')
end
it 'shows password reset link' do
expect(mail.body).to include('app/auth/password/edit')
end
end
context 'when user has no inviter' do
it 'shows standard welcome message and activation instructions' do
expect(mail.body).to match('We have a suite of powerful tools ready for you to explore')
expect(mail.body).to match('Please take a moment and click the link below and activate your account')
end
it 'shows confirmation link' do
expect(mail.body).to include("app/auth/confirmation?confirmation_token=#{confirmable_user.confirmation_token}")
end
end
context 'when user is already confirmed' do
let(:inviter_val) { create(:user, :administrator, skip_confirmation: true, account: account) }
before do
confirmable_user.confirm
end
it 'shows regular login link' do
expect(mail.body).to include('/auth/sign_in')
expect(mail.body).not_to match('SSO portal')
end
end
context 'when user updates email' do
before do
confirmable_user.update!(email: 'updated@example.com')
end
it 'shows confirmation link for email verification' do
expect(mail.body).to include('app/auth/confirmation?confirmation_token')
expect(confirmable_user.unconfirmed_email.blank?).to be false
end
end
end
end
end