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 @@ - - - plg_system_ldapautocreate - Infinito.Nexus - 1.0.0 - Auto-create Joomla users after successful LDAP authentication. - - ldapautocreate.php - - diff --git a/roles/web-app-joomla/tasks/02_install.yml b/roles/web-app-joomla/tasks/01_install.yml similarity index 100% rename from roles/web-app-joomla/tasks/02_install.yml rename to roles/web-app-joomla/tasks/01_install.yml diff --git a/roles/web-app-joomla/tasks/01_ldap_files.yml b/roles/web-app-joomla/tasks/01_ldap_files.yml deleted file mode 100644 index 9a3b6845..00000000 --- a/roles/web-app-joomla/tasks/01_ldap_files.yml +++ /dev/null @@ -1,46 +0,0 @@ -- name: "Render LDAP CLI helper" - template: - src: ldap/cli.php.j2 - dest: "{{ JOOMLA_LDAP_CONF_FILE }}" - mode: "0644" - when: JOOMLA_LDAP_ENABLED | bool - notify: docker compose restart - -- block: - - name: "Ensure ldapautocreate plugin hostdir exists" - file: - path: "{{ JOOMLA_LDAP_AUT_CRT_HOST_DIR }}" - state: directory - mode: "0755" - - - name: "Deploy ldapautocreate plugin files" - copy: - src: "ldapautocreate.{{ item }}" - dest: "{{ [ JOOMLA_LDAP_AUT_CRT_HOST_DIR, 'ldapautocreate.' ~ item ] | path_join }}" - mode: "0644" - notify: docker compose restart - loop: - - 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 diff --git a/roles/web-app-joomla/tasks/03_debug.yml b/roles/web-app-joomla/tasks/02_debug.yml similarity index 100% rename from roles/web-app-joomla/tasks/03_debug.yml rename to roles/web-app-joomla/tasks/02_debug.yml diff --git a/roles/web-app-joomla/tasks/04_patch.yml b/roles/web-app-joomla/tasks/03_patch.yml similarity index 100% rename from roles/web-app-joomla/tasks/04_patch.yml rename to roles/web-app-joomla/tasks/03_patch.yml diff --git a/roles/web-app-joomla/tasks/06_assert.yml b/roles/web-app-joomla/tasks/04_assert.yml similarity index 100% rename from roles/web-app-joomla/tasks/06_assert.yml rename to roles/web-app-joomla/tasks/04_assert.yml diff --git a/roles/web-app-joomla/tasks/05_ldap.yml b/roles/web-app-joomla/tasks/05_ldap.yml deleted file mode 100644 index 22790f97..00000000 --- a/roles/web-app-joomla/tasks/05_ldap.yml +++ /dev/null @@ -1,55 +0,0 @@ -- name: "Configure LDAP plugin params via helper" - command: > - docker exec {{ JOOMLA_CONTAINER }} - sh -c 'test -f /var/www/html/cli/cli-ldap.php && php /var/www/html/cli/cli-ldap.php' - register: ldap_conf - 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 }}" - -- name: "Register & enable ldapautocreate Joomla system plugin" - command: > - docker exec {{ JOOMLA_CONTAINER }} - sh -lc ' - test -f /var/www/html/plugins/system/ldapautocreate/ldapautocreate.php || - { echo "ERROR: plugin file missing"; exit 1; }; - php -r " - define(\"_JEXEC\",1); - \$root=\"/var/www/html\"; - require \$root.\"/includes/defines.php\"; - require \$root.\"/includes/framework.php\"; - \$dbo = Joomla\\CMS\\Factory::getDbo(); - \$ext = \$dbo->setQuery( - \"SELECT * FROM #__extensions WHERE type=\\\"plugin\\\" AND folder=\\\"system\\\" AND element=\\\"ldapautocreate\\\"\" - )->loadObject(); - if (!\$ext) { - \$row = (object)[ - \"name\" => \"plg_system_ldapautocreate\", - \"type\" => \"plugin\", - \"element\" => \"ldapautocreate\", - \"folder\" => \"system\", - \"enabled\" => 1, - \"access\" => 1, - \"protected\" => 0, - \"manifest_cache\" => \"{}\", - \"params\" => \"{}\", - \"custom_data\" => \"{}\", - \"state\" => 0, - \"ordering\" => 0, - \"client_id\" => 0 - ]; - \$dbo->insertObject(\"#__extensions\", \$row); - echo \"Plugin registered + enabled\\n\"; - } else { - \$ext->enabled = 1; - \$dbo->updateObject(\"#__extensions\", \$ext, \"extension_id\"); - echo \"Plugin already exists, just enabled\\n\"; - } - " - ' - register: ldapautocreate_reg - changed_when: > - ('registered + enabled' in (ldapautocreate_reg.stdout | lower)) or - ('just enabled' in (ldapautocreate_reg.stdout | lower)) - failed_when: ldapautocreate_reg.rc != 0 - when: JOOMLA_LDAP_AUTO_CREATE_ENABLED | bool \ No newline at end of file diff --git a/roles/web-app-joomla/tasks/07_diagnose.yml b/roles/web-app-joomla/tasks/07_diagnose.yml deleted file mode 100644 index c5597ec6..00000000 --- a/roles/web-app-joomla/tasks/07_diagnose.yml +++ /dev/null @@ -1,14 +0,0 @@ -- 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 diff --git a/roles/web-app-joomla/tasks/main.yml b/roles/web-app-joomla/tasks/main.yml index 8579d2a4..8892fd84 100644 --- a/roles/web-app-joomla/tasks/main.yml +++ b/roles/web-app-joomla/tasks/main.yml @@ -1,40 +1,26 @@ --- -#- name: "Include role srv-domain-provision for {{ application_id }}" -# include_role: -# name: srv-domain-provision -# loop: "{{ JOOMLA_DOMAINS }}" -# loop_control: -# loop_var: domain -# vars: -# http_port: "{{ ports.localhost.http[application_id] }}" +- name: "Include role srv-domain-provision for {{ application_id }}" + include_role: + name: srv-domain-provision + loop: "{{ JOOMLA_DOMAINS }}" + loop_control: + loop_var: domain + vars: + http_port: "{{ ports.localhost.http[application_id] }}" - name: "load docker and db for {{ application_id }}" include_role: name: cmp-db-docker vars: - docker_compose_flush_handlers: false - -- name: Include install routines - include_tasks: "01_ldap_files.yml" - -- name: "flush docker compose handlers" - meta: flush_handlers + docker_compose_flush_handlers: true - name: Include install routines include_tasks: "{{ item }}" loop: - - 02_install.yml - - 03_debug.yml - - 04_patch.yml - -- name: Include LDAP routines - include_tasks: "05_ldap.yml" - when: JOOMLA_LDAP_ENABLED | bool + - 01_install.yml + - 02_debug.yml + - 03_patch.yml - name: Include assert routines - include_tasks: "06_assert.yml" + include_tasks: "04_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 \ No newline at end of file diff --git a/roles/web-app-joomla/templates/Dockerfile.j2 b/roles/web-app-joomla/templates/Dockerfile.j2 index 0770d5d4..b780da8b 100644 --- a/roles/web-app-joomla/templates/Dockerfile.j2 +++ b/roles/web-app-joomla/templates/Dockerfile.j2 @@ -1,15 +1 @@ FROM {{ JOOMLA_IMAGE }}:{{ JOOMLA_VERSION }} -{% if JOOMLA_LDAP_ENABLED %} -ENV DEBIAN_FRONTEND=noninteractive -RUN set -eux; \ - apt-get update; \ - PHPV="$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;')" || PHPV=""; \ - apt-get install -y --no-install-recommends "php${PHPV}-ldap" \ - || ( \ - apt-get install -y --no-install-recommends libldap2-dev libsasl2-dev pkg-config; \ - docker-php-ext-configure ldap --with-ldap=/usr --with-ldap-sasl=/usr \ - || docker-php-ext-configure ldap --with-ldap=/usr; \ - docker-php-ext-install -j"$(nproc)" ldap \ - ); \ - rm -rf /var/lib/apt/lists/* -{% endif %} diff --git a/roles/web-app-joomla/templates/docker-compose.yml.j2 b/roles/web-app-joomla/templates/docker-compose.yml.j2 index 33ac66af..8298ef27 100644 --- a/roles/web-app-joomla/templates/docker-compose.yml.j2 +++ b/roles/web-app-joomla/templates/docker-compose.yml.j2 @@ -9,17 +9,6 @@ {% include 'roles/docker-container/templates/base.yml.j2' %} volumes: - data:/var/www/html -{% if JOOMLA_LDAP_ENABLED %} - - {{ JOOMLA_LDAP_CONF_FILE }}:/var/www/html/cli/cli-ldap.php:ro -{% 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 }}" {% include 'roles/docker-container/templates/healthcheck/curl.yml.j2' %} diff --git a/roles/web-app-joomla/templates/env.j2 b/roles/web-app-joomla/templates/env.j2 index 53a51909..407ed9d8 100644 --- a/roles/web-app-joomla/templates/env.j2 +++ b/roles/web-app-joomla/templates/env.j2 @@ -12,24 +12,3 @@ JOOMLA_DB_PASSWORD={{ database_password }} JOOMLA_DB_NAME={{ database_name }} JOOMLA_DB_TYPE={{ JOOMLA_DB_CONNECTOR }} {% endif %} - -{% 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 }} -JOOMLA_LDAP_USER_TREE_DN={{ JOOMLA_LDAP_USER_TREE_DN }} -JOOMLA_LDAP_GROUP_TREE_DN={{ JOOMLA_LDAP_GROUP_TREE_DN }} -JOOMLA_LDAP_UID_ATTR={{ JOOMLA_LDAP_UID_ATTR }} -JOOMLA_LDAP_EMAIL_ATTR={{ JOOMLA_LDAP_EMAIL_ATTR }} -JOOMLA_LDAP_NAME_ATTR={{ JOOMLA_LDAP_NAME_ATTR }} -JOOMLA_LDAP_BIND_DN={{ JOOMLA_LDAP_BIND_DN }} -JOOMLA_LDAP_BIND_PASSWORD={{ JOOMLA_LDAP_BIND_PASSWORD }} -JOOMLA_LDAP_USE_STARTTLS={{ JOOMLA_LDAP_USE_STARTTLS | ternary('1','') }} -JOOMLA_LDAP_IGNORE_CERT={{ JOOMLA_LDAP_IGNORE_CERT | ternary('1','') }} -JOOMLA_LDAP_MAP_FULLNAME={{ JOOMLA_LDAP_MAP_FULLNAME | ternary('1','') }} -JOOMLA_LDAP_MAP_EMAIL={{ JOOMLA_LDAP_MAP_EMAIL | ternary('1','') }} -JOOMLA_LDAP_AUTH_METHOD={{ JOOMLA_LDAP_AUTH_METHOD }} -JOOMLA_LDAP_USER_SEARCH_STRING={{ JOOMLA_LDAP_USER_SEARCH_STRING }} -{% endif %} \ No newline at end of file diff --git a/roles/web-app-joomla/templates/ldap/auth-trace.php.j2 b/roles/web-app-joomla/templates/ldap/auth-trace.php.j2 deleted file mode 100644 index 70d19cfe..00000000 --- a/roles/web-app-joomla/templates/ldap/auth-trace.php.j2 +++ /dev/null @@ -1,242 +0,0 @@ - --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) ); 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 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); diff --git a/roles/web-app-joomla/templates/ldap/plugins.php.j2 b/roles/web-app-joomla/templates/ldap/plugins.php.j2 deleted file mode 100644 index de25ccb8..00000000 --- a/roles/web-app-joomla/templates/ldap/plugins.php.j2 +++ /dev/null @@ -1,59 +0,0 @@ -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 ?: '{}' - ); -} diff --git a/roles/web-app-joomla/vars/main.yml b/roles/web-app-joomla/vars/main.yml index 6aca966c..c11aba3e 100644 --- a/roles/web-app-joomla/vars/main.yml +++ b/roles/web-app-joomla/vars/main.yml @@ -19,30 +19,3 @@ JOOMLA_USER_NAME: "{{ users.administrator.username }}" JOOMLA_USER: "{{ JOOMLA_USER_NAME | capitalize }}" JOOMLA_USER_PASSWORD: "{{ users.administrator.password }}" JOOMLA_USER_EMAIL: "{{ users.administrator.email }}" - -# LDAP -JOOMLA_LDAP_CONF_FILE: "{{ [ docker_compose.directories.volumes, 'cli-ldap.php' ] | path_join }}" -JOOMLA_LDAP_ENABLED: "{{ applications | get_app_conf(application_id, 'features.ldap') }}" -JOOMLA_LDAP_AUTO_CREATE_ENABLED: "{{ applications | get_app_conf(application_id, 'autocreate_users') }}" -JOOMLA_LDAP_HOST: "{{ LDAP.SERVER.DOMAIN }}" -JOOMLA_LDAP_PORT: "{{ LDAP.SERVER.PORT }}" -JOOMLA_LDAP_BASE_DN: "{{ LDAP.DN.ROOT }}" -JOOMLA_LDAP_USER_TREE_DN: "{{ LDAP.DN.OU.USERS }}" -JOOMLA_LDAP_GROUP_TREE_DN: "{{ LDAP.DN.OU.GROUPS }}" -JOOMLA_LDAP_UID_ATTR: "{{ LDAP.USER.ATTRIBUTES.ID }}" # e.g. uid -JOOMLA_LDAP_EMAIL_ATTR: "{{ LDAP.USER.ATTRIBUTES.MAIL }}" -JOOMLA_LDAP_NAME_ATTR: "{{ LDAP.USER.ATTRIBUTES.FULLNAME }}" -JOOMLA_LDAP_BIND_DN: "{{ LDAP.DN.ADMINISTRATOR.DATA }}" -JOOMLA_LDAP_BIND_PASSWORD: "{{ LDAP.BIND_CREDENTIAL }}" -JOOMLA_LDAP_USE_STARTTLS: false -JOOMLA_LDAP_IGNORE_CERT: true -JOOMLA_LDAP_MAP_FULLNAME: true -JOOMLA_LDAP_MAP_EMAIL: true -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"