From 33058b5f3f8bc21554426d49d751a7ec36d688f5 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Thu, 4 Sep 2025 02:00:42 +0530 Subject: [PATCH] feat: add saml model & controller [CW-2958] (#12289) This PR adds the foundation for account-level SAML SSO configuration in Chatwoot Enterprise. It introduces a new `AccountSamlSettings` model and management API that allows accounts to configure their own SAML identity providers independently, this also includes the certificate generation flow The implementation includes a new controller (`Api::V1::Accounts::SamlSettingsController`) that provides CRUD operations for SAML configuration The feature is properly gated behind the 'saml' feature flag and includes administrator-only authorization via Pundit policies. --- config/features.yml | 4 + config/locales/en.yml | 2 + config/routes.rb | 1 + ...0825070005_create_account_saml_settings.rb | 14 + db/schema.rb | 14 +- .../v1/accounts/saml_settings_controller.rb | 48 ++++ .../app/models/account_saml_settings.rb | 59 ++++ enterprise/app/models/enterprise/account.rb | 4 + .../app/models/enterprise/concerns/account.rb | 2 + .../policies/account_saml_settings_policy.rb | 17 ++ .../saml_settings/create.json.jbuilder | 1 + .../accounts/saml_settings/show.json.jbuilder | 1 + .../saml_settings/update.json.jbuilder | 1 + .../_account_saml_settings.json.jbuilder | 10 + .../accounts/saml_settings_controller_spec.rb | 265 ++++++++++++++++++ .../models/account_saml_settings_spec.rb | 117 ++++++++ spec/factories/account_saml_settings.rb | 31 ++ 17 files changed, 590 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20250825070005_create_account_saml_settings.rb create mode 100644 enterprise/app/controllers/api/v1/accounts/saml_settings_controller.rb create mode 100644 enterprise/app/models/account_saml_settings.rb create mode 100644 enterprise/app/policies/account_saml_settings_policy.rb create mode 100644 enterprise/app/views/api/v1/accounts/saml_settings/create.json.jbuilder create mode 100644 enterprise/app/views/api/v1/accounts/saml_settings/show.json.jbuilder create mode 100644 enterprise/app/views/api/v1/accounts/saml_settings/update.json.jbuilder create mode 100644 enterprise/app/views/api/v1/models/_account_saml_settings.json.jbuilder create mode 100644 spec/enterprise/controllers/api/v1/accounts/saml_settings_controller_spec.rb create mode 100644 spec/enterprise/models/account_saml_settings_spec.rb create mode 100644 spec/factories/account_saml_settings.rb diff --git a/config/features.yml b/config/features.yml index c8ed2e189..9b2fc33c5 100644 --- a/config/features.yml +++ b/config/features.yml @@ -204,3 +204,7 @@ enabled: false premium: true chatwoot_internal: true +- name: saml + display_name: SAML + enabled: false + premium: true diff --git a/config/locales/en.yml b/config/locales/en.yml index cb72bb699..c9614d3a6 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -55,6 +55,8 @@ en: failed: Signup failed assignment_policy: not_found: Assignment policy not found + saml: + feature_not_enabled: SAML feature not enabled for this account data_import: data_type: invalid: Invalid data type diff --git a/config/routes.rb b/config/routes.rb index 67ded50cb..139476706 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -69,6 +69,7 @@ Rails.application.routes.draw do end resources :documents, only: [:index, :show, :create, :destroy] end + resource :saml_settings, only: [:show, :create, :update, :destroy] resources :agent_bots, only: [:index, :create, :show, :update, :destroy] do delete :avatar, on: :member post :reset_access_token, on: :member diff --git a/db/migrate/20250825070005_create_account_saml_settings.rb b/db/migrate/20250825070005_create_account_saml_settings.rb new file mode 100644 index 000000000..7a0937fbf --- /dev/null +++ b/db/migrate/20250825070005_create_account_saml_settings.rb @@ -0,0 +1,14 @@ +class CreateAccountSamlSettings < ActiveRecord::Migration[7.1] + def change + create_table :account_saml_settings do |t| + t.references :account, null: false + t.string :sso_url + t.text :certificate + t.string :sp_entity_id + t.string :idp_entity_id + t.json :role_mappings, default: {} + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index f48ff6707..e320cf2b6 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2025_08_22_061042) do +ActiveRecord::Schema[7.1].define(version: 2025_08_25_070005) do # These extensions should be enabled to support this database enable_extension "pg_stat_statements" enable_extension "pg_trgm" @@ -28,6 +28,18 @@ ActiveRecord::Schema[7.1].define(version: 2025_08_22_061042) do t.index ["token"], name: "index_access_tokens_on_token", unique: true end + create_table "account_saml_settings", force: :cascade do |t| + t.bigint "account_id", null: false + t.string "sso_url" + t.text "certificate" + t.string "sp_entity_id" + t.string "idp_entity_id" + t.json "role_mappings", default: {} + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["account_id"], name: "index_account_saml_settings_on_account_id" + end + create_table "account_users", force: :cascade do |t| t.bigint "account_id" t.bigint "user_id" diff --git a/enterprise/app/controllers/api/v1/accounts/saml_settings_controller.rb b/enterprise/app/controllers/api/v1/accounts/saml_settings_controller.rb new file mode 100644 index 000000000..efeb61a2f --- /dev/null +++ b/enterprise/app/controllers/api/v1/accounts/saml_settings_controller.rb @@ -0,0 +1,48 @@ +class Api::V1::Accounts::SamlSettingsController < Api::V1::Accounts::BaseController + before_action :check_saml_feature_enabled + before_action :check_authorization + before_action :set_saml_settings + + def show; end + + def create + @saml_settings = Current.account.build_saml_settings(saml_settings_params) + @saml_settings.save! + end + + def update + @saml_settings.update!(saml_settings_params) + end + + def destroy + @saml_settings.destroy! + head :no_content + end + + private + + def set_saml_settings + @saml_settings = Current.account.saml_settings || + Current.account.build_saml_settings + end + + def saml_settings_params + params.require(:saml_settings).permit( + :sso_url, + :certificate, + :idp_entity_id, + :sp_entity_id, + role_mappings: {} + ) + end + + def check_authorization + authorize(AccountSamlSettings) + end + + def check_saml_feature_enabled + return if Current.account.feature_enabled?('saml') + + render json: { error: I18n.t('errors.saml.feature_not_enabled') }, status: :forbidden + end +end diff --git a/enterprise/app/models/account_saml_settings.rb b/enterprise/app/models/account_saml_settings.rb new file mode 100644 index 000000000..25e8aeffe --- /dev/null +++ b/enterprise/app/models/account_saml_settings.rb @@ -0,0 +1,59 @@ +# == Schema Information +# +# Table name: account_saml_settings +# +# id :bigint not null, primary key +# certificate :text +# role_mappings :json +# sso_url :string +# created_at :datetime not null +# updated_at :datetime not null +# account_id :bigint not null +# idp_entity_id :string +# sp_entity_id :string +# +# Indexes +# +# index_account_saml_settings_on_account_id (account_id) +# +class AccountSamlSettings < ApplicationRecord + belongs_to :account + + validates :account_id, presence: true + validates :sso_url, presence: true + validates :certificate, presence: true + validates :idp_entity_id, presence: true + + before_validation :set_sp_entity_id, if: :sp_entity_id_needs_generation? + + def saml_enabled? + sso_url.present? && certificate.present? + end + + def certificate_fingerprint + return nil if certificate.blank? + + begin + cert = OpenSSL::X509::Certificate.new(certificate) + OpenSSL::Digest::SHA1.new(cert.to_der).hexdigest + .upcase.gsub(/(.{2})(?=.)/, '\1:') + rescue OpenSSL::X509::CertificateError + nil + end + end + + private + + def set_sp_entity_id + base_url = GlobalConfigService.load('FRONTEND_URL', 'http://localhost:3000') + self.sp_entity_id = "#{base_url}/saml/sp/#{account_id}" + end + + def sp_entity_id_needs_generation? + sp_entity_id.blank? + end + + def installation_name + GlobalConfigService.load('INSTALLATION_NAME', 'Chatwoot') + end +end diff --git a/enterprise/app/models/enterprise/account.rb b/enterprise/app/models/enterprise/account.rb index dff97c0ee..0bf93c98b 100644 --- a/enterprise/app/models/enterprise/account.rb +++ b/enterprise/app/models/enterprise/account.rb @@ -27,4 +27,8 @@ module Enterprise::Account def unmark_for_deletion custom_attributes.delete('marked_for_deletion_at') && custom_attributes.delete('marked_for_deletion_reason') && save end + + def saml_enabled? + saml_settings&.saml_enabled? || false + end end diff --git a/enterprise/app/models/enterprise/concerns/account.rb b/enterprise/app/models/enterprise/concerns/account.rb index e1136fd07..b52ac4b3e 100644 --- a/enterprise/app/models/enterprise/concerns/account.rb +++ b/enterprise/app/models/enterprise/concerns/account.rb @@ -13,5 +13,7 @@ module Enterprise::Concerns::Account has_many :copilot_threads, dependent: :destroy_async has_many :voice_channels, dependent: :destroy_async, class_name: '::Channel::Voice' + + has_one :saml_settings, dependent: :destroy_async, class_name: 'AccountSamlSettings' end end diff --git a/enterprise/app/policies/account_saml_settings_policy.rb b/enterprise/app/policies/account_saml_settings_policy.rb new file mode 100644 index 000000000..fc1418935 --- /dev/null +++ b/enterprise/app/policies/account_saml_settings_policy.rb @@ -0,0 +1,17 @@ +class AccountSamlSettingsPolicy < ApplicationPolicy + def show? + @account_user.administrator? + end + + def create? + @account_user.administrator? + end + + def update? + @account_user.administrator? + end + + def destroy? + @account_user.administrator? + end +end diff --git a/enterprise/app/views/api/v1/accounts/saml_settings/create.json.jbuilder b/enterprise/app/views/api/v1/accounts/saml_settings/create.json.jbuilder new file mode 100644 index 000000000..fa0c761c9 --- /dev/null +++ b/enterprise/app/views/api/v1/accounts/saml_settings/create.json.jbuilder @@ -0,0 +1 @@ +json.partial! 'api/v1/models/account_saml_settings', account_saml_settings: @saml_settings diff --git a/enterprise/app/views/api/v1/accounts/saml_settings/show.json.jbuilder b/enterprise/app/views/api/v1/accounts/saml_settings/show.json.jbuilder new file mode 100644 index 000000000..fa0c761c9 --- /dev/null +++ b/enterprise/app/views/api/v1/accounts/saml_settings/show.json.jbuilder @@ -0,0 +1 @@ +json.partial! 'api/v1/models/account_saml_settings', account_saml_settings: @saml_settings diff --git a/enterprise/app/views/api/v1/accounts/saml_settings/update.json.jbuilder b/enterprise/app/views/api/v1/accounts/saml_settings/update.json.jbuilder new file mode 100644 index 000000000..fa0c761c9 --- /dev/null +++ b/enterprise/app/views/api/v1/accounts/saml_settings/update.json.jbuilder @@ -0,0 +1 @@ +json.partial! 'api/v1/models/account_saml_settings', account_saml_settings: @saml_settings diff --git a/enterprise/app/views/api/v1/models/_account_saml_settings.json.jbuilder b/enterprise/app/views/api/v1/models/_account_saml_settings.json.jbuilder new file mode 100644 index 000000000..b17944605 --- /dev/null +++ b/enterprise/app/views/api/v1/models/_account_saml_settings.json.jbuilder @@ -0,0 +1,10 @@ +json.id account_saml_settings.id +json.account_id account_saml_settings.account_id +json.sso_url account_saml_settings.sso_url +json.certificate account_saml_settings.certificate +json.fingerprint account_saml_settings.certificate_fingerprint +json.idp_entity_id account_saml_settings.idp_entity_id +json.sp_entity_id account_saml_settings.sp_entity_id +json.role_mappings account_saml_settings.role_mappings || {} +json.created_at account_saml_settings.created_at +json.updated_at account_saml_settings.updated_at diff --git a/spec/enterprise/controllers/api/v1/accounts/saml_settings_controller_spec.rb b/spec/enterprise/controllers/api/v1/accounts/saml_settings_controller_spec.rb new file mode 100644 index 000000000..14bf0ebb0 --- /dev/null +++ b/spec/enterprise/controllers/api/v1/accounts/saml_settings_controller_spec.rb @@ -0,0 +1,265 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Api::V1::Accounts::SamlSettings', type: :request do + let(:account) { create(:account) } + let(:agent) { create(:user, account: account, role: :agent) } + let(:administrator) { create(:user, account: account, role: :administrator) } + + before do + account.enable_features('saml') + account.save! + end + + def json_response + JSON.parse(response.body, symbolize_names: true) + end + + describe 'GET /api/v1/accounts/{account.id}/saml_settings' do + context 'when unauthenticated' do + it 'returns unauthorized' do + get "/api/v1/accounts/#{account.id}/saml_settings" + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when authenticated as administrator' do + context 'when SAML settings exist' do + let(:saml_settings) do + create(:account_saml_settings, + account: account, + sso_url: 'https://idp.example.com/saml/sso', + role_mappings: { 'Admins' => { 'role' => 1 } }) + end + + before do + saml_settings # Ensure the record exists + end + + it 'returns the SAML settings' do + get "/api/v1/accounts/#{account.id}/saml_settings", + headers: administrator.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:success) + expect(json_response[:sso_url]).to eq('https://idp.example.com/saml/sso') + expect(json_response[:role_mappings]).to eq({ Admins: { role: 1 } }) + end + end + + context 'when SAML settings do not exist' do + it 'returns default SAML settings' do + get "/api/v1/accounts/#{account.id}/saml_settings", + headers: administrator.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:success) + expect(json_response[:role_mappings]).to eq({}) + end + end + end + + context 'when authenticated as agent' do + it 'returns unauthorized' do + get "/api/v1/accounts/#{account.id}/saml_settings", + headers: agent.create_new_auth_token + + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when SAML feature is not enabled' do + before do + account.disable_features('saml') + account.save! + end + + it 'returns forbidden with feature not enabled message' do + get "/api/v1/accounts/#{account.id}/saml_settings", + headers: administrator.create_new_auth_token + + expect(response).to have_http_status(:forbidden) + end + end + end + + describe 'POST /api/v1/accounts/{account.id}/saml_settings' do + let(:valid_params) do + key = OpenSSL::PKey::RSA.new(2048) + cert = OpenSSL::X509::Certificate.new + cert.version = 2 + cert.serial = 1 + cert.subject = OpenSSL::X509::Name.parse('/C=US/ST=Test/L=Test/O=Test/CN=test.example.com') + cert.issuer = cert.subject + cert.public_key = key.public_key + cert.not_before = Time.zone.now + cert.not_after = cert.not_before + (365 * 24 * 60 * 60) + cert.sign(key, OpenSSL::Digest.new('SHA256')) + + { + saml_settings: { + sso_url: 'https://idp.example.com/saml/sso', + certificate: cert.to_pem, + idp_entity_id: 'https://idp.example.com/saml/metadata', + role_mappings: { 'Admins' => { 'role' => 1 }, 'Users' => { 'role' => 0 } } + } + } + end + + context 'when unauthenticated' do + it 'returns unauthorized' do + post "/api/v1/accounts/#{account.id}/saml_settings", params: valid_params + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when authenticated as administrator' do + context 'with valid parameters' do + it 'creates SAML settings' do + expect do + post "/api/v1/accounts/#{account.id}/saml_settings", + params: valid_params, + headers: administrator.create_new_auth_token, + as: :json + end.to change(AccountSamlSettings, :count).by(1) + + expect(response).to have_http_status(:success) + + saml_settings = AccountSamlSettings.find_by(account: account) + expect(saml_settings.sso_url).to eq('https://idp.example.com/saml/sso') + expect(saml_settings.role_mappings).to eq({ 'Admins' => { 'role' => 1 }, 'Users' => { 'role' => 0 } }) + end + end + + context 'with invalid parameters' do + let(:invalid_params) do + valid_params.tap do |params| + params[:saml_settings][:sso_url] = nil + end + end + + it 'returns unprocessable entity' do + post "/api/v1/accounts/#{account.id}/saml_settings", + params: invalid_params, + headers: administrator.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:unprocessable_entity) + expect(AccountSamlSettings.count).to eq(0) + end + end + end + + context 'when authenticated as agent' do + it 'returns unauthorized' do + post "/api/v1/accounts/#{account.id}/saml_settings", + params: valid_params, + headers: agent.create_new_auth_token + + expect(response).to have_http_status(:unauthorized) + expect(AccountSamlSettings.count).to eq(0) + end + end + end + + describe 'PUT /api/v1/accounts/{account.id}/saml_settings' do + let(:saml_settings) do + create(:account_saml_settings, + account: account, + sso_url: 'https://old.example.com/saml') + end + let(:update_params) do + key = OpenSSL::PKey::RSA.new(2048) + cert = OpenSSL::X509::Certificate.new + cert.version = 2 + cert.serial = 3 + cert.subject = OpenSSL::X509::Name.parse('/C=US/ST=Test/L=Test/O=Test/CN=update.example.com') + cert.issuer = cert.subject + cert.public_key = key.public_key + cert.not_before = Time.zone.now + cert.not_after = cert.not_before + (365 * 24 * 60 * 60) + cert.sign(key, OpenSSL::Digest.new('SHA256')) + + { + saml_settings: { + sso_url: 'https://new.example.com/saml/sso', + certificate: cert.to_pem, + role_mappings: { 'NewGroup' => { 'custom_role_id' => 5 } } + } + } + end + + before do + saml_settings # Ensure the record exists + end + + context 'when unauthenticated' do + it 'returns unauthorized' do + put "/api/v1/accounts/#{account.id}/saml_settings", params: update_params + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when authenticated as administrator' do + it 'updates SAML settings' do + put "/api/v1/accounts/#{account.id}/saml_settings", + params: update_params, + headers: administrator.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:success) + + saml_settings.reload + expect(saml_settings.sso_url).to eq('https://new.example.com/saml/sso') + expect(saml_settings.role_mappings).to eq({ 'NewGroup' => { 'custom_role_id' => 5 } }) + end + end + + context 'when authenticated as agent' do + it 'returns unauthorized' do + put "/api/v1/accounts/#{account.id}/saml_settings", + params: update_params, + headers: agent.create_new_auth_token + + expect(response).to have_http_status(:unauthorized) + end + end + end + + describe 'DELETE /api/v1/accounts/{account.id}/saml_settings' do + let(:saml_settings) { create(:account_saml_settings, account: account) } + + before do + saml_settings # Ensure the record exists + end + + context 'when unauthenticated' do + it 'returns unauthorized' do + delete "/api/v1/accounts/#{account.id}/saml_settings" + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when authenticated as administrator' do + it 'destroys SAML settings' do + expect do + delete "/api/v1/accounts/#{account.id}/saml_settings", + headers: administrator.create_new_auth_token + end.to change(AccountSamlSettings, :count).by(-1) + + expect(response).to have_http_status(:no_content) + end + end + + context 'when authenticated as agent' do + it 'returns unauthorized' do + delete "/api/v1/accounts/#{account.id}/saml_settings", + headers: agent.create_new_auth_token + + expect(response).to have_http_status(:unauthorized) + expect(AccountSamlSettings.count).to eq(1) + end + end + end +end diff --git a/spec/enterprise/models/account_saml_settings_spec.rb b/spec/enterprise/models/account_saml_settings_spec.rb new file mode 100644 index 000000000..65d5119d7 --- /dev/null +++ b/spec/enterprise/models/account_saml_settings_spec.rb @@ -0,0 +1,117 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe AccountSamlSettings, type: :model do + let(:account) { create(:account) } + let(:saml_settings) { build(:account_saml_settings, account: account) } + + describe 'associations' do + it { is_expected.to belong_to(:account) } + end + + describe 'validations' do + it 'requires sso_url' do + settings = build(:account_saml_settings, account: account, sso_url: nil) + expect(settings).not_to be_valid + expect(settings.errors[:sso_url]).to include("can't be blank") + end + + it 'requires certificate' do + settings = build(:account_saml_settings, account: account, certificate: nil) + expect(settings).not_to be_valid + expect(settings.errors[:certificate]).to include("can't be blank") + end + + it 'requires idp_entity_id' do + settings = build(:account_saml_settings, account: account, idp_entity_id: nil) + expect(settings).not_to be_valid + expect(settings.errors[:idp_entity_id]).to include("can't be blank") + end + end + + describe '#saml_enabled?' do + it 'returns true when required fields are present' do + settings = build(:account_saml_settings, + account: account, + sso_url: 'https://example.com/sso', + certificate: 'valid-certificate') + expect(settings.saml_enabled?).to be true + end + + it 'returns false when sso_url is missing' do + settings = build(:account_saml_settings, + account: account, + sso_url: nil, + certificate: 'valid-certificate') + expect(settings.saml_enabled?).to be false + end + + it 'returns false when certificate is missing' do + settings = build(:account_saml_settings, + account: account, + sso_url: 'https://example.com/sso', + certificate: nil) + expect(settings.saml_enabled?).to be false + end + end + + describe 'sp_entity_id auto-generation' do + it 'automatically generates sp_entity_id when creating' do + settings = build(:account_saml_settings, account: account, sp_entity_id: nil) + expect(settings).to be_valid + settings.save! + expect(settings.sp_entity_id).to eq("http://localhost:3000/saml/sp/#{account.id}") + end + + it 'does not override existing sp_entity_id' do + custom_id = 'https://custom.example.com/saml/sp/123' + settings = build(:account_saml_settings, account: account, sp_entity_id: custom_id) + settings.save! + expect(settings.sp_entity_id).to eq(custom_id) + end + end + + describe '#certificate_fingerprint' do + let(:valid_cert_pem) do + key = OpenSSL::PKey::RSA.new(2048) + cert = OpenSSL::X509::Certificate.new + cert.version = 2 + cert.serial = 1 + cert.subject = OpenSSL::X509::Name.parse('/C=US/ST=Test/L=Test/O=Test/CN=test.example.com') + cert.issuer = cert.subject + cert.public_key = key.public_key + cert.not_before = Time.zone.now + cert.not_after = cert.not_before + (365 * 24 * 60 * 60) + cert.sign(key, OpenSSL::Digest.new('SHA256')) + cert.to_pem + end + + it 'returns fingerprint for valid certificate' do + settings = build(:account_saml_settings, account: account, certificate: valid_cert_pem) + fingerprint = settings.certificate_fingerprint + + expect(fingerprint).to be_present + expect(fingerprint).to match(/^[A-F0-9]{2}(:[A-F0-9]{2}){19}$/) # SHA1 fingerprint format + end + + it 'returns nil for blank certificate' do + settings = build(:account_saml_settings, account: account, certificate: '') + expect(settings.certificate_fingerprint).to be_nil + end + + it 'returns nil for invalid certificate' do + settings = build(:account_saml_settings, account: account, certificate: 'invalid-cert-data') + expect(settings.certificate_fingerprint).to be_nil + end + + it 'formats fingerprint correctly' do + settings = build(:account_saml_settings, account: account, certificate: valid_cert_pem) + fingerprint = settings.certificate_fingerprint + + # Should be uppercase with colons separating each byte + expect(fingerprint).to match(/^[A-F0-9:]+$/) + expect(fingerprint.count(':')).to eq(19) # 20 bytes = 19 colons + end + end +end diff --git a/spec/factories/account_saml_settings.rb b/spec/factories/account_saml_settings.rb new file mode 100644 index 000000000..4262daf7b --- /dev/null +++ b/spec/factories/account_saml_settings.rb @@ -0,0 +1,31 @@ +FactoryBot.define do + factory :account_saml_settings do + account + sso_url { 'https://idp.example.com/saml/sso' } + certificate do + key = OpenSSL::PKey::RSA.new(2048) + cert = OpenSSL::X509::Certificate.new + cert.version = 2 + cert.serial = 1 + cert.subject = OpenSSL::X509::Name.parse('/C=US/ST=Test/L=Test/O=Test/CN=test.example.com') + cert.issuer = cert.subject + cert.public_key = key.public_key + cert.not_before = Time.zone.now + cert.not_after = cert.not_before + (365 * 24 * 60 * 60) + cert.sign(key, OpenSSL::Digest.new('SHA256')) + cert.to_pem + end + idp_entity_id { 'https://idp.example.com/saml/metadata' } + role_mappings { {} } + + trait :with_role_mappings do + role_mappings do + { + 'Administrators' => { 'role' => 1 }, + 'Agents' => { 'role' => 0 }, + 'Custom-Team' => { 'custom_role_id' => 5 } + } + end + end + end +end