feat: Control the allowed login methods via Super Admin (#12892)
- Control the allowed authentication methods for a chatwoot installation via super admin configs. [SAML, Google Auth etc] ------ [Codex Task](https://chatgpt.com/codex/tasks/task_e_6917d503b6e48326a261672c1de91462) --------- Co-authored-by: Pranav <pranav@chatwoot.com> Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
This commit is contained in:
@@ -78,10 +78,18 @@ class DashboardController < ActionController::Base
|
||||
WHATSAPP_CONFIGURATION_ID: GlobalConfigService.load('WHATSAPP_CONFIGURATION_ID', ''),
|
||||
IS_ENTERPRISE: ChatwootApp.enterprise?,
|
||||
AZURE_APP_ID: GlobalConfigService.load('AZURE_APP_ID', ''),
|
||||
GIT_SHA: GIT_HASH
|
||||
GIT_SHA: GIT_HASH,
|
||||
ALLOWED_LOGIN_METHODS: allowed_login_methods
|
||||
}
|
||||
end
|
||||
|
||||
def allowed_login_methods
|
||||
methods = ['email']
|
||||
methods << 'google_oauth' if GlobalConfigService.load('ENABLE_GOOGLE_OAUTH_LOGIN', 'true').to_s != 'false'
|
||||
methods << 'saml' if ChatwootHub.pricing_plan != 'community' && GlobalConfigService.load('ENABLE_SAML_SSO_LOGIN', 'true').to_s != 'false'
|
||||
methods
|
||||
end
|
||||
|
||||
def set_application_pack
|
||||
@application_pack = if request.path.include?('/auth') || request.path.include?('/login')
|
||||
'v3app'
|
||||
|
||||
@@ -15,14 +15,20 @@ class SuperAdmin::AppConfigsController < SuperAdmin::ApplicationController
|
||||
end
|
||||
|
||||
def create
|
||||
errors = []
|
||||
params['app_config'].each do |key, value|
|
||||
next unless @allowed_configs.include?(key)
|
||||
|
||||
i = InstallationConfig.where(name: key).first_or_create(value: value, locked: false)
|
||||
i.value = value
|
||||
i.save!
|
||||
errors.concat(i.errors.full_messages) unless i.save
|
||||
end
|
||||
|
||||
if errors.any?
|
||||
redirect_to super_admin_app_config_path(config: @config), alert: errors.join(', ')
|
||||
else
|
||||
redirect_to super_admin_settings_path, notice: "App Configs - #{@config.titleize} updated successfully"
|
||||
end
|
||||
redirect_to super_admin_settings_path, notice: "App Configs - #{@config.titleize} updated successfully"
|
||||
end
|
||||
|
||||
private
|
||||
@@ -42,7 +48,7 @@ class SuperAdmin::AppConfigsController < SuperAdmin::ApplicationController
|
||||
'instagram' => %w[INSTAGRAM_APP_ID INSTAGRAM_APP_SECRET INSTAGRAM_VERIFY_TOKEN INSTAGRAM_API_VERSION ENABLE_INSTAGRAM_CHANNEL_HUMAN_AGENT],
|
||||
'whatsapp_embedded' => %w[WHATSAPP_APP_ID WHATSAPP_APP_SECRET WHATSAPP_CONFIGURATION_ID WHATSAPP_API_VERSION],
|
||||
'notion' => %w[NOTION_CLIENT_ID NOTION_CLIENT_SECRET],
|
||||
'google' => %w[GOOGLE_OAUTH_CLIENT_ID GOOGLE_OAUTH_CLIENT_SECRET GOOGLE_OAUTH_REDIRECT_URI]
|
||||
'google' => %w[GOOGLE_OAUTH_CLIENT_ID GOOGLE_OAUTH_CLIENT_SECRET GOOGLE_OAUTH_REDIRECT_URI ENABLE_GOOGLE_OAUTH_LOGIN]
|
||||
}
|
||||
|
||||
@allowed_configs = mapping.fetch(
|
||||
|
||||
@@ -9,6 +9,13 @@ captain:
|
||||
icon: 'icon-captain'
|
||||
config_key: 'captain'
|
||||
enterprise: true
|
||||
saml:
|
||||
name: 'SAML SSO'
|
||||
description: 'Configuration for controlling SAML Single Sign-On availability'
|
||||
enabled: <%= ChatwootApp.enterprise? %>
|
||||
icon: 'icon-lock-line'
|
||||
config_key: 'saml'
|
||||
enterprise: true
|
||||
custom_branding:
|
||||
name: 'Custom Branding'
|
||||
description: 'Apply your own branding to this installation.'
|
||||
|
||||
@@ -411,6 +411,7 @@
|
||||
"TITLE": "Security",
|
||||
"DESCRIPTION": "Manage your account security settings.",
|
||||
"LINK_TEXT": "Learn more about SAML SSO",
|
||||
"SAML_DISABLED_MESSAGE": "SAML SSO is currently disabled. Please contact your administrator to enable this feature.",
|
||||
"SAML": {
|
||||
"TITLE": "SAML SSO",
|
||||
"NOTE": "Configure SAML single sign-on for your account. Users will authenticate through your identity provider instead of using email/password.",
|
||||
|
||||
@@ -10,13 +10,23 @@ import { INSTALLATION_TYPES } from 'dashboard/constants/installationTypes';
|
||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||
const { shouldShow, shouldShowPaywall } = usePolicy();
|
||||
|
||||
const shouldShowSaml = computed(() =>
|
||||
shouldShow(
|
||||
const allowedLoginMethods = computed(
|
||||
() => window.chatwootConfig.allowedLoginMethods || ['email']
|
||||
);
|
||||
|
||||
const isSamlSsoEnabled = computed(() =>
|
||||
allowedLoginMethods.value.includes('saml')
|
||||
);
|
||||
|
||||
const shouldShowSaml = computed(() => {
|
||||
const hasPermission = shouldShow(
|
||||
FEATURE_FLAGS.SAML,
|
||||
['administrator'],
|
||||
[INSTALLATION_TYPES.CLOUD, INSTALLATION_TYPES.ENTERPRISE]
|
||||
)
|
||||
);
|
||||
);
|
||||
return hasPermission && isSamlSsoEnabled.value;
|
||||
});
|
||||
|
||||
const showPaywall = computed(() => shouldShowPaywall('saml'));
|
||||
</script>
|
||||
|
||||
@@ -36,6 +46,9 @@ const showPaywall = computed(() => shouldShowPaywall('saml'));
|
||||
<template #body>
|
||||
<SamlPaywall v-if="showPaywall" />
|
||||
<SamlSettings v-else-if="shouldShowSaml" />
|
||||
<div v-else class="mt-6 text-sm text-slate-600">
|
||||
{{ $t('SECURITY_SETTINGS.SAML_DISABLED_MESSAGE') }}
|
||||
</div>
|
||||
</template>
|
||||
</SettingsLayout>
|
||||
</template>
|
||||
|
||||
@@ -99,8 +99,14 @@ export default {
|
||||
}
|
||||
return '';
|
||||
},
|
||||
allowedLoginMethods() {
|
||||
return window.chatwootConfig.allowedLoginMethods || ['email'];
|
||||
},
|
||||
showGoogleOAuth() {
|
||||
return Boolean(window.chatwootConfig.googleOAuthClientId);
|
||||
return (
|
||||
this.allowedLoginMethods.includes('google_oauth') &&
|
||||
Boolean(window.chatwootConfig.googleOAuthClientId)
|
||||
);
|
||||
},
|
||||
isFormValid() {
|
||||
return !this.v$.$invalid && this.hasAValidCaptcha;
|
||||
@@ -296,7 +302,7 @@ export default {
|
||||
</form>
|
||||
<div class="flex flex-col">
|
||||
<SimpleDivider
|
||||
v-if="showGoogleOAuth || showSamlLogin"
|
||||
v-if="showGoogleOAuth"
|
||||
:label="$t('COMMON.OR')"
|
||||
bg="bg-n-background"
|
||||
class="uppercase"
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// utils and composables
|
||||
import { login } from '../../api/auth';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { parseBoolean } from '@chatwoot/utils';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { required, email } from '@vuelidate/validators';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
@@ -85,14 +84,20 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({ globalConfig: 'globalConfig/get' }),
|
||||
allowedLoginMethods() {
|
||||
return window.chatwootConfig.allowedLoginMethods || ['email'];
|
||||
},
|
||||
showGoogleOAuth() {
|
||||
return Boolean(window.chatwootConfig.googleOAuthClientId);
|
||||
return (
|
||||
this.allowedLoginMethods.includes('google_oauth') &&
|
||||
Boolean(window.chatwootConfig.googleOAuthClientId)
|
||||
);
|
||||
},
|
||||
showSignupLink() {
|
||||
return parseBoolean(window.chatwootConfig.signupEnabled);
|
||||
return window.chatwootConfig.signupEnabled === 'true';
|
||||
},
|
||||
showSamlLogin() {
|
||||
return this.globalConfig.isEnterprise;
|
||||
return this.allowedLoginMethods.includes('saml');
|
||||
},
|
||||
},
|
||||
created() {
|
||||
|
||||
@@ -23,6 +23,7 @@ class InstallationConfig < ApplicationRecord
|
||||
|
||||
before_validation :set_lock
|
||||
validates :name, presence: true
|
||||
validate :saml_sso_users_check, if: -> { name == 'ENABLE_SAML_SSO_LOGIN' }
|
||||
|
||||
# TODO: Get rid of default scope
|
||||
# https://stackoverflow.com/a/1834250/939299
|
||||
@@ -54,4 +55,11 @@ class InstallationConfig < ApplicationRecord
|
||||
def clear_cache
|
||||
GlobalConfig.clear_cache
|
||||
end
|
||||
|
||||
def saml_sso_users_check
|
||||
return unless value == false || value == 'false'
|
||||
return unless User.exists?(provider: 'saml')
|
||||
|
||||
errors.add(:base, 'Cannot disable SAML SSO login while users are using SAML authentication')
|
||||
end
|
||||
end
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
instagramAppId: '<%= @global_config['INSTAGRAM_APP_ID'] %>',
|
||||
googleOAuthClientId: '<%= ENV.fetch('GOOGLE_OAUTH_CLIENT_ID', nil) %>',
|
||||
googleOAuthCallbackUrl: '<%= ENV.fetch('GOOGLE_OAUTH_CALLBACK_URL', nil) %>',
|
||||
allowedLoginMethods: <%= @global_config['ALLOWED_LOGIN_METHODS'].to_json.html_safe %>,
|
||||
fbApiVersion: '<%= @global_config['FACEBOOK_API_VERSION'] %>',
|
||||
whatsappAppId: '<%= @global_config['WHATSAPP_APP_ID'] %>',
|
||||
whatsappConfigurationId: '<%= @global_config['WHATSAPP_CONFIGURATION_ID'] %>',
|
||||
|
||||
@@ -438,8 +438,23 @@
|
||||
value:
|
||||
locked: false
|
||||
description: 'The redirect URI configured in your Google OAuth app'
|
||||
- name: ENABLE_GOOGLE_OAUTH_LOGIN
|
||||
display_title: 'Enable Google OAuth login'
|
||||
value: true
|
||||
locked: false
|
||||
description: 'Show Google OAuth as a login option when credentials are configured'
|
||||
type: boolean
|
||||
## ------ End of Configs added for Google OAuth ------ ##
|
||||
|
||||
## ------ Configs added for SAML SSO ------ ##
|
||||
- name: ENABLE_SAML_SSO_LOGIN
|
||||
display_title: 'Enable SAML SSO login'
|
||||
value: true
|
||||
locked: false
|
||||
description: 'Show SAML SSO as a login option. Cannot be disabled if any users are using SAML authentication.'
|
||||
type: boolean
|
||||
## ------ End of Configs added for SAML SSO ------ ##
|
||||
|
||||
## ------ Configs added for Cloudflare ------ ##
|
||||
- name: CLOUDFLARE_API_KEY
|
||||
display_title: 'Cloudflare API Key'
|
||||
|
||||
@@ -64,6 +64,7 @@ en:
|
||||
not_found: Assignment policy not found
|
||||
saml:
|
||||
feature_not_enabled: SAML feature not enabled for this account
|
||||
sso_not_enabled: SAML SSO is not enabled for this installation
|
||||
data_import:
|
||||
data_type:
|
||||
invalid: Invalid data type
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
class Api::V1::Accounts::SamlSettingsController < Api::V1::Accounts::BaseController
|
||||
before_action :check_saml_sso_enabled
|
||||
before_action :check_saml_feature_enabled
|
||||
before_action :check_authorization
|
||||
before_action :set_saml_settings
|
||||
@@ -53,4 +54,10 @@ class Api::V1::Accounts::SamlSettingsController < Api::V1::Accounts::BaseControl
|
||||
|
||||
render json: { error: I18n.t('errors.saml.feature_not_enabled') }, status: :forbidden
|
||||
end
|
||||
|
||||
def check_saml_sso_enabled
|
||||
return if GlobalConfigService.load('ENABLE_SAML_SSO_LOGIN', 'true').to_s == 'true'
|
||||
|
||||
render json: { error: I18n.t('errors.saml.sso_not_enabled') }, status: :forbidden
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,6 +3,11 @@ class Api::V1::AuthController < Api::BaseController
|
||||
before_action :find_user_and_account, only: [:saml_login]
|
||||
|
||||
def saml_login
|
||||
unless saml_sso_enabled?
|
||||
render json: { error: 'SAML SSO login is not enabled' }, status: :forbidden
|
||||
return
|
||||
end
|
||||
|
||||
return if @account.nil?
|
||||
|
||||
relay_state = params[:target] || 'web'
|
||||
@@ -69,4 +74,8 @@ class Api::V1::AuthController < Api::BaseController
|
||||
|
||||
"#{frontend_url}/app/login/sso#{query_fragment}"
|
||||
end
|
||||
|
||||
def saml_sso_enabled?
|
||||
GlobalConfigService.load('ENABLE_SAML_SSO_LOGIN', 'true').to_s == 'true'
|
||||
end
|
||||
end
|
||||
|
||||
@@ -11,6 +11,8 @@ module Enterprise::SuperAdmin::AppConfigsController
|
||||
@allowed_configs = internal_config_options
|
||||
when 'captain'
|
||||
@allowed_configs = captain_config_options
|
||||
when 'saml'
|
||||
@allowed_configs = saml_config_options
|
||||
else
|
||||
super
|
||||
end
|
||||
@@ -46,4 +48,8 @@ module Enterprise::SuperAdmin::AppConfigsController
|
||||
CAPTAIN_FIRECRAWL_API_KEY
|
||||
]
|
||||
end
|
||||
|
||||
def saml_config_options
|
||||
%w[ENABLE_SAML_SSO_LOGIN]
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user