feat: kickstart basic controller
This commit is contained in:
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user