Files
sure/app/controllers/admin/sso_providers_controller.rb
Josh Waldrep 14993d871c feat: comprehensive SSO/OIDC upgrade with enterprise features
Multi-provider SSO support:
   - Database-backed SSO provider management with admin UI
   - Support for OpenID Connect, Google OAuth2, GitHub, and SAML 2.0
   - Flipper feature flag (db_sso_providers) for dynamic provider loading
   - ProviderLoader service for YAML or database configuration

   Admin functionality:
   - Admin::SsoProvidersController for CRUD operations
   - Admin::UsersController for super_admin role management
   - Pundit policies for authorization
   - Test connection endpoint for validating provider config

   User provisioning improvements:
   - JIT (just-in-time) account creation with configurable default role
   - Changed default JIT role from admin to member (security)
   - User attribute sync on each SSO login
   - Group/role mapping from IdP claims

   SSO identity management:
   - Settings::SsoIdentitiesController for users to manage connected accounts
   - Issuer validation for OIDC identities
   - Unlink protection when no password set

   Audit logging:
   - SsoAuditLog model tracking login, logout, link, unlink, JIT creation
   - Captures IP address, user agent, and metadata

   Advanced OIDC features:
   - Custom scopes per provider
   - Configurable prompt parameter (login, consent, select_account, none)
   - RP-initiated logout (federated logout to IdP)
   - id_token storage for logout

   SAML 2.0 support:
   - omniauth-saml gem integration
   - IdP metadata URL or manual configuration
   - Certificate and fingerprint validation
   - NameID format configuration
2026-01-03 17:56:42 -05:00

158 lines
4.4 KiB
Ruby

# frozen_string_literal: true
module Admin
class SsoProvidersController < Admin::BaseController
before_action :set_sso_provider, only: %i[show edit update destroy toggle test_connection]
def index
authorize SsoProvider
@sso_providers = policy_scope(SsoProvider).order(:name)
end
def show
authorize @sso_provider
end
def new
@sso_provider = SsoProvider.new
authorize @sso_provider
end
def create
@sso_provider = SsoProvider.new(processed_params)
authorize @sso_provider
# Auto-generate redirect_uri if not provided
if @sso_provider.redirect_uri.blank? && @sso_provider.name.present?
@sso_provider.redirect_uri = "#{request.base_url}/auth/#{@sso_provider.name}/callback"
end
if @sso_provider.save
log_provider_change(:create, @sso_provider)
clear_provider_cache
redirect_to admin_sso_providers_path, notice: t(".success")
else
render :new, status: :unprocessable_entity
end
end
def edit
authorize @sso_provider
end
def update
authorize @sso_provider
# Auto-update redirect_uri if name changed
params_hash = processed_params.to_h
if params_hash[:name].present? && params_hash[:name] != @sso_provider.name
params_hash[:redirect_uri] = "#{request.base_url}/auth/#{params_hash[:name]}/callback"
end
if @sso_provider.update(params_hash)
log_provider_change(:update, @sso_provider)
clear_provider_cache
redirect_to admin_sso_providers_path, notice: t(".success")
else
render :edit, status: :unprocessable_entity
end
end
def destroy
authorize @sso_provider
@sso_provider.destroy!
log_provider_change(:destroy, @sso_provider)
clear_provider_cache
redirect_to admin_sso_providers_path, notice: t(".success")
end
def toggle
authorize @sso_provider
@sso_provider.update!(enabled: !@sso_provider.enabled)
log_provider_change(:toggle, @sso_provider)
clear_provider_cache
notice = @sso_provider.enabled? ? t(".success_enabled") : t(".success_disabled")
redirect_to admin_sso_providers_path, notice: notice
end
def test_connection
authorize @sso_provider
tester = SsoProviderTester.new(@sso_provider)
result = tester.test!
render json: {
success: result.success?,
message: result.message,
details: result.details
}
end
private
def set_sso_provider
@sso_provider = SsoProvider.find(params[:id])
end
def sso_provider_params
params.require(:sso_provider).permit(
:strategy,
:name,
:label,
:icon,
:enabled,
:issuer,
:client_id,
:client_secret,
:redirect_uri,
:scopes,
:prompt,
settings: [
:default_role, :scopes, :prompt,
# SAML settings
:idp_metadata_url, :idp_sso_url, :idp_slo_url,
:idp_certificate, :idp_cert_fingerprint, :name_id_format,
role_mapping: {}
]
)
end
# Process params to convert role_mapping comma-separated strings to arrays
def processed_params
result = sso_provider_params.to_h
if result[:settings].present? && result[:settings][:role_mapping].present?
result[:settings][:role_mapping] = result[:settings][:role_mapping].transform_values do |v|
# Convert comma-separated string to array, removing empty values
v.to_s.split(",").map(&:strip).reject(&:blank?)
end
# Remove empty role mappings
result[:settings][:role_mapping] = result[:settings][:role_mapping].reject { |_, v| v.empty? }
result[:settings].delete(:role_mapping) if result[:settings][:role_mapping].empty?
end
result
end
def log_provider_change(action, provider)
Rails.logger.info(
"[Admin::SsoProviders] #{action.to_s.upcase} - " \
"user_id=#{Current.user.id} " \
"provider_id=#{provider.id} " \
"provider_name=#{provider.name} " \
"strategy=#{provider.strategy} " \
"enabled=#{provider.enabled}"
)
end
def clear_provider_cache
ProviderLoader.clear_cache
Rails.logger.info("[Admin::SsoProviders] Provider cache cleared by user_id=#{Current.user.id}")
end
end
end