diff --git a/roles/web-app-joomla/config/main.yml b/roles/web-app-joomla/config/main.yml
index 90b9040e..2011d2a7 100644
--- a/roles/web-app-joomla/config/main.yml
+++ b/roles/web-app-joomla/config/main.yml
@@ -5,7 +5,8 @@ features:
desktop: true
central_database: true
logout: true
- ldap: true
+ ldap: false # There is no working free open source LDAP solution 2025-08-28
+ oidc: false # There is no working free open source OIDC solution 2025-08-28
server:
domains:
canonical:
diff --git a/roles/web-app-joomla/files/ldapautocreate.php b/roles/web-app-joomla/files/ldapautocreate.php
deleted file mode 100644
index 2049d68d..00000000
--- a/roles/web-app-joomla/files/ldapautocreate.php
+++ /dev/null
@@ -1,158 +0,0 @@
-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');
- }
-
- /**
- * Fires after authentication handlers; frontend and backend.
- * @param array $options
- * @param object $response ->status, ->type, ->error_message, ->username, etc.
- */
- public function onUserAfterAuthenticate($options, $response)
- {
- // 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;
- }
-
- if (!$user) {
- $this->log('skip-missing-username');
- return;
- }
-
- // If user exists locally, nothing to do
- $dbo = Factory::getDbo();
- $count = (int) $dbo->setQuery(
- $dbo->getQuery(true)
- ->select('COUNT(*)')
- ->from($dbo->quoteName('#__users'))
- ->where($dbo->quoteName('username') . ' = ' . $dbo->quote($user))
- )->loadResult();
-
- if ($count > 0) {
- $this->log('user-exists', ['username' => $user]);
- return;
- }
-
- // 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("type='plugin' AND folder='authentication' AND element='ldap'")
- )->loadObject();
-
- if (!$ldapExt) {
- $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';
- $attrMail = $p['ldap_email'] ?? 'mail';
- $attrName = $p['ldap_fullname'] ?? 'cn';
-
- $this->log('ldap-params', [
- 'host' => $host, 'port' => $port, 'base_dn' => $baseDn,
- 'attrUid' => $attrUid, 'attrMail' => $attrMail, 'attrName' => $attrName,
- ]);
-
- $ds = @ldap_connect($host, $port);
- 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($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] ?? $user) : $user;
- $email = $entry ? (@ldap_get_values($ds, $entry, $attrMail)[0] ?? ($user.'@example.invalid')) : ($user.'@example.invalid');
-
- if ($ds) { @ldap_unbind($ds); }
-
- $this->log('creating-user', ['username' => $user, 'name' => $name, 'email' => $email]);
-
- // Create Joomla user in Registered (id=2)
- $data = [
- 'name' => $name,
- 'username' => $user,
- 'email' => $email,
- 'password' => bin2hex(random_bytes(12)),
- 'block' => 0,
- 'groups' => [2],
- ];
-
- $joomUser = new User;
- if (!$joomUser->bind($data)) {
- $this->log('user-bind-failed', ['error' => 'bind() returned false']);
- return;
- }
-
- if (!$joomUser->save()) {
- $this->log('user-save-failed', ['error' => 'save() returned false']);
- return;
- }
-
- $this->log('user-created', ['id' => $joomUser->id, 'username' => $user]);
- }
-}
diff --git a/roles/web-app-joomla/files/ldapautocreate.xml b/roles/web-app-joomla/files/ldapautocreate.xml
deleted file mode 100644
index 70892334..00000000
--- a/roles/web-app-joomla/files/ldapautocreate.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
' [--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) );
diff --git a/roles/web-app-joomla/templates/ldap/cli.php.j2 b/roles/web-app-joomla/templates/ldap/cli.php.j2
deleted file mode 100644
index 65e26e8b..00000000
--- a/roles/web-app-joomla/templates/ldap/cli.php.j2
+++ /dev/null
@@ -1,68 +0,0 @@
-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($query);
-$ext = $dbo->loadObject();
-
-if (!$ext) {
- fwrite(STDERR, "LDAP plugin not found.\n");
- exit(2);
-}
-
-// Helper to strip quotes if present in env-file values
-$get = static fn($k) => preg_replace('/^(["\'])(.*)\1$/', '$2', getenv($k) ?: '');
-
-// Desired plugin parameters (must match Joomla LDAP plugin schema)
-$desired = [
- // Connection settings
- "host" => $get('JOOMLA_LDAP_HOST'),
- "port" => (int) $get('JOOMLA_LDAP_PORT'),
- "use_ldapV3" => true,
- "negotiate_tls" => (bool) $get('JOOMLA_LDAP_USE_STARTTLS'),
- "no_referrals" => false,
-
- // Authentication settings
- "auth_method" => $get('JOOMLA_LDAP_AUTH_METHOD') ?: "search", // "search" or "bind"
- "base_dn" => $get('JOOMLA_LDAP_BASE_DN'),
- "search_string" => $get('JOOMLA_LDAP_USER_SEARCH_STRING'), // e.g. uid=[username]
- "users_dn" => $get('JOOMLA_LDAP_USER_TREE_DN'), // required for "bind" mode
- "username" => $get('JOOMLA_LDAP_BIND_DN'),
- "password" => $get('JOOMLA_LDAP_BIND_PASSWORD'),
-
- // Attribute mapping
- "ldap_uid" => $get('JOOMLA_LDAP_UID_ATTR') ?: "uid",
- "ldap_email" => $get('JOOMLA_LDAP_EMAIL_ATTR') ?: "mail",
- "ldap_fullname" => $get('JOOMLA_LDAP_NAME_ATTR') ?: "cn",
-];
-
-// Merge current parameters with desired values
-$current = json_decode($ext->params ?: "{}", true) ?: [];
-$clean = array_filter($desired, static fn($v) => $v !== null && $v !== '');
-$merged = array_replace($current, $clean);
-
-// Save back to database and enable the plugin
-$ext->params = json_encode($merged, JSON_UNESCAPED_SLASHES);
-$ext->enabled = 1;
-$dbo->updateObject('#__extensions', $ext, 'extension_id');
-
-echo "LDAP plugin enabled={$ext->enabled} and configured.\n";
diff --git a/roles/web-app-joomla/templates/ldap/diagnose.php.j2 b/roles/web-app-joomla/templates/ldap/diagnose.php.j2
deleted file mode 100644
index 69ce6271..00000000
--- a/roles/web-app-joomla/templates/ldap/diagnose.php.j2
+++ /dev/null
@@ -1,194 +0,0 @@
- [--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