--password '

' [--json] * * Exit codes: * 0 = authentication SUCCESS * 1 = authentication FAILED (see messages) * 2 = usage error * 3 = LDAP plugin row not found / misconfigured * 4 = PHP ldap extension missing * * Notes: * - This bypasses Joomla plugin boot issues in CLI and gives precise LDAP errors. * - Honors LDAP plugin params + ENV overrides (same names as your role uses). */ define('_JEXEC', 1); define('JPATH_BASE', __DIR__ . '/..'); // Bootstrap minimal Joomla DB access (no need to boot Site/CMS app) require JPATH_BASE . '/includes/defines.php'; require JPATH_BASE . '/includes/framework.php'; ini_set('display_errors', '1'); error_reporting(E_ALL); use Joomla\CMS\Factory; function getenv_clean(string $key, $default = null) { $v = getenv($key); if ($v === false || $v === '') return $default; return preg_replace('/^(["\'])(.*)\1$/', '$2', $v); } function outln($msg) { echo $msg . "\n"; } function result($ok, $label, $detail = null) { $prefix = $ok ? '[OK] ' : '[ERR] '; echo $prefix . $label . ($detail !== null ? " — $detail" : '') . "\n"; } /** * Attempt a full LDAP auth: * - connect (host:port) * - optional StartTLS (negotiate_tls) * - service bind (admin) * - lookup DN (search or users_dn template) * - user bind with provided password */ function ldap_auth_flow(array $cfg, string $username, string $password, bool $asJson): int { $report = [ 'connect' => null, 'starttls' => null, 'serviceBind' => null, 'userSearch' => null, 'userBind' => null, 'dn' => null, 'messages' => [], ]; if (!extension_loaded('ldap')) { $msg = 'PHP LDAP extension not loaded'; if ($asJson) echo json_encode(['error' => $msg], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n"; else result(false, $msg); return 4; } // Connect $ds = @ldap_connect($cfg['host'], $cfg['port']); if (!$ds) { $report['connect'] = false; $report['messages'][] = "ldap_connect failed to {$cfg['host']}:{$cfg['port']}"; return emit($report, $asJson); } ldap_set_option($ds, LDAP_OPT_PROTOCOL_VERSION, 3); ldap_set_option($ds, LDAP_OPT_NETWORK_TIMEOUT, 5); $report['connect'] = true; // StartTLS if configured if ($cfg['use_tls']) { if (@ldap_start_tls($ds)) { $report['starttls'] = true; } else { $report['starttls'] = false; $report['messages'][] = 'StartTLS failed: ' . ldap_error($ds); return emit($report, $asJson, $ds); } } else { $report['starttls'] = null; // not requested } // Service/admin bind (may be empty for anonymous) $serviceBound = @ldap_bind($ds, $cfg['bind_dn'], $cfg['bind_pw']); if (!$serviceBound) { $report['serviceBind'] = false; $report['messages'][] = 'Service bind failed: ' . ldap_error($ds) . sprintf(" (bind_dn=%s)", $cfg['bind_dn'] ?: '(anonymous)'); return emit($report, $asJson, $ds); } $report['serviceBind'] = true; // Resolve user DN $userDn = null; if (strtolower($cfg['auth_method']) === 'bind') { // Direct template substitution (no search) $userDn = str_replace(['[username]', '[USER]', '[uid]'], $username, $cfg['users_dn']); $report['userSearch'] = 'skipped (bind mode)'; } else { // search mode: find DN first $filter = sprintf('(%s=%s)', $cfg['uid_attr'], ldap_escape($username, '', LDAP_ESCAPE_FILTER)); $sr = @ldap_search($ds, $cfg['base_dn'], $filter, [$cfg['name_attr'], $cfg['mail_attr']]); if (!$sr) { $report['userSearch'] = false; $report['messages'][] = 'Search failed: ' . ldap_error($ds) . " (base_dn={$cfg['base_dn']}, filter={$filter})"; return emit($report, $asJson, $ds); } $entries = @ldap_get_entries($ds, $sr); $count = (int)($entries['count'] ?? 0); if ($count < 1) { $report['userSearch'] = false; $report['messages'][] = "User not found under base_dn={$cfg['base_dn']} with {$cfg['uid_attr']}={$username}"; return emit($report, $asJson, $ds); } $userDn = $entries[0]['dn'] ?? null; $report['userSearch'] = true; } if (!$userDn) { $report['messages'][] = 'No user DN resolved.'; return emit($report, $asJson, $ds); } $report['dn'] = $userDn; // Attempt user bind with provided password $ok = @ldap_bind($ds, $userDn, $password); if ($ok) { $report['userBind'] = true; return emit($report, $asJson, $ds, /*success*/true); } else { $report['userBind'] = false; $report['messages'][] = 'User bind failed: ' . ldap_error($ds); return emit($report, $asJson, $ds); } } function emit(array $report, bool $asJson, $ds = null, bool $success = false): int { if ($asJson) { echo json_encode($report, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n"; } else { if ($report['connect'] !== null) result((bool)$report['connect'], 'Connected'); if ($report['starttls'] !== null) result((bool)$report['starttls'], 'StartTLS'); if ($report['serviceBind'] !== null) result((bool)$report['serviceBind'], 'Service bind'); if ($report['userSearch'] !== null && $report['userSearch'] !== 'skipped (bind mode)') { result((bool)$report['userSearch'], 'User search'); } elseif ($report['userSearch'] === 'skipped (bind mode)') { outln('Info User search: skipped (bind mode)'); } if ($report['dn']) outln('Info User DN: ' . $report['dn']); if ($report['userBind'] !== null) result((bool)$report['userBind'], 'User bind'); foreach ($report['messages'] as $m) { outln('Note ' . $m); } outln($success ? 'Overall: SUCCESS' : 'Overall: FAIL'); } if ($ds) { @ldap_unbind($ds); } return $success ? 0 : 1; } // ------ Parse CLI args ------ $args = getopt('', ['username:', 'password:', 'json']); $username = $args['username'] ?? null; $password = $args['password'] ?? null; $asJson = array_key_exists('json', $args); if (!$username || $password === null) { fwrite(STDERR, "Usage: --username --password '

' [--json]\n"); exit(2); } // ------ Load LDAP plugin params from DB ------ $dbo = Factory::getDbo(); $q = $dbo->getQuery(true) ->select('*') ->from($dbo->quoteName('#__extensions')) ->where($dbo->quoteName('type') . ' = ' . $dbo->quote('plugin')) ->where($dbo->quoteName('folder') . ' = ' . $dbo->quote('authentication')) ->where($dbo->quoteName('element') . ' = ' . $dbo->quote('ldap')); $dbo->setQuery($q); $ext = $dbo->loadObject(); if (!$ext) { if ($asJson) echo json_encode(['error' => 'LDAP plugin row not found in #__extensions'], JSON_PRETTY_PRINT) . "\n"; else result(false, 'LDAP plugin not found in #__extensions (authentication/ldap)'); exit(3); } $params = json_decode($ext->params ?: "{}", true) ?: []; // Effective config with ENV overrides (matches your role variables) $cfg = [ 'host' => getenv_clean('JOOMLA_LDAP_HOST', $params['host'] ?? 'openldap'), 'port' => (int) getenv_clean('JOOMLA_LDAP_PORT', (string)($params['port'] ?? 389)), 'base_dn' => getenv_clean('JOOMLA_LDAP_BASE_DN', $params['base_dn'] ?? ''), 'users_dn' => getenv_clean('JOOMLA_LDAP_USER_TREE_DN', $params['users_dn'] ?? ''), 'bind_dn' => getenv_clean('JOOMLA_LDAP_BIND_DN', $params['username'] ?? ''), 'bind_pw' => getenv_clean('JOOMLA_LDAP_BIND_PASSWORD', $params['password'] ?? ''), 'use_tls' => filter_var(getenv_clean('JOOMLA_LDAP_USE_STARTTLS', $params['negotiate_tls'] ?? false), FILTER_VALIDATE_BOOL), 'auth_method' => getenv_clean('JOOMLA_LDAP_AUTH_METHOD', $params['auth_method'] ?? 'search'), 'search_string' => getenv_clean('JOOMLA_LDAP_USER_SEARCH_STRING', $params['search_string'] ?? 'uid=[username]'), 'uid_attr' => getenv_clean('JOOMLA_LDAP_UID_ATTR', $params['ldap_uid'] ?? 'uid'), 'mail_attr' => getenv_clean('JOOMLA_LDAP_EMAIL_ATTR', $params['ldap_email'] ?? 'mail'), 'name_attr' => getenv_clean('JOOMLA_LDAP_NAME_ATTR', $params['ldap_fullname'] ?? 'cn'), ]; // Print effective config (non-JSON mode) with masked password if (!$asJson) { outln('Effective LDAP configuration:'); outln(' host: ' . $cfg['host']); outln(' port: ' . $cfg['port']); outln(' base_dn: ' . $cfg['base_dn']); outln(' users_dn: ' . $cfg['users_dn']); outln(' bind_dn: ' . ($cfg['bind_dn'] ?: '(anonymous)')); outln(' bind_pw: ' . ($cfg['bind_pw'] !== '' ? '***' : '')); outln(' use_tls: ' . ($cfg['use_tls'] ? '1' : '')); outln(' auth_method: ' . $cfg['auth_method']); outln(' uid_attr: ' . $cfg['uid_attr']); } // Run the flow exit( ldap_auth_flow($cfg, $username, $password, $asJson) );