namespace :cartodb do namespace :ldap do desc "Tests an LDAP connection. Returns a summary of all tests ran, with success status and error messages." task :test_ldap_connection, [:from_database] => :environment do |_t, args| args.with_defaults(from_database: false) test_user = ENV['TEST_USER'] test_password = ENV['TEST_PASSWORD'] test_search = ENV['TEST_SEARCH'] ldap = if args.from_database Carto::Ldap::Configuration.first else ldap_configuration_from_environment(Carto::Organization.new) end # Test configuration config_result = if ldap.valid? { success: true } else { success: false, error: ldap.errors.to_h } end result = { config: config_result } REQUIRED_FIELDS = [:host, :port, :connection_user, :connection_password].freeze if !config_result[:success] && config_result[:error].keys.any? { |k| REQUIRED_FIELDS.include?(k) } result[:connection] = { success: false, error: 'Configuration not valid' } end # Test connection result[:connection] = ldap.test_connection unless result[:connection] CONNECTION_DEPENDENT_TESTS = [:login, :user_search, :group_search, :search].freeze if result[:connection][:success] result[:connection].delete(:connection) if ldap.domain_bases.present? result[:login] = if test_user.present? && test_password.present? test_ldap_user(ldap, test_user, test_password) else { success: false, error: 'Test credentials not provided' } end result[:user_search] = if ldap.user_object_class.present? user_count = ldap.users.count { success: user_count > 0, count: user_count } else { success: false, error: 'User class not provided' } end result[:group_search] = if ldap.group_object_class.present? group_count = ldap.groups.count { success: group_count > 0, count: group_count } else { success: false, error: 'Group class not provided' } end result[:search] = if test_search.present? object_count = ldap.send(:search_in_domain_bases, Net::LDAP::Filter.eq('objectClass', test_search)).count { success: true, count: object_count } else { success: false, error: 'Object class not provided' } end else CONNECTION_DEPENDENT_TESTS.each do |test| result[test] = { success: false, error: 'Domain bases not provided' } end end else CONNECTION_DEPENDENT_TESTS.each do |test| result[test] = { success: false, error: 'Connection failed' } end end puts JSON.pretty_generate(result) end # INFO: Separate multiple domain names by commas desc "Creates an LDAP Configuration entry" task :create_ldap_configuration, [] => :environment do |_t, _args| if ENV['ORGANIZATION_ID'].blank? if ENV['ORGANIZATION_NAME'].blank? raise "Missing ORGANIZATION_ID and ORGANIZATION_NAME. Must provide one of both" else organization = Carto::Organization.where(name: ENV['ORGANIZATION_NAME']).first end else organization = Carto::Organization.find(ENV['ORGANIZATION_ID']) end ldap = ldap_configuration_from_environment(organization) unless ldap.valid? missing = ldap.errors.keys.reject { |k| k == :domain_bases_list } raise "Missing: " + missing.join(', ').upcase end if ldap.save puts "LDAP configuration created with id: #{ldap.id}" else puts "Error saving LDAP configuration" end end desc "Deletes existing LDAP Configuration entries" task :reset_ldap_configuration, [] => :environment do |_t, _args| Carto::Ldap::Configuration.delete_all end end private def ldap_configuration_from_environment(organization) # Mandatory: connection parameters host = ENV['HOST'] port = ENV['PORT'] ssl_version = ENV['SSL_VERSION'].blank? ? nil : ENV['SSL_VERSION'] encryption = ENV['ENCRYPTION'].blank? ? nil : ENV['ENCRYPTION'] connection_user = ENV['CONNECTION_USER'] connection_password = ENV['CONNECTION_PASSWORD'] # Optional: for testing auth/searches user_id_field = ENV['USER_ID_FIELD'] domain_bases = ENV['DOMAIN_BASES'] additional_search_filter = ENV['ADDITIONAL_SEARCH_FILTER'] # Optional: for testing searches username_field = ENV['USERNAME_FIELD'] email_field = ENV['EMAIL_FIELD'] user_object_class = ENV['USER_OBJECT_CLASS'] group_object_class = ENV['GROUP_OBJECT_CLASS'] Carto::Ldap::Configuration.new( organization: organization, host: host, port: port, encryption: encryption, ssl_version: ssl_version, connection_user: connection_user, connection_password: connection_password, user_id_field: user_id_field, username_field: username_field, additional_search_filter: additional_search_filter, email_field: email_field, domain_bases: domain_bases, user_object_class: user_object_class, group_object_class: group_object_class ) end def test_ldap_user(ldap, test_user, test_password) unless ldap.user_id_field.present? && ldap.username_field.present? && ldap.email_field.present? return { success: false, error: 'User id, username or email attribute names not specified.' } end entry = ldap.authenticate(test_user, test_password) return { success: false, error: 'Cannot login with test credentials' } unless entry if entry.email && entry.username { success: true, email: entry.email, username: entry.username } else { success: false, error: 'Cannot retrieve user attributes. Check the email and username attribute names' } end end end