feat: kickstart basic controller

This commit is contained in:
Shivam Mishra
2025-08-27 12:27:27 +05:30
parent 572df07365
commit f9d4816db1
7 changed files with 285 additions and 0 deletions
+3
View File
@@ -77,6 +77,9 @@ gem 'devise_token_auth', '>= 1.2.3'
# authorization
gem 'jwt'
gem 'pundit'
# SAML authentication
gem 'omniauth-saml', '~> 2.1'
gem 'omniauth-multi-provider'
# super admin
gem 'administrate', '>= 0.20.1'
gem 'administrate-field-active_storage', '>= 1.0.3'
+10
View File
@@ -595,12 +595,17 @@ GEM
oauth2 (~> 2.0)
omniauth (~> 2.0)
omniauth-oauth2 (~> 1.8)
omniauth-multi-provider (0.4.0)
omniauth
omniauth-oauth2 (1.8.0)
oauth2 (>= 1.4, < 3)
omniauth (~> 2.0)
omniauth-rails_csrf_protection (1.0.2)
actionpack (>= 4.2)
omniauth (~> 2.0)
omniauth-saml (2.2.4)
omniauth (~> 2.1)
ruby-saml (~> 1.18)
openssl (3.2.0)
orm_adapter (0.5.0)
os (1.1.4)
@@ -767,6 +772,9 @@ GEM
faraday (>= 1)
faraday-multipart (>= 1)
ruby-progressbar (1.13.0)
ruby-saml (1.18.1)
nokogiri (>= 1.13.10)
rexml
ruby-vips (2.1.4)
ffi (~> 1.12)
ruby2_keywords (0.0.5)
@@ -1034,8 +1042,10 @@ DEPENDENCIES
newrelic_rpm
omniauth (>= 2.1.2)
omniauth-google-oauth2 (>= 1.1.3)
omniauth-multi-provider
omniauth-oauth2
omniauth-rails_csrf_protection (~> 1.0, >= 1.0.2)
omniauth-saml (~> 2.1)
pg
pg_search
pgvector
+46
View File
@@ -0,0 +1,46 @@
class OmniauthController < ApplicationController
skip_before_action :verify_authenticity_token
skip_before_action :authenticate_user!
skip_before_action :set_current_user
skip_before_action :check_authorization
def request
# This will be handled by OmniAuth middleware
# The request will be redirected to the IdP
end
def callback
auth = request.env['omniauth.auth']
account_id = params[:account_id]
if auth.present?
render json: {
message: 'SAML authentication successful',
account_id: account_id,
provider: auth.provider,
uid: auth.uid,
info: {
email: auth.info.email,
name: auth.info.name,
first_name: auth.info.first_name,
last_name: auth.info.last_name
},
extra: {
raw_info: auth.extra.raw_info
}
}
else
render json: {
error: 'SAML authentication failed',
message: request.env['omniauth.error'] || 'Unknown error'
}, status: :unauthorized
end
end
def failure
render json: {
error: 'SAML authentication failed',
message: params[:message] || request.env['omniauth.error'] || 'Unknown error'
}, status: :unauthorized
end
end
+37
View File
@@ -0,0 +1,37 @@
Rails.application.config.middleware.use OmniAuth::Builder do
if defined?(OmniAuth::MultiProvider)
OmniAuth::MultiProvider.register(
self,
provider_name: :saml,
identity_provider_id_regex: /\d+/,
path_prefix: '/auth/saml'
) do |account_id, rack_env|
# Find the account's SAML settings
saml_settings = AccountSamlSettings.find_by(account_id: account_id, enabled: true)
if saml_settings
# Store the account in the rack environment for later use
rack_env['chatwoot.account_id'] = account_id
# Return the SAML provider options with correct option names
{
assertion_consumer_service_url: "#{ENV.fetch('FRONTEND_URL', 'http://localhost:3000')}/auth/saml/#{account_id}/callback",
sp_entity_id: saml_settings.sp_entity_id_or_default,
idp_sso_service_url: saml_settings.sso_url,
idp_cert_fingerprint: saml_settings.certificate_fingerprint,
idp_cert: saml_settings.certificate,
name_identifier_format: 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
attribute_statements: saml_settings.attribute_mappings || {
email: ['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'],
name: ['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name'],
first_name: ['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname'],
last_name: ['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname']
}
}
end
end
end
end
OmniAuth.config.allowed_request_methods = [:post, :get]
OmniAuth.config.silence_get_warning = true
+10
View File
@@ -8,6 +8,11 @@ Rails.application.routes.draw do
omniauth_callbacks: 'devise_overrides/omniauth_callbacks'
}, via: [:get, :post]
# OmniAuth SAML routes
match '/auth/saml/:account_id', to: 'omniauth#request', via: [:get, :post], as: :saml_auth
match '/auth/saml/:account_id/callback', to: 'omniauth#callback', via: [:get, :post], as: :saml_callback
match '/auth/failure', to: 'omniauth#failure', via: [:get, :post], as: :saml_failure
## renders the frontend paths only if its not an api only server
if ActiveModel::Type::Boolean.new.cast(ENV.fetch('CW_API_ONLY_SERVER', false))
root to: 'api#index'
@@ -70,6 +75,11 @@ Rails.application.routes.draw do
resources :documents, only: [:index, :show, :create, :destroy]
end
resource :saml_settings, only: [:show, :create, :update, :destroy]
namespace :saml do
get 'sso', to: 'callbacks#sso'
post 'callback', to: 'callbacks#create'
end
resources :agent_bots, only: [:index, :create, :show, :update, :destroy] do
delete :avatar, on: :member
post :reset_access_token, on: :member
@@ -0,0 +1,49 @@
class Api::V1::Accounts::Saml::CallbacksController < Api::V1::Accounts::BaseController
skip_before_action :authenticate_user!
def create
@saml_settings = AccountSamlSettings.find_by(account_id: @current_account.id, enabled: true)
return render_saml_not_enabled unless @saml_settings
render json: {
saml_settings: {
sp_entity_id: @saml_settings.sp_entity_id_or_default,
enabled: @saml_settings.enabled,
sso_url: @saml_settings.sso_url,
certificate_fingerprint: @saml_settings.certificate_fingerprint,
enforced_sso: @saml_settings.enforced_sso,
attribute_mappings: @saml_settings.attribute_mappings,
role_mappings: @saml_settings.role_mappings
},
received_data: {
saml_response: params[:SAMLResponse],
relay_state: params[:RelayState]
},
message: 'SAML callback received successfully'
}
end
def sso
@saml_settings = AccountSamlSettings.find_by(account_id: @current_account.id, enabled: true)
return render_saml_not_enabled unless @saml_settings
render json: {
sso_url: @saml_settings.sso_url,
sp_entity_id: @saml_settings.sp_entity_id_or_default,
acs_url: acs_url,
certificate_fingerprint: @saml_settings.certificate_fingerprint
}
end
private
def render_saml_not_enabled
render json: { error: 'SAML not enabled for this account' }, status: :not_found
end
def acs_url
api_v1_account_saml_callback_url(@current_account)
end
end
@@ -0,0 +1,130 @@
require 'rails_helper'
RSpec.describe Api::V1::Accounts::Saml::CallbacksController, type: :controller do
let(:account) { create(:account) }
let(:saml_settings) { create(:account_saml_settings, :enabled, account: account) }
describe 'POST #create' do
before do
allow(AccountSamlSettings).to receive(:find_by).and_return(saml_settings)
end
context 'when SAML response is received' do
let(:saml_response) { Base64.encode64('<SAMLResponse>dummy</SAMLResponse>') }
let(:relay_state) { "/app/accounts/#{account.id}/dashboard" }
it 'receives the SAML response' do
post :create, params: {
account_id: account.id,
SAMLResponse: saml_response,
RelayState: relay_state
}
expect(response).to have_http_status(:ok)
end
it 'loads the correct account SAML settings' do
expect(AccountSamlSettings).to receive(:find_by).with(account_id: account.id, enabled: true)
post :create, params: {
account_id: account.id,
SAMLResponse: saml_response,
RelayState: relay_state
}
end
it 'returns SAML settings in response' do
post :create, params: {
account_id: account.id,
SAMLResponse: saml_response,
RelayState: relay_state
}
json_response = JSON.parse(response.body)
expect(json_response['saml_settings']).to include(
'sp_entity_id' => saml_settings.sp_entity_id_or_default,
'enabled' => true,
'sso_url' => saml_settings.sso_url,
'certificate_fingerprint' => saml_settings.certificate_fingerprint,
'enforced_sso' => saml_settings.enforced_sso,
'attribute_mappings' => saml_settings.attribute_mappings,
'role_mappings' => saml_settings.role_mappings
)
end
it 'includes received SAML data in response' do
post :create, params: {
account_id: account.id,
SAMLResponse: saml_response,
RelayState: relay_state
}
json_response = JSON.parse(response.body)
expect(json_response['received_data']).to include(
'saml_response' => saml_response,
'relay_state' => relay_state
)
end
end
context 'when SAML is not enabled for account' do
before do
allow(AccountSamlSettings).to receive(:find_by).and_return(nil)
end
it 'returns not found error' do
post :create, params: {
account_id: account.id,
SAMLResponse: 'dummy'
}
expect(response).to have_http_status(:not_found)
json_response = JSON.parse(response.body)
expect(json_response['error']).to eq('SAML not enabled for this account')
end
end
context 'when account does not exist' do
it 'returns not found error' do
post :create, params: {
account_id: 0,
SAMLResponse: 'dummy'
}
expect(response).to have_http_status(:not_found)
end
end
end
describe 'GET #sso' do
before do
allow(AccountSamlSettings).to receive(:find_by).and_return(saml_settings)
end
context 'when initiating SSO' do
it 'returns SSO configuration' do
get :sso, params: { account_id: account.id }
expect(response).to have_http_status(:ok)
json_response = JSON.parse(response.body)
expect(json_response).to include(
'sso_url' => saml_settings.sso_url,
'sp_entity_id' => saml_settings.sp_entity_id_or_default,
'acs_url' => "http://test.host/api/v1/accounts/#{account.id}/saml/callback"
)
end
end
context 'when SAML is not enabled' do
before do
allow(AccountSamlSettings).to receive(:find_by).and_return(nil)
end
it 'returns not found error' do
get :sso, params: { account_id: account.id }
expect(response).to have_http_status(:not_found)
end
end
end
end