diff --git a/app/controllers/accounts/sso_providers_controller.rb b/app/controllers/accounts/sso_providers_controller.rb index 1180476f..b2b39dff 100644 --- a/app/controllers/accounts/sso_providers_controller.rb +++ b/app/controllers/accounts/sso_providers_controller.rb @@ -60,6 +60,24 @@ module Accounts end end + def test_connection + ldap_configuration = LDAPConfiguration.new(ldap_configuration_params) + result = LDAP::Authenticator.new(ldap_configuration).test_connection + + if result.success? + render turbo_stream: turbo_stream.replace( + "ldap_test_connection_result", + partial: "accounts/sso_providers/ldap/connection_success" + ) + else + render turbo_stream: turbo_stream.replace( + "ldap_test_connection_result", + partial: "accounts/sso_providers/ldap/connection_failed", + locals: { error_message: result.error_message } + ) + end + end + private def sso_provider_params diff --git a/app/javascript/controllers/ldap_test_connection_controller.js b/app/javascript/controllers/ldap_test_connection_controller.js new file mode 100644 index 00000000..162f54b9 --- /dev/null +++ b/app/javascript/controllers/ldap_test_connection_controller.js @@ -0,0 +1,44 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static targets = ["button", "result"] + + async test(event) { + debugger + event.preventDefault() + + const form = this.element.closest("form") + const formData = new FormData(form) + + // Clear previous result + this.resultTarget.innerHTML = "" + + // Update button to show loading state + const button = this.buttonTarget + const originalContent = button.innerHTML + button.disabled = true + button.innerHTML = ' Testing...' + + try { + const response = await fetch("/accounts/sso_provider/test_connection", { + method: "POST", + headers: { + "X-CSRF-Token": document.querySelector('meta[name="csrf-token"]').content, + "Accept": "text/vnd.turbo-stream.html" + }, + body: formData + }) + + if (response.ok) { + const html = await response.text() + Turbo.renderStreamMessage(html) + } + } catch (error) { + console.error("LDAP test connection failed:", error) + } finally { + // Always re-enable the button so it can be clicked again + button.disabled = false + button.innerHTML = originalContent + } + } +} diff --git a/app/services/ldap/authenticator.rb b/app/services/ldap/authenticator.rb index 3f27486d..90f7c6cc 100644 --- a/app/services/ldap/authenticator.rb +++ b/app/services/ldap/authenticator.rb @@ -21,8 +21,33 @@ module LDAP # Public API # ---------- - # call(username:, password:) -> Result + # test_connection -> Result (tests bind credentials only) + # call(username:, password:) -> Result (full authentication) # + def test_connection + # Validate required configuration + if config.host.blank? + return Result.new(success?: false, error_message: "Host is required") + end + + if config.bind_dn.blank? && !config.allow_anonymous_reads? + return Result.new(success?: false, error_message: "Bind DN and password are required when anonymous reads are disabled") + end + + reader_ldap = build_reader_connection + + if reader_ldap.bind + Result.new(success?: true, error_message: nil) + else + msg = "LDAP bind failed: #{reader_ldap.get_operation_result.message}" + @logger.warn msg + Result.new(success?: false, error_message: msg) + end + rescue => e + @logger.error "LDAP test connection: unexpected error - #{e.class}: #{e.message}" + Result.new(success?: false, error_message: e.message) + end + def call(username:, password:, fetch_groups:) # 1) Bind as reader (service account or anonymous) reader_ldap = build_reader_connection @@ -54,8 +79,8 @@ module LDAP end # 4) Successful LDAP auth → map attributes, fetch groups - email = resolve_email(entry, username) - name = resolve_name(entry, username) + email = resolve_email(entry, username) + name = resolve_name(entry, username) if fetch_groups groups = fetch_group_membership(entry) else @@ -103,6 +128,7 @@ module LDAP # No way to bind safely # Let caller see failure via bind result logger.info "LDAP: no reader credentials and anonymous reads disabled" + raise "LDAP: no reader credentials and anonymous reads disabled" end Net::LDAP.new(options) diff --git a/app/views/accounts/sso_providers/ldap/_connection_failed.html.erb b/app/views/accounts/sso_providers/ldap/_connection_failed.html.erb new file mode 100644 index 00000000..77d84c14 --- /dev/null +++ b/app/views/accounts/sso_providers/ldap/_connection_failed.html.erb @@ -0,0 +1,11 @@ +<%= turbo_frame_tag "ldap_test_connection_result", class: "block mt-3", data: { ldap_test_connection_target: "result" } do %> +