feat: SAML authentication controllers [CW-2958] (#12319)
This commit is contained in:
214
spec/enterprise/builders/saml_user_builder_spec.rb
Normal file
214
spec/enterprise/builders/saml_user_builder_spec.rb
Normal file
@@ -0,0 +1,214 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe SamlUserBuilder do
|
||||
let(:email) { 'saml.user@example.com' }
|
||||
let(:auth_hash) do
|
||||
{
|
||||
'provider' => 'saml',
|
||||
'uid' => 'saml-uid-123',
|
||||
'info' => {
|
||||
'email' => email,
|
||||
'name' => 'SAML User',
|
||||
'first_name' => 'SAML',
|
||||
'last_name' => 'User'
|
||||
},
|
||||
'extra' => {
|
||||
'raw_info' => {
|
||||
'groups' => %w[Administrators Users]
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
let(:account) { create(:account) }
|
||||
let(:builder) { described_class.new(auth_hash, account.id) }
|
||||
|
||||
describe '#perform' do
|
||||
context 'when user does not exist' do
|
||||
it 'creates a new user' do
|
||||
expect { builder.perform }.to change(User, :count).by(1)
|
||||
end
|
||||
|
||||
it 'creates user with correct attributes' do
|
||||
user = builder.perform
|
||||
|
||||
expect(user.email).to eq(email)
|
||||
expect(user.name).to eq('SAML User')
|
||||
expect(user.display_name).to eq('SAML')
|
||||
expect(user.provider).to eq('saml')
|
||||
expect(user.uid).to eq(email) # User model sets uid to email in before_validation callback
|
||||
expect(user.confirmed_at).to be_present
|
||||
end
|
||||
|
||||
it 'creates user with a random password' do
|
||||
user = builder.perform
|
||||
expect(user.encrypted_password).to be_present
|
||||
end
|
||||
|
||||
it 'adds user to the account' do
|
||||
user = builder.perform
|
||||
expect(user.accounts).to include(account)
|
||||
end
|
||||
|
||||
it 'sets default role as agent' do
|
||||
user = builder.perform
|
||||
account_user = AccountUser.find_by(user: user, account: account)
|
||||
expect(account_user.role).to eq('agent')
|
||||
end
|
||||
|
||||
context 'when name is not provided' do
|
||||
let(:auth_hash) do
|
||||
{
|
||||
'provider' => 'saml',
|
||||
'uid' => 'saml-uid-123',
|
||||
'info' => {
|
||||
'email' => email
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
it 'derives name from email' do
|
||||
user = builder.perform
|
||||
expect(user.name).to eq('saml.user')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user already exists' do
|
||||
let!(:existing_user) { create(:user, email: email) }
|
||||
|
||||
it 'does not create a new user' do
|
||||
expect { builder.perform }.not_to change(User, :count)
|
||||
end
|
||||
|
||||
it 'returns the existing user' do
|
||||
user = builder.perform
|
||||
expect(user).to eq(existing_user)
|
||||
end
|
||||
|
||||
it 'adds existing user to the account if not already added' do
|
||||
user = builder.perform
|
||||
expect(user.accounts).to include(account)
|
||||
end
|
||||
|
||||
it 'converts existing user to SAML' do
|
||||
expect(existing_user.provider).not_to eq('saml')
|
||||
|
||||
builder.perform
|
||||
|
||||
expect(existing_user.reload.provider).to eq('saml')
|
||||
end
|
||||
|
||||
it 'does not change provider if user is already SAML' do
|
||||
existing_user.update!(provider: 'saml')
|
||||
|
||||
expect { builder.perform }.not_to(change { existing_user.reload.provider })
|
||||
end
|
||||
|
||||
it 'does not duplicate account association' do
|
||||
existing_user.account_users.create!(account: account, role: 'agent')
|
||||
|
||||
expect { builder.perform }.not_to change(AccountUser, :count)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with role mappings' do
|
||||
let(:saml_settings) do
|
||||
create(:account_saml_settings,
|
||||
account: account,
|
||||
role_mappings: {
|
||||
'Administrators' => { 'role' => 'administrator' },
|
||||
'Agents' => { 'role' => 'agent' }
|
||||
})
|
||||
end
|
||||
|
||||
before { saml_settings }
|
||||
|
||||
it 'applies administrator role based on SAML groups' do
|
||||
user = builder.perform
|
||||
account_user = AccountUser.find_by(user: user, account: account)
|
||||
expect(account_user.role).to eq('administrator')
|
||||
end
|
||||
|
||||
context 'with custom role mapping' do
|
||||
let!(:custom_role) { create(:custom_role, account: account) }
|
||||
let(:saml_settings) do
|
||||
create(:account_saml_settings,
|
||||
account: account,
|
||||
role_mappings: {
|
||||
'Administrators' => { 'custom_role_id' => custom_role.id }
|
||||
})
|
||||
end
|
||||
|
||||
before { saml_settings }
|
||||
|
||||
it 'applies custom role based on SAML groups' do
|
||||
user = builder.perform
|
||||
account_user = AccountUser.find_by(user: user, account: account)
|
||||
expect(account_user.custom_role_id).to eq(custom_role.id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user is not in any mapped groups' do
|
||||
let(:auth_hash) do
|
||||
{
|
||||
'provider' => 'saml',
|
||||
'uid' => 'saml-uid-123',
|
||||
'info' => {
|
||||
'email' => email,
|
||||
'name' => 'SAML User'
|
||||
},
|
||||
'extra' => {
|
||||
'raw_info' => {
|
||||
'groups' => ['UnmappedGroup']
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
it 'keeps default agent role' do
|
||||
user = builder.perform
|
||||
account_user = AccountUser.find_by(user: user, account: account)
|
||||
expect(account_user.role).to eq('agent')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with different group attribute names' do
|
||||
let(:auth_hash) do
|
||||
{
|
||||
'provider' => 'saml',
|
||||
'uid' => 'saml-uid-123',
|
||||
'info' => {
|
||||
'email' => email,
|
||||
'name' => 'SAML User'
|
||||
},
|
||||
'extra' => {
|
||||
'raw_info' => {
|
||||
'memberOf' => ['CN=Administrators,OU=Groups,DC=example,DC=com']
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
it 'reads groups from memberOf attribute' do
|
||||
builder_instance = described_class.new(auth_hash, account_id: account.id)
|
||||
allow(builder_instance).to receive(:saml_groups).and_return(['CN=Administrators,OU=Groups,DC=example,DC=com'])
|
||||
user = builder_instance.perform
|
||||
expect(user).to be_persisted
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there are errors' do
|
||||
it 'returns unsaved user object when user creation fails' do
|
||||
allow(User).to receive(:create).and_return(User.new(email: email))
|
||||
user = builder.perform
|
||||
expect(user.persisted?).to be false
|
||||
end
|
||||
|
||||
it 'does not create account association for failed user' do
|
||||
allow(User).to receive(:create).and_return(User.new(email: email))
|
||||
expect { builder.perform }.not_to change(AccountUser, :count)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,61 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Enterprise SAML OmniAuth Callbacks', type: :request do
|
||||
let!(:account) { create(:account) }
|
||||
let(:saml_settings) { create(:account_saml_settings, account: account) }
|
||||
|
||||
def set_saml_config(email = 'test@example.com')
|
||||
OmniAuth.config.test_mode = true
|
||||
OmniAuth.config.mock_auth[:saml] = OmniAuth::AuthHash.new(
|
||||
provider: 'saml',
|
||||
uid: '123545',
|
||||
info: {
|
||||
name: 'Test User',
|
||||
email: email
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
before do
|
||||
allow(ChatwootApp).to receive(:enterprise?).and_return(true)
|
||||
account.enable_features!('saml')
|
||||
saml_settings
|
||||
end
|
||||
|
||||
describe '#saml callback' do
|
||||
it 'creates new user and logs them in' do
|
||||
with_modified_env FRONTEND_URL: 'http://www.example.com' do
|
||||
set_saml_config('new_user@example.com')
|
||||
|
||||
get "/omniauth/saml/callback?account_id=#{account.id}"
|
||||
|
||||
# expect a 302 redirect to auth/saml/callback
|
||||
expect(response).to redirect_to('http://www.example.com/auth/saml/callback')
|
||||
follow_redirect!
|
||||
|
||||
# expect redirect to login with SSO token
|
||||
expect(response).to redirect_to(%r{/app/login\?email=.+&sso_auth_token=.+$})
|
||||
|
||||
# verify user was created
|
||||
user = User.from_email('new_user@example.com')
|
||||
expect(user).to be_present
|
||||
expect(user.provider).to eq('saml')
|
||||
end
|
||||
end
|
||||
|
||||
it 'logs in existing user' do
|
||||
with_modified_env FRONTEND_URL: 'http://www.example.com' do
|
||||
create(:user, email: 'existing@example.com', account: account)
|
||||
set_saml_config('existing@example.com')
|
||||
|
||||
get "/omniauth/saml/callback?account_id=#{account.id}"
|
||||
|
||||
# expect a 302 redirect to auth/saml/callback
|
||||
expect(response).to redirect_to('http://www.example.com/auth/saml/callback')
|
||||
follow_redirect!
|
||||
|
||||
expect(response).to redirect_to(%r{/app/login\?email=.+&sso_auth_token=.+$})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,36 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Enterprise Passwords Controller', type: :request do
|
||||
let!(:account) { create(:account) }
|
||||
|
||||
describe 'POST /auth/password' do
|
||||
context 'with SAML user email' do
|
||||
let!(:saml_user) { create(:user, email: 'saml@example.com', provider: 'saml', account: account) }
|
||||
|
||||
it 'prevents password reset and returns forbidden with custom error message' do
|
||||
params = { email: saml_user.email, redirect_url: 'http://test.host' }
|
||||
|
||||
post user_password_path, params: params, as: :json
|
||||
|
||||
expect(response).to have_http_status(:forbidden)
|
||||
json_response = JSON.parse(response.body)
|
||||
expect(json_response['success']).to be(false)
|
||||
expect(json_response['errors']).to include(I18n.t('messages.reset_password_saml_user'))
|
||||
end
|
||||
end
|
||||
|
||||
context 'with non-SAML user email' do
|
||||
let!(:regular_user) { create(:user, email: 'regular@example.com', provider: 'email', account: account) }
|
||||
|
||||
it 'allows password reset for non-SAML users' do
|
||||
params = { email: regular_user.email, redirect_url: 'http://test.host' }
|
||||
|
||||
post user_password_path, params: params, as: :json
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
json_response = JSON.parse(response.body)
|
||||
expect(json_response['message']).to be_present
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -5,32 +5,75 @@ RSpec.describe 'Enterprise Audit API', type: :request do
|
||||
let!(:user) { create(:user, password: 'Password1!', account: account) }
|
||||
|
||||
describe 'POST /sign_in' do
|
||||
it 'creates a sign_in audit event wwith valid credentials' do
|
||||
params = { email: user.email, password: 'Password1!' }
|
||||
context 'with SAML user attempting password login' do
|
||||
let(:saml_settings) { create(:account_saml_settings, account: account) }
|
||||
let(:saml_user) { create(:user, email: 'saml@example.com', provider: 'saml', account: account) }
|
||||
|
||||
expect do
|
||||
post new_user_session_url,
|
||||
params: params,
|
||||
as: :json
|
||||
end.to change(Enterprise::AuditLog, :count).by(1)
|
||||
before do
|
||||
saml_settings
|
||||
saml_user
|
||||
end
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.body).to include(user.email)
|
||||
it 'prevents login and returns SAML authentication error' do
|
||||
params = { email: saml_user.email, password: 'Password1!' }
|
||||
|
||||
# Check if the sign_in event is created
|
||||
user.reload
|
||||
expect(user.audits.last.action).to eq('sign_in')
|
||||
expect(user.audits.last.associated_id).to eq(account.id)
|
||||
expect(user.audits.last.associated_type).to eq('Account')
|
||||
post new_user_session_url, params: params, as: :json
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
json_response = JSON.parse(response.body)
|
||||
expect(json_response['success']).to be(false)
|
||||
expect(json_response['errors']).to include(I18n.t('messages.login_saml_user'))
|
||||
end
|
||||
|
||||
it 'allows login with valid SSO token' do
|
||||
valid_token = saml_user.generate_sso_auth_token
|
||||
params = { email: saml_user.email, sso_auth_token: valid_token, password: 'Password1!' }
|
||||
|
||||
expect do
|
||||
post new_user_session_url, params: params, as: :json
|
||||
end.to change(Enterprise::AuditLog, :count).by(1)
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.body).to include(saml_user.email)
|
||||
end
|
||||
end
|
||||
|
||||
it 'will not create a sign_in audit event with invalid credentials' do
|
||||
params = { email: user.email, password: 'invalid' }
|
||||
expect do
|
||||
post new_user_session_url,
|
||||
params: params,
|
||||
as: :json
|
||||
end.not_to change(Enterprise::AuditLog, :count)
|
||||
context 'with regular user credentials' do
|
||||
it 'creates a sign_in audit event wwith valid credentials' do
|
||||
params = { email: user.email, password: 'Password1!' }
|
||||
|
||||
expect do
|
||||
post new_user_session_url,
|
||||
params: params,
|
||||
as: :json
|
||||
end.to change(Enterprise::AuditLog, :count).by(1)
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.body).to include(user.email)
|
||||
|
||||
# Check if the sign_in event is created
|
||||
user.reload
|
||||
expect(user.audits.last.action).to eq('sign_in')
|
||||
expect(user.audits.last.associated_id).to eq(account.id)
|
||||
expect(user.audits.last.associated_type).to eq('Account')
|
||||
end
|
||||
|
||||
it 'will not create a sign_in audit event with invalid credentials' do
|
||||
params = { email: user.email, password: 'invalid' }
|
||||
expect do
|
||||
post new_user_session_url,
|
||||
params: params,
|
||||
as: :json
|
||||
end.not_to change(Enterprise::AuditLog, :count)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with blank email' do
|
||||
it 'skips SAML check and processes normally' do
|
||||
params = { email: '', password: 'Password1!' }
|
||||
post new_user_session_url, params: params, as: :json
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
Reference in New Issue
Block a user