Files
sure/app/services/provider_loader.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

88 lines
2.9 KiB
Ruby

# frozen_string_literal: true
# Service class to load SSO provider configurations from either YAML or database
# based on the :db_sso_providers feature flag.
#
# Usage:
# providers = ProviderLoader.load_providers
#
class ProviderLoader
CACHE_KEY = "sso_providers_config"
CACHE_EXPIRES_IN = 5.minutes
class << self
# Load providers from either DB or YAML based on feature flag
# Returns an array of provider configuration hashes
def load_providers
# Check cache first for performance
cached = Rails.cache.read(CACHE_KEY)
return cached if cached.present?
providers = if use_database_providers?
load_from_database
else
load_from_yaml
end
# Cache the result
Rails.cache.write(CACHE_KEY, providers, expires_in: CACHE_EXPIRES_IN)
providers
end
# Clear the provider cache (call after updating providers in admin)
def clear_cache
Rails.cache.delete(CACHE_KEY)
end
private
def use_database_providers?
return false if Rails.env.test?
begin
# Check if feature exists, create if not (defaults to disabled)
unless Flipper.exist?(:db_sso_providers)
Flipper.add(:db_sso_providers)
end
Flipper.enabled?(:db_sso_providers)
rescue ActiveRecord::NoDatabaseError, ActiveRecord::StatementInvalid, StandardError => e
# Database not ready or other error, fall back to YAML
Rails.logger.warn("[ProviderLoader] Could not check feature flag (#{e.class}), falling back to YAML providers")
false
end
end
def load_from_database
begin
providers = SsoProvider.enabled.order(:name).map(&:to_omniauth_config)
if providers.empty?
Rails.logger.info("[ProviderLoader] No enabled providers in database, falling back to YAML")
return load_from_yaml
end
Rails.logger.info("[ProviderLoader] Loaded #{providers.count} provider(s) from database")
providers
rescue ActiveRecord::StatementInvalid, ActiveRecord::NoDatabaseError => e
Rails.logger.error("[ProviderLoader] Database error loading providers: #{e.message}, falling back to YAML")
load_from_yaml
rescue StandardError => e
Rails.logger.error("[ProviderLoader] Unexpected error loading providers from database: #{e.message}, falling back to YAML")
load_from_yaml
end
end
def load_from_yaml
begin
auth_config = Rails.application.config_for(:auth)
providers = auth_config.dig("providers") || []
Rails.logger.info("[ProviderLoader] Loaded #{providers.count} provider(s) from YAML")
providers
rescue RuntimeError, Errno::ENOENT => e
Rails.logger.error("[ProviderLoader] Error loading auth.yml: #{e.message}")
[]
end
end
end
end