Added all LDAP changes before removing, because it doesn't work. Will trty to replace it by OIDC

This commit is contained in:
2025-08-28 19:22:37 +02:00
parent ef801aa498
commit fe399c3967
13 changed files with 641 additions and 43 deletions

View File

@@ -1,8 +1,7 @@
<?php
/**
* System plugin that auto-creates a Joomla user after successful LDAP authentication.
* It reads the LDAP Auth plugin params from #__extensions (folder=authentication, element=ldap),
* looks up cn/mail for the authenticated uid, and creates a local Joomla user if missing.
* Now with structured logging (enable via JOOMLA_LDAP_AUTOCREATE_LOG=1).
*/
defined('_JEXEC') || die;
@@ -10,99 +9,150 @@ defined('_JEXEC') || die;
use Joomla\CMS\Factory;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\User\User;
use Joomla\Database\DatabaseDriver;
use Joomla\Authentication\Authentication;
use Joomla\CMS\Log\Log;
class PlgSystemLdapautocreate extends CMSPlugin
{
protected $app;
private bool $logEnabled = false;
public function __construct(&$subject, $config)
{
parent::__construct($subject, $config);
// Enable logger only when explicitly requested
$this->logEnabled = (bool) filter_var(getenv('JOOMLA_LDAP_AUTOCREATE_LOG') ?: '0', FILTER_VALIDATE_BOOL);
if ($this->logEnabled) {
// Register a dedicated channel and file
Log::addLogger(
['text_file' => 'ldapauth.log', 'extension' => 'plg_system_ldapautocreate'],
Log::ALL,
['ldapautocreate']
);
$this->log('logger-initialized', ['version' => '1.0.0']);
}
}
private function log(string $event, array $ctx = []): void
{
if (!$this->logEnabled) {
return;
}
$payload = json_encode(['event' => $event, 'ctx' => $ctx], JSON_UNESCAPED_SLASHES);
Log::add($payload, Log::INFO, 'ldapautocreate');
}
/**
* Runs after authentication handlers; fires for both frontend and backend.
* @param array $options Contains 'username' and more after auth
* @return void
* Fires after authentication handlers; frontend and backend.
* @param array $options
* @param object $response ->status, ->type, ->error_message, ->username, etc.
*/
public function onUserAfterAuthenticate($options, $response)
{
// Only proceed on success
if (($response->status ?? null) !== Authentication::STATUS_SUCCESS) {
// Defensive: normalize shape
$status = $response->status ?? null;
$type = $response->type ?? '(unknown)';
$user = $response->username ?? ($options['username'] ?? null);
$this->log('after-auth-enter', [
'username' => $user,
'status' => $status,
'type' => $type,
'error' => $response->error_message ?? null,
]);
// Only proceed when LDAP (or any plugin) actually succeeded
if ($status !== Authentication::STATUS_SUCCESS) {
$this->log('skip-non-success', ['reason' => 'status!=' . Authentication::STATUS_SUCCESS]);
return;
}
$username = $response->username ?? $options['username'] ?? null;
if (!$username) {
if (!$user) {
$this->log('skip-missing-username');
return;
}
/** @var DatabaseDriver $dbo */
// If user exists locally, nothing to do
$dbo = Factory::getDbo();
// If user already exists locally, nothing to do
$exists = (int) $dbo->setQuery(
$count = (int) $dbo->setQuery(
$dbo->getQuery(true)
->select('COUNT(*)')
->from($dbo->quoteName('#__users'))
->where($dbo->quoteName('username') . ' = ' . $dbo->quote($username))
->where($dbo->quoteName('username') . ' = ' . $dbo->quote($user))
)->loadResult();
if ($exists) {
if ($count > 0) {
$this->log('user-exists', ['username' => $user]);
return;
}
// Read LDAP Auth plugin params to connect (the ones we configured via cli-ldap.php)
// Read LDAP plugin params (host/port/base_dn/attrs) and fetch cn/mail
$ldapExt = $dbo->setQuery(
$dbo->getQuery(true)
->select('*')
->from($dbo->quoteName('#__extensions'))
->where($dbo->quoteName('type') . " = 'plugin'")
->where($dbo->quoteName('folder') . " = 'authentication'")
->where($dbo->quoteName('element') . " = 'ldap'")
->where("type='plugin' AND folder='authentication' AND element='ldap'")
)->loadObject();
if (!$ldapExt) {
return; // LDAP plugin not found; bail out silently
$this->log('ldap-plugin-missing');
return;
}
$p = json_decode($ldapExt->params ?: "{}", true) ?: [];
$host = $p['host'] ?? 'openldap';
$port = (int) ($p['port'] ?? 389);
$baseDn = $p['base_dn'] ?? '';
$bindDn = $p['username'] ?? '';
$bindPw = $p['password'] ?? '';
$attrUid = $p['ldap_uid'] ?? 'uid';
$host = $p['host'] ?? 'openldap';
$port = (int) ($p['port'] ?? 389);
$baseDn = $p['base_dn'] ?? '';
$bindDn = $p['username'] ?? '';
$bindPw = $p['password'] ?? '';
$attrUid = $p['ldap_uid'] ?? 'uid';
$attrMail = $p['ldap_email'] ?? 'mail';
$attrName = $p['ldap_fullname'] ?? 'cn';
// Look up user in LDAP to fetch name/email
$this->log('ldap-params', [
'host' => $host, 'port' => $port, 'base_dn' => $baseDn,
'attrUid' => $attrUid, 'attrMail' => $attrMail, 'attrName' => $attrName,
]);
$ds = @ldap_connect($host, $port);
if (!$ds) { return; }
if (!$ds) { $this->log('ldap-connect-failed'); return; }
ldap_set_option($ds, LDAP_OPT_PROTOCOL_VERSION, 3);
@ldap_bind($ds, $bindDn, $bindPw);
$filter = sprintf('(%s=%s)', $attrUid, ldap_escape($username, '', LDAP_ESCAPE_FILTER));
$filter = sprintf('(%s=%s)', $attrUid, ldap_escape($user, '', LDAP_ESCAPE_FILTER));
$sr = @ldap_search($ds, $baseDn, $filter, [$attrName, $attrMail]);
$entry = $sr ? @ldap_first_entry($ds, $sr) : null;
$name = $entry ? (@ldap_get_values($ds, $entry, $attrName)[0] ?? $username) : $username;
$email = $entry ? (@ldap_get_values($ds, $entry, $attrMail)[0] ?? ($username.'@example.invalid')) : ($username.'@example.invalid');
$name = $entry ? (@ldap_get_values($ds, $entry, $attrName)[0] ?? $user) : $user;
$email = $entry ? (@ldap_get_values($ds, $entry, $attrMail)[0] ?? ($user.'@example.invalid')) : ($user.'@example.invalid');
if ($ds) { @ldap_unbind($ds); }
// Create Joomla user (Registered group id=2)
$this->log('creating-user', ['username' => $user, 'name' => $name, 'email' => $email]);
// Create Joomla user in Registered (id=2)
$data = [
'name' => $name,
'username' => $username,
'username' => $user,
'email' => $email,
// Password is irrelevant for LDAP; set a random one
'password' => bin2hex(random_bytes(12)),
'block' => 0,
'groups' => [2],
];
$user = new User;
if (!$user->bind($data)) {
$joomUser = new User;
if (!$joomUser->bind($data)) {
$this->log('user-bind-failed', ['error' => 'bind() returned false']);
return;
}
$user->save();
if (!$joomUser->save()) {
$this->log('user-save-failed', ['error' => 'save() returned false']);
return;
}
$this->log('user-created', ['id' => $joomUser->id, 'username' => $user]);
}
}

View File

@@ -1,6 +1,6 @@
- name: "Render LDAP CLI helper"
template:
src: cli-ldap.php.j2
src: ldap/cli.php.j2
dest: "{{ JOOMLA_LDAP_CONF_FILE }}"
mode: "0644"
when: JOOMLA_LDAP_ENABLED | bool
@@ -23,3 +23,24 @@
- php
- xml
when: JOOMLA_LDAP_AUTO_CREATE_ENABLED | bool
- name: "Deploy LDAP diagnose CLI"
template:
src: ldap/diagnose.php.j2
dest: "{{ docker_compose.directories.volumes }}/cli-ldap-diagnose.php"
mode: "0644"
when: MODE_DEBUG | bool
- name: "Deploy Joomla plugin inspector CLI (list state)"
template:
src: ldap/plugins.php.j2
dest: "{{ docker_compose.directories.volumes }}/cli-plugins.php"
mode: "0644"
when: MODE_DEBUG | bool
- name: "Deploy Joomla auth trace CLI"
template:
src: ldap/auth-trace.php.j2
dest: "{{ docker_compose.directories.volumes }}/cli-ldap-auth-trace.php"
mode: "0644"
when: MODE_DEBUG | bool

View File

@@ -6,7 +6,6 @@
changed_when: "'configured' in ldap_conf.stdout | lower"
async: "{{ ASYNC_TIME if ASYNC_ENABLED | bool else omit }}"
poll: "{{ ASYNC_POLL if ASYNC_ENABLED | bool else omit }}"
when: JOOMLA_LDAP_ENABLED | bool
- name: "Register & enable ldapautocreate Joomla system plugin"
command: >

View File

@@ -2,4 +2,3 @@
command:
argv: [ docker, exec, "{{ JOOMLA_CONTAINER }}", php, "-l", "{{ JOOMLA_CONFIG_FILE }}" ]
changed_when: false
when: MODE_ASSERT | bool

View File

@@ -0,0 +1,14 @@
- name: "Run LDAP diagnose"
command:
argv:
- docker
- exec
- "{{ JOOMLA_CONTAINER }}"
- php
- /var/www/html/cli/ldap-diagnose.php
- "--username={{ users.administrator.username }}"
register: ldap_diag
changed_when: false
- debug:
var: ldap_diag.stdout_lines

View File

@@ -26,5 +26,15 @@
- 02_install.yml
- 03_debug.yml
- 04_patch.yml
- 05_ldap.yml
- 06_assert.yml
- name: Include LDAP routines
include_tasks: "05_ldap.yml"
when: JOOMLA_LDAP_ENABLED | bool
- name: Include assert routines
include_tasks: "06_assert.yml"
when: MODE_ASSERT | bool
- name: Include LDAP diagnose routines
include_tasks: "07_diagnose.yml"
when: MODE_DEBUG | bool and JOOMLA_LDAP_ENABLED | bool

View File

@@ -14,6 +14,11 @@
{% if JOOMLA_LDAP_AUTO_CREATE_ENABLED | bool %}
- {{ JOOMLA_LDAP_AUT_CRT_HOST_DIR }}:{{ JOOMLA_LDAP_AUT_CRT_DOCK_DIR }}:ro
{% endif %}
{% if MODE_DEBUG | bool %}
- {{ JOOMLA_LDAP_DIAG_HOST_FILE }}:{{ JOOMLA_LDAP_DIAG_DOCK_FILE }}:ro
- {{ docker_compose.directories.volumes }}/cli-plugins.php:/var/www/html/cli/cli-plugins.php:ro
- {{ docker_compose.directories.volumes }}/cli-ldap-auth-trace.php:/var/www/html/cli/ldap-auth-trace.php:ro
{% endif %}
{% endif %}
ports:
- "127.0.0.1:{{ ports.localhost.http[application_id] }}:{{ container_port }}"

View File

@@ -15,6 +15,7 @@ JOOMLA_DB_TYPE={{ JOOMLA_DB_CONNECTOR }}
{% if JOOMLA_LDAP_ENABLED %}
# LDAP
JOOMLA_LDAP_AUTOCREATE_LOG={{ MODE_DEBUG | ternary('1','') }}
JOOMLA_LDAP_HOST={{ JOOMLA_LDAP_HOST }}
JOOMLA_LDAP_PORT={{ JOOMLA_LDAP_PORT }}
JOOMLA_LDAP_BASE_DN={{ JOOMLA_LDAP_BASE_DN }}

View File

@@ -0,0 +1,242 @@
<?php
/**
* CLI: LDAP auth trace (plugin-accurate, without booting Joomla plugins)
*
* This script reads Joomla's "Authentication - LDAP" plugin parameters from #__extensions
* and then performs the same login flow the plugin would:
* - Connect → optional StartTLS → bind (service) → search user → bind as user with given password
*
* Usage:
* php /var/www/html/cli/ldap-auth-trace.php --username <u> --password '<p>' [--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 <u> --password '<p>' [--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) );

View File

@@ -0,0 +1,194 @@
<?php
/**
* Joomla LDAP Diagnostic CLI
*
* Usage inside the container:
* php /var/www/html/cli/ldap-diagnose.php --username <uid> [--verbose]
*
* Behavior:
* - Loads the "Authentication - LDAP" plugin parameters from #__extensions.
* - Allows ENV overrides for any LDAP setting (see list below).
* - Checks step-by-step: PHP-LDAP → connect → (optional) StartTLS → bind → search → read attributes.
* - Optionally previews the candidate DN for user-bind when auth_method=bind (no password tested).
*
* Supported ENV overrides (optional):
* JOOMLA_LDAP_HOST
* JOOMLA_LDAP_PORT
* JOOMLA_LDAP_BASE_DN
* JOOMLA_LDAP_USER_TREE_DN
* JOOMLA_LDAP_BIND_DN
* JOOMLA_LDAP_BIND_PASSWORD
* JOOMLA_LDAP_USE_STARTTLS (true/false)
* JOOMLA_LDAP_AUTH_METHOD ("search" or "bind")
* JOOMLA_LDAP_USER_SEARCH_STRING (e.g., "uid=[username]")
* JOOMLA_LDAP_UID_ATTR (e.g., "uid")
* JOOMLA_LDAP_EMAIL_ATTR (e.g., "mail")
* JOOMLA_LDAP_NAME_ATTR (e.g., "cn")
* TEST_USERNAME (fallback if --username is not passed)
*/
define('_JEXEC', 1);
define('JPATH_BASE', __DIR__ . '/..');
require JPATH_BASE . '/includes/defines.php';
require JPATH_BASE . '/includes/framework.php';
use Joomla\CMS\Factory;
function println($msg, ?bool $ok = null, int $indent = 0): void {
$pad = str_repeat(' ', $indent);
if ($ok === true) {
echo $pad . "[OK] $msg\n";
} elseif ($ok === false) {
echo $pad . "[ERR] $msg\n";
} else {
echo $pad . "$msg\n";
}
}
function getenv_clean(string $key, $default = null) {
$v = getenv($key);
if ($v === false || $v === '') return $default;
// Strip surrounding single/double quotes if present
return preg_replace('/^(["\'])(.*)\1$/', '$2', $v);
}
function env_bool($key, $default = false): bool {
$v = getenv_clean($key, null);
if ($v === null) return (bool)$default;
return filter_var($v, FILTER_VALIDATE_BOOL);
}
// ---- CLI args
$args = getopt('', ['username:', 'verbose']);
$username = $args['username'] ?? getenv_clean('TEST_USERNAME', null);
$verbose = array_key_exists('verbose', $args);
// ---- Preflight: php-ldap
if (!extension_loaded('ldap')) {
println('PHP LDAP extension not loaded (php-ldap missing in the image).', false);
exit(1);
}
println('PHP LDAP extension: loaded', true);
// ---- 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) {
println('LDAP plugin row not found in #__extensions (authentication/ldap).', false);
exit(2);
}
$params = json_decode($ext->params ?: "{}", true) ?: [];
// ---- Effective config (DB params with ENV overrides)
$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' => env_bool('JOOMLA_LDAP_USE_STARTTLS', $params['negotiate_tls'] ?? false),
'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'),
];
if (!$username) {
println('Missing username. Provide --username <uid> or set TEST_USERNAME.', false);
exit(3);
}
// ---- Print effective config (mask password)
$cfg_to_show = $cfg;
$cfg_to_show['bind_pw'] = ($cfg['bind_pw'] !== '') ? '***' : '';
println('Effective LDAP configuration:');
foreach ($cfg_to_show as $k => $v) {
println("$k: $v", null, 1);
}
// ---- Connect
$ds = @ldap_connect($cfg['host'], $cfg['port']);
if (!$ds) {
println("ldap_connect to {$cfg['host']}:{$cfg['port']} failed.", false);
exit(4);
}
ldap_set_option($ds, LDAP_OPT_PROTOCOL_VERSION, 3);
ldap_set_option($ds, LDAP_OPT_NETWORK_TIMEOUT, 5);
println("ldap_connect to {$cfg['host']}:{$cfg['port']}", true);
// ---- Optional StartTLS
if ($cfg['use_tls']) {
if (@ldap_start_tls($ds)) {
println('StartTLS negotiated', true);
} else {
println('StartTLS failed: ' . ldap_error($ds), false);
exit(5);
}
} else {
println('StartTLS: disabled');
}
// ---- Service/Admin bind
if ($cfg['bind_dn'] !== '') {
if (@ldap_bind($ds, $cfg['bind_dn'], $cfg['bind_pw'])) {
println("Bind as {$cfg['bind_dn']}", true);
} else {
println("Bind failed for {$cfg['bind_dn']}: " . ldap_error($ds), false);
exit(6);
}
} else {
// Anonymous bind
if (@ldap_bind($ds)) {
println('Anonymous bind', true);
} else {
println('Anonymous bind failed: ' . ldap_error($ds), false);
exit(6);
}
}
// ---- Search user entry
$filter = sprintf('(%s=%s)', $cfg['uid_attr'], ldap_escape($username, '', LDAP_ESCAPE_FILTER));
$attrs = [$cfg['uid_attr'], $cfg['mail_attr'], $cfg['name_attr']];
$sr = @ldap_search($ds, $cfg['base_dn'], $filter, $attrs);
if (!$sr) {
println("Search failed under base_dn={$cfg['base_dn']} filter={$filter}: " . ldap_error($ds), false);
exit(7);
}
$entries = @ldap_get_entries($ds, $sr);
$count = (int)($entries['count'] ?? 0);
println("Search entries returned: $count", $count > 0);
if ($count < 1) {
println('No entry found. Check base_dn, uid_attr, filter, and where the user actually lives.', false, 1);
@ldap_unbind($ds);
exit(8);
}
// ---- Print first entry summary
$dn = $entries[0]['dn'] ?? '(unknown DN)';
$name = $entries[0][strtolower($cfg['name_attr'])][0] ?? '(missing name attribute)';
$mail = $entries[0][strtolower($cfg['mail_attr'])][0] ?? '(missing mail attribute)';
println("Found DN: $dn", true, 1);
println("Name: $name", null, 1);
println("Mail: $mail", null, 1);
// ---- If auth_method=bind, preview candidate DN (no password test here)
if (strtolower($cfg['auth_method']) === 'bind') {
$candidate = str_replace(['[username]', '[USER]', '[uid]'], $username, $cfg['users_dn']);
println("auth_method=bind → candidate DN for user-bind: $candidate");
println("Note: This script does not attempt the real user password bind.", null, 1);
}
@ldap_unbind($ds);
println('Diagnosis finished: basic LDAP connectivity and search are OK.', true);
exit(0);

View File

@@ -0,0 +1,59 @@
<?php
/**
* Joomla CLI: List plugin states from #__extensions
*
* Usage:
* php /var/www/html/cli/cli-plugins.php [filter]
*
* Example:
* php cli-plugins.php ldap
* php cli-plugins.php authentication
*/
define('_JEXEC', 1);
define('JPATH_BASE', __DIR__ . '/..');
require JPATH_BASE . '/includes/defines.php';
require JPATH_BASE . '/includes/framework.php';
use Joomla\CMS\Factory;
$dbo = Factory::getDbo();
// Optional filter argument
$filter = $argv[1] ?? null;
// Build query
$query = $dbo->getQuery(true)
->select('*')
->from($dbo->quoteName('#__extensions'))
->where($dbo->quoteName('type') . ' = ' . $dbo->quote('plugin'));
if ($filter) {
$query->where(
'(' .
$dbo->quoteName('element') . ' LIKE ' . $dbo->quote("%$filter%") . ' OR ' .
$dbo->quoteName('folder') . ' LIKE ' . $dbo->quote("%$filter%") .
')'
);
}
$dbo->setQuery($query);
$rows = $dbo->loadObjectList();
if (!$rows) {
echo "No plugins found.\n";
exit(0);
}
foreach ($rows as $row) {
printf(
"[%s/%s] enabled=%d ordering=%d access=%d\n params=%s\n\n",
$row->folder,
$row->element,
$row->enabled,
$row->ordering,
$row->access,
$row->params ?: '{}'
);
}

View File

@@ -42,3 +42,7 @@ JOOMLA_LDAP_AUTH_METHOD: "search" # "bind" or "search"
JOOMLA_LDAP_USER_SEARCH_STRING: "{{ JOOMLA_LDAP_UID_ATTR }}=[username]"
JOOMLA_LDAP_AUT_CRT_HOST_DIR: "{{ [ docker_compose.directories.volumes, 'ldapautocreate' ] | path_join }}"
JOOMLA_LDAP_AUT_CRT_DOCK_DIR: "/var/www/html/plugins/system/ldapautocreate"
# Diagnose
JOOMLA_LDAP_DIAG_HOST_FILE: "{{ [ docker_compose.directories.volumes, 'cli-ldap-diagnose.php' ] | path_join }}"
JOOMLA_LDAP_DIAG_DOCK_FILE: "/var/www/html/cli/ldap-diagnose.php"