Files
sure/lib/tasks/sso_providers.rake
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

155 lines
4.8 KiB
Ruby

# frozen_string_literal: true
namespace :sso_providers do
desc "Seed SSO providers from config/auth.yml into the database"
task seed: :environment do
dry_run = ENV["DRY_RUN"] == "true"
puts "=" * 80
puts "SSO Provider Seeding Task"
puts "=" * 80
puts "Mode: #{dry_run ? 'DRY RUN (no changes will be saved)' : 'LIVE (changes will be saved)'}"
puts "Source: config/auth.yml"
puts "-" * 80
begin
# Load auth.yml safely
auth_config_path = Rails.root.join("config", "auth.yml")
unless File.exist?(auth_config_path)
puts "ERROR: config/auth.yml not found"
exit 1
end
# Use safe_load to prevent code injection
auth_config = YAML.safe_load(
ERB.new(File.read(auth_config_path)).result,
permitted_classes: [ Symbol ],
aliases: true
)
# Get providers for current environment
env_config = auth_config[Rails.env] || auth_config["default"]
providers = env_config&.dig("providers") || []
if providers.empty?
puts "WARNING: No providers found in config/auth.yml for #{Rails.env} environment"
exit 0
end
puts "Found #{providers.count} provider(s) in config/auth.yml"
puts "-" * 80
created_count = 0
updated_count = 0
skipped_count = 0
errors = []
ActiveRecord::Base.transaction do
providers.each do |provider_config|
provider_config = provider_config.deep_symbolize_keys
# Extract provider attributes
name = provider_config[:name] || provider_config[:id]
strategy = provider_config[:strategy]
unless name.present? && strategy.present?
puts "SKIP: Provider missing name or strategy: #{provider_config.inspect}"
skipped_count += 1
next
end
# Find or initialize provider
provider = SsoProvider.find_or_initialize_by(name: name)
is_new = provider.new_record?
# Build attributes hash
attributes = {
strategy: strategy,
label: provider_config[:label] || name.titleize,
icon: provider_config[:icon],
enabled: provider_config.key?(:enabled) ? provider_config[:enabled] : true,
issuer: provider_config[:issuer],
client_id: provider_config[:client_id],
redirect_uri: provider_config[:redirect_uri],
settings: provider_config[:settings] || {}
}
# Only set client_secret if provided (don't overwrite existing)
if provider_config[:client_secret].present?
attributes[:client_secret] = provider_config[:client_secret]
end
# Assign attributes
provider.assign_attributes(attributes.compact)
# Check if changed
if provider.changed?
if dry_run
puts "#{is_new ? 'CREATE' : 'UPDATE'} (dry-run): #{name} (#{strategy})"
puts " Changes: #{provider.changes.keys.join(', ')}"
else
if provider.save
puts "#{is_new ? 'CREATE' : 'UPDATE'}: #{name} (#{strategy})"
is_new ? created_count += 1 : updated_count += 1
else
error_msg = "Failed to save #{name}: #{provider.errors.full_messages.join(', ')}"
puts "ERROR: #{error_msg}"
errors << error_msg
end
end
else
puts "SKIP: #{name} (no changes)"
skipped_count += 1
end
end
# Rollback transaction if dry run
raise ActiveRecord::Rollback if dry_run
end
puts "-" * 80
puts "Summary:"
puts " Created: #{created_count}"
puts " Updated: #{updated_count}"
puts " Skipped: #{skipped_count}"
puts " Errors: #{errors.count}"
if errors.any?
puts "\nErrors encountered:"
errors.each { |error| puts " - #{error}" }
end
if dry_run
puts "\nDRY RUN: No changes were saved to the database"
puts "Run without DRY_RUN=true to apply changes"
else
puts "\nSeeding completed successfully!"
puts "Note: Clear provider cache or restart server for changes to take effect"
end
puts "=" * 80
rescue => e
puts "ERROR: #{e.class}: #{e.message}"
puts e.backtrace.first(5).join("\n")
exit 1
end
end
desc "List all SSO providers in the database"
task list: :environment do
providers = SsoProvider.order(:name)
if providers.empty?
puts "No SSO providers found in database"
else
puts "SSO Providers (#{providers.count}):"
puts "-" * 80
providers.each do |provider|
status = provider.enabled? ? "✓ enabled" : "✗ disabled"
puts "#{provider.name.ljust(20)} | #{provider.strategy.ljust(20)} | #{status}"
end
end
end
end