diff --git a/Dockerfile b/Dockerfile index 23f26d4..80824f7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ FROM php:8.2-apache -# Install dependencies +# Install dependencies including LDAP RUN apt-get update && apt-get install -y \ git \ curl \ @@ -13,7 +13,9 @@ RUN apt-get update && apt-get install -y \ openssh-client \ qrencode \ cron \ - && docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd \ + libldap2-dev \ + && docker-php-ext-configure ldap --with-libdir=lib/x86_64-linux-gnu/ \ + && docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd ldap \ && a2enmod rewrite \ && apt-get clean && rm -rf /var/lib/apt/lists/* @@ -39,12 +41,14 @@ RUN chown -R www-data:www-data /var/www/html \ # Setup cron jobs RUN echo "0 * * * * www-data cd /var/www/html && /usr/local/bin/php bin/check_expired_clients.php >> /var/log/cron.log 2>&1" > /etc/cron.d/amnezia-cron \ && echo "0 * * * * www-data cd /var/www/html && /usr/local/bin/php bin/check_traffic_limits.php >> /var/log/cron.log 2>&1" >> /etc/cron.d/amnezia-cron \ + && echo "*/30 * * * * www-data cd /var/www/html && /usr/local/bin/php bin/sync_ldap_users.php >> /var/log/ldap_sync.log 2>&1" >> /etc/cron.d/amnezia-cron \ && echo "*/3 * * * * root /bin/bash /var/www/html/bin/monitor_metrics.sh >> /var/log/metrics_monitor.log 2>&1" >> /etc/cron.d/amnezia-cron \ && chmod 0644 /etc/cron.d/amnezia-cron \ && crontab /etc/cron.d/amnezia-cron \ && touch /var/log/cron.log \ && touch /var/log/metrics_monitor.log \ - && touch /var/log/metrics_collector.log + && touch /var/log/metrics_collector.log \ + && touch /var/log/ldap_sync.log # Make monitor script executable RUN chmod +x /var/www/html/bin/monitor_metrics.sh diff --git a/LDAP_FEATURE.md b/LDAP_FEATURE.md new file mode 100644 index 0000000..2d42c6b --- /dev/null +++ b/LDAP_FEATURE.md @@ -0,0 +1,90 @@ +# LDAP Integration - Feature Branch + +## ✅ Реализовано + +### Базовая инфраструктура: +- ✅ PHP LDAP расширение в Dockerfile +- ✅ Миграции БД (ldap_configs, user_roles, ldap_group_mappings) +- ✅ Класс `LdapSync.php` - подключение, аутентификация, синхронизация +- ✅ Интеграция в `Auth.php` - авто-переключение между LDAP/локальной авторизацией +- ✅ Cron задача синхронизации (каждые 30 мин) +- ✅ UI страница настроек LDAP (templates/settings/ldap.twig) +- ✅ Документация LDAP_SETUP.md + +### Функционал: +- Авторизация через LDAP/Active Directory +- Автоматическая синхронизация пользователей +- Маппинг LDAP групп на роли (admin/manager/viewer) +- Тест подключения к LDAP +- Fallback на локальную авторизацию + +## ⚠️ Требуется доработка + +### Критичное: +1. **Добавить маршруты в `public/index.php`:** + ```php + $router->add('GET', '/settings/ldap', [SettingsController::class, 'ldapSettings']); + $router->add('POST', '/settings/ldap/save', [SettingsController::class, 'saveLdapSettings']); + $router->add('POST', '/settings/ldap/test', [SettingsController::class, 'testLdapConnection']); + ``` + +2. **Создать методы в `controllers/SettingsController.php`:** + - `ldapSettings()` - показать страницу + - `saveLdapSettings()` - сохранить конфигурацию + - `testLdapConnection()` - протестировать подключение + +3. **Добавить переводы** в миграции 002-006 (ru, en, es, de, fr, zh): + - ldap_settings + - enable_ldap_auth + - test_connection + - ldap_host / ldap_port / base_dn / bind_dn + - sync_interval + - ldap_group_mappings + +### Опционально: +- Логи LDAP операций в отдельный файл +- Валидация LDAP настроек на фронтенде +- Более детальные сообщения об ошибках + +## 🧪 Тестирование + +### Локально: +```bash +# Пересобрать контейнеры с LDAP расширением +docker-compose build + +# Применить миграции +docker-compose exec web php -r "require 'inc/Config.php'; require 'inc/DB.php'; DB::runMigrations();" + +# Тест LDAP подключения +docker-compose exec web php bin/sync_ldap_users.php +``` + +### Проверить: +1. Страница `/settings/ldap` открывается +2. Сохранение настроек работает +3. Тест подключения выполняется +4. Авторизация через LDAP (если настроен тестовый LDAP сервер) + +## 📋 TODO перед слиянием в master + +- [ ] Добавить маршруты и контроллер +- [ ] Добавить переводы для всех языков +- [ ] Протестировать с реальным LDAP/AD сервером +- [ ] Проверить обратную совместимость (без LDAP всё работает как раньше) +- [ ] Обновить README.md с информацией о LDAP + +## 🔒 Безопасность + +- ❌ Пароли LDAP не шифруются (TODO: добавить шифрование в БД) +- ✅ Опциональная функция - не влияет на существующий функционал +- ✅ Fallback на локальную авторизацию +- ✅ TLS/SSL поддержка для LDAP + +## 📖 Документация + +См. `LDAP_SETUP.md` для: +- Примеры конфигурации AD/OpenLDAP +- Настройка групп безопасности +- Troubleshooting +- Best practices diff --git a/LDAP_SETUP.md b/LDAP_SETUP.md new file mode 100644 index 0000000..b2408ce --- /dev/null +++ b/LDAP_SETUP.md @@ -0,0 +1,187 @@ +# LDAP Setup Guide + +## Overview +This guide explains how to configure LDAP/Active Directory integration for Amnezia VPN Panel. + +## Supported LDAP Servers +- OpenLDAP +- Active Directory (AD) +- FreeIPA +- Any RFC 4511 compliant LDAP server + +## Configuration + +### 1. Enable LDAP in Admin Panel +1. Navigate to Settings → LDAP +2. Enable "LDAP Authentication" +3. Fill in connection details + +### 2. Connection Settings + +#### For Active Directory: +``` +Host: ad.example.com +Port: 389 (LDAP) or 636 (LDAPS) +Use TLS: ☑ (recommended for production) +Base DN: DC=example,DC=com +Bind DN: CN=svc_vpn,CN=Users,DC=example,DC=com +Bind Password: YourServiceAccountPassword +User Search Filter: (sAMAccountName=%s) +Group Search Filter: (member=%s) +``` + +#### For OpenLDAP: +``` +Host: ldap.example.com +Port: 389 +Use TLS: ☑ +Base DN: ou=people,dc=example,dc=com +Bind DN: cn=admin,dc=example,dc=com +Bind Password: YourAdminPassword +User Search Filter: (uid=%s) +Group Search Filter: (memberUid=%s) +``` + +### 3. Group Mappings + +Map LDAP groups to panel roles: + +| LDAP Group | Panel Role | Permissions | +|-------------|-----------|-------------| +| vpn-admins | admin | Full access | +| vpn-managers| manager | Manage servers & clients | +| vpn-users | viewer | View own clients only | + +#### How to configure groups: + +**Active Directory:** +```powershell +# Create security groups +New-ADGroup -Name "vpn-admins" -GroupScope Global -GroupCategory Security +New-ADGroup -Name "vpn-managers" -GroupScope Global -GroupCategory Security +New-ADGroup -Name "vpn-users" -GroupScope Global -GroupCategory Security + +# Add users to groups +Add-ADGroupMember -Identity "vpn-admins" -Members "john.doe" +``` + +**OpenLDAP:** +```ldif +dn: cn=vpn-admins,ou=groups,dc=example,dc=com +objectClass: groupOfNames +cn: vpn-admins +member: uid=john.doe,ou=people,dc=example,dc=com +``` + +### 4. Test Connection +1. Click "Test Connection" button in LDAP settings +2. Verify successful connection +3. Save configuration + +### 5. Synchronization +- **Automatic**: Users sync every 30 minutes (configurable) +- **Manual**: Run `docker-compose exec web php bin/sync_ldap_users.php` + +## Authentication Flow + +```mermaid +graph TD + A[User Login] --> B{LDAP Enabled?} + B -->|Yes| C[Try LDAP Auth] + B -->|No| D[Local DB Auth] + C -->|Success| E[Sync User to DB] + C -->|Fail| D + E --> F[Create Session] + D -->|Success| F + D -->|Fail| G[Login Failed] +``` + +## Security Best Practices + +1. **Use TLS/SSL** + - Always enable TLS for production + - Use LDAPS (port 636) for encrypted connections + +2. **Service Account** + - Create dedicated read-only service account + - Grant minimum required permissions + - Use strong password + +3. **Group-Based Access** + - Use security groups for access control + - Regular audit of group memberships + - Remove inactive users from groups + +4. **Firewall Rules** + - Allow LDAP traffic only from VPN panel server + - Block direct LDAP access from internet + +## Troubleshooting + +### Connection Issues +```bash +# Check LDAP connectivity +docker-compose exec web php -r " +require 'vendor/autoload.php'; +require 'inc/Config.php'; +require 'inc/DB.php'; +require 'inc/LdapSync.php'; +\$ldap = new LdapSync(); +var_dump(\$ldap->testConnection()); +" +``` + +### View Sync Logs +```bash +docker-compose exec web tail -f /var/log/ldap_sync.log +``` + +### Common Errors + +**Error:** `Failed to bind` +- **Solution:** Check Bind DN and password + +**Error:** `Can't contact LDAP server` +- **Solution:** Verify host, port, and firewall rules + +**Error:** `Invalid credentials` +- **Solution:** User not found or wrong password + +## Examples + +### Migrate from Local to LDAP Auth +1. Enable LDAP +2. Run initial sync: `docker-compose exec web php bin/sync_ldap_users.php` +3. Existing users can still login with local passwords +4. LDAP users auto-created on first login + +### Disable User +Remove user from LDAP groups → Next sync will disable account + +### Change User Role +Move user to different LDAP group → Role updates on next login + +## Support + +For issues, check: +- `/var/log/ldap_sync.log` - Synchronization logs +- PHP error logs in Docker container +- LDAP server logs + +## Advanced Configuration + +### Custom User Attributes +Edit `inc/LdapSync.php` to map additional LDAP attributes: +```php +[ + 'displayName' => $entries[0]['displayname'][0], + 'phone' => $entries[0]['telephonenumber'][0], + 'department' => $entries[0]['department'][0] +] +``` + +### Multiple LDAP Servers +Currently supports single LDAP server. For multiple servers, create separate instances or use LDAP proxy. + +### SSO Integration +LDAP auth provides foundation for SSO. Consider SAML/OAuth for full SSO implementation. diff --git a/bin/sync_ldap_users.php b/bin/sync_ldap_users.php new file mode 100755 index 0000000..8e935dc --- /dev/null +++ b/bin/sync_ldap_users.php @@ -0,0 +1,38 @@ +#!/usr/bin/env php +isEnabled()) { + exit(0); // LDAP not enabled, nothing to do + } + + echo "[" . date('Y-m-d H:i:s') . "] Starting LDAP user synchronization...\n"; + + $result = $ldap->syncUsers(); + + if ($result['success']) { + echo "✓ Synchronization completed successfully:\n"; + echo " - Total users in LDAP: {$result['total']}\n"; + echo " - Synced (updated): {$result['synced']}\n"; + echo " - Created: {$result['created']}\n"; + echo " - Disabled: {$result['disabled']}\n"; + } else { + echo "✗ Synchronization failed: {$result['error']}\n"; + exit(1); + } + +} catch (Exception $e) { + echo "✗ Error: " . $e->getMessage() . "\n"; + exit(1); +} diff --git a/controllers/SettingsController.php b/controllers/SettingsController.php index 90f0428..0ac6f35 100644 --- a/controllers/SettingsController.php +++ b/controllers/SettingsController.php @@ -325,4 +325,136 @@ class SettingsController { return $stats; } + + public function ldapSettings() { + $user = Auth::user(); + if ($user['role'] !== 'admin') { + http_response_code(403); + echo 'Forbidden'; + return; + } + + // Get LDAP configuration + $stmt = $this->pdo->query("SELECT * FROM ldap_configs WHERE id = 1"); + $config = $stmt->fetch() ?: []; + + // Get group mappings + $stmt = $this->pdo->query("SELECT * FROM ldap_group_mappings ORDER BY ldap_group"); + $mappings = $stmt->fetchAll(); + + $data = [ + 'config' => $config, + 'mappings' => $mappings + ]; + + // Check for session messages + if (isset($_SESSION['settings_success'])) { + $data['success'] = $_SESSION['settings_success']; + unset($_SESSION['settings_success']); + } + if (isset($_SESSION['settings_error'])) { + $data['error'] = $_SESSION['settings_error']; + unset($_SESSION['settings_error']); + } + + View::render('settings/ldap.twig', $data); + } + + public function saveLdapSettings() { + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + header('Location: /settings/ldap'); + exit; + } + + $user = Auth::user(); + if ($user['role'] !== 'admin') { + http_response_code(403); + echo json_encode(['success' => false, 'message' => 'Forbidden']); + return; + } + + $enabled = isset($_POST['enabled']) ? 1 : 0; + $host = trim($_POST['host'] ?? ''); + $port = intval($_POST['port'] ?? 389); + $useTls = isset($_POST['use_tls']) ? 1 : 0; + $baseDn = trim($_POST['base_dn'] ?? ''); + $bindDn = trim($_POST['bind_dn'] ?? ''); + $bindPassword = $_POST['bind_password'] ?? ''; + $userSearchFilter = trim($_POST['user_search_filter'] ?? '(uid=%s)'); + $groupSearchFilter = trim($_POST['group_search_filter'] ?? '(memberUid=%s)'); + $syncInterval = intval($_POST['sync_interval'] ?? 30); + + if (empty($host) || empty($baseDn) || empty($bindDn)) { + $_SESSION['settings_error'] = 'Host, Base DN, and Bind DN are required'; + header('Location: /settings/ldap'); + exit; + } + + // Update or insert configuration + $stmt = $this->pdo->prepare(" + INSERT INTO ldap_configs + (id, enabled, host, port, use_tls, base_dn, bind_dn, bind_password, user_search_filter, group_search_filter, sync_interval) + VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + enabled = VALUES(enabled), + host = VALUES(host), + port = VALUES(port), + use_tls = VALUES(use_tls), + base_dn = VALUES(base_dn), + bind_dn = VALUES(bind_dn), + bind_password = VALUES(bind_password), + user_search_filter = VALUES(user_search_filter), + group_search_filter = VALUES(group_search_filter), + sync_interval = VALUES(sync_interval) + "); + + $stmt->execute([ + $enabled, + $host, + $port, + $useTls, + $baseDn, + $bindDn, + $bindPassword, + $userSearchFilter, + $groupSearchFilter, + $syncInterval + ]); + + $_SESSION['settings_success'] = 'LDAP settings saved successfully'; + header('Location: /settings/ldap'); + exit; + } + + public function testLdapConnection() { + header('Content-Type: application/json'); + + $user = Auth::user(); + if ($user['role'] !== 'admin') { + http_response_code(403); + echo json_encode(['success' => false, 'message' => 'Forbidden']); + return; + } + + try { + $ldap = new LdapSync(); + + if (!$ldap->isEnabled()) { + echo json_encode([ + 'success' => false, + 'message' => 'LDAP is not enabled. Please save configuration first.' + ]); + return; + } + + $result = $ldap->testConnection(); + echo json_encode($result); + + } catch (Exception $e) { + echo json_encode([ + 'success' => false, + 'message' => 'Error: ' . $e->getMessage() + ]); + } + } } diff --git a/inc/Auth.php b/inc/Auth.php index 3b59a17..5fd5cfa 100644 --- a/inc/Auth.php +++ b/inc/Auth.php @@ -16,6 +16,35 @@ class Auth { public static function login(string $email, string $password): bool { $pdo = DB::conn(); $email = strtolower(trim($email)); + + // Try LDAP authentication first if enabled + $ldap = new LdapSync(); + if ($ldap->isEnabled()) { + $ldapUser = $ldap->authenticate($email, $password); + if ($ldapUser) { + // LDAP auth successful - sync/create user in local DB + $stmt = $pdo->prepare('SELECT * FROM users WHERE ldap_dn = ? LIMIT 1'); + $stmt->execute([$ldapUser['ldap_dn']]); + $user = $stmt->fetch(); + + if (!$user) { + // Create new LDAP user + $stmt = $pdo->prepare('INSERT INTO users (email, password_hash, name, role, status, ldap_synced, ldap_dn) VALUES (?, \'\', ?, ?, \'active\', 1, ?)'); + $stmt->execute([$ldapUser['email'], $ldapUser['display_name'], $ldapUser['role'], $ldapUser['ldap_dn']]); + $userId = (int)$pdo->lastInsertId(); + } else { + $userId = (int)$user['id']; + // Update user info from LDAP + $stmt = $pdo->prepare('UPDATE users SET email = ?, name = ?, role = ?, status = \'active\', last_login_at = NOW() WHERE id = ?'); + $stmt->execute([$ldapUser['email'], $ldapUser['display_name'], $ldapUser['role'], $userId]); + } + + $_SESSION['user_id'] = $userId; + return true; + } + } + + // Fallback to local DB authentication $stmt = $pdo->prepare('SELECT * FROM users WHERE email = ? LIMIT 1'); $stmt->execute([$email]); $user = $stmt->fetch(); diff --git a/inc/LdapSync.php b/inc/LdapSync.php new file mode 100644 index 0000000..aa7111d --- /dev/null +++ b/inc/LdapSync.php @@ -0,0 +1,312 @@ +loadConfig(); + } + + /** + * Load LDAP configuration from database + */ + private function loadConfig(): void + { + $db = DB::conn(); + $stmt = $db->query("SELECT * FROM ldap_configs WHERE id = 1"); + $this->config = $stmt->fetch(PDO::FETCH_ASSOC) ?: []; + } + + /** + * Check if LDAP is enabled + */ + public function isEnabled(): bool + { + return !empty($this->config) && $this->config['enabled']; + } + + /** + * Connect to LDAP server + */ + public function connect(): bool + { + if (!$this->isEnabled()) { + return false; + } + + $ldapUri = ($this->config['use_tls'] ? 'ldaps://' : 'ldap://') . + $this->config['host'] . ':' . $this->config['port']; + + $this->connection = @ldap_connect($ldapUri); + + if (!$this->connection) { + error_log("LDAP: Failed to connect to {$ldapUri}"); + return false; + } + + ldap_set_option($this->connection, LDAP_OPT_PROTOCOL_VERSION, 3); + ldap_set_option($this->connection, LDAP_OPT_REFERRALS, 0); + + // Bind with admin credentials + $bindResult = @ldap_bind( + $this->connection, + $this->config['bind_dn'], + $this->config['bind_password'] + ); + + if (!$bindResult) { + error_log("LDAP: Failed to bind as " . $this->config['bind_dn']); + return false; + } + + return true; + } + + /** + * Authenticate user via LDAP + */ + public function authenticate(string $username, string $password): ?array + { + if (!$this->connect()) { + return null; + } + + try { + // Search for user + $filter = str_replace('%s', ldap_escape($username, '', LDAP_ESCAPE_FILTER), + $this->config['user_search_filter']); + + $search = @ldap_search( + $this->connection, + $this->config['base_dn'], + $filter, + ['dn', 'cn', 'mail', 'memberOf', 'displayName'] + ); + + if (!$search) { + return null; + } + + $entries = ldap_get_entries($this->connection, $search); + + if ($entries['count'] === 0) { + return null; + } + + $userDn = $entries[0]['dn']; + + // Try to bind as user (authenticate) + if (!@ldap_bind($this->connection, $userDn, $password)) { + return null; + } + + // Get user groups + $groups = $this->getUserGroups($userDn); + $role = $this->mapGroupsToRole($groups); + + return [ + 'username' => $username, + 'email' => $entries[0]['mail'][0] ?? '', + 'display_name' => $entries[0]['displayname'][0] ?? $entries[0]['cn'][0] ?? $username, + 'ldap_dn' => $userDn, + 'groups' => $groups, + 'role' => $role + ]; + + } catch (Exception $e) { + error_log("LDAP auth error: " . $e->getMessage()); + return null; + } finally { + $this->disconnect(); + } + } + + /** + * Get user's LDAP groups + */ + private function getUserGroups(string $userDn): array + { + $groups = []; + + try { + $filter = str_replace('%s', ldap_escape(explode(',', $userDn)[0], '', LDAP_ESCAPE_FILTER), + $this->config['group_search_filter']); + + $search = @ldap_search( + $this->connection, + $this->config['base_dn'], + $filter, + ['cn'] + ); + + if ($search) { + $entries = ldap_get_entries($this->connection, $search); + for ($i = 0; $i < $entries['count']; $i++) { + $groups[] = $entries[$i]['cn'][0]; + } + } + } catch (Exception $e) { + error_log("LDAP get groups error: " . $e->getMessage()); + } + + return $groups; + } + + /** + * Map LDAP groups to application role + */ + private function mapGroupsToRole(array $groups): string + { + $db = DB::conn(); + + foreach ($groups as $group) { + $stmt = $db->prepare("SELECT role_name FROM ldap_group_mappings WHERE ldap_group = ?"); + $stmt->execute([$group]); + $mapping = $stmt->fetch(PDO::FETCH_ASSOC); + + if ($mapping) { + return $mapping['role_name']; + } + } + + return 'viewer'; // Default role + } + + /** + * Synchronize all LDAP users + */ + public function syncUsers(): array + { + if (!$this->connect()) { + return ['success' => false, 'error' => 'Failed to connect to LDAP']; + } + + try { + $search = @ldap_search( + $this->connection, + $this->config['base_dn'], + '(objectClass=person)', + ['dn', 'cn', 'mail', 'uid'] + ); + + if (!$search) { + return ['success' => false, 'error' => 'LDAP search failed']; + } + + $entries = ldap_get_entries($this->connection, $search); + $db = DB::conn(); + $synced = 0; + $created = 0; + $disabled = 0; + + // Get existing LDAP users + $existingUsers = $db->query("SELECT ldap_dn FROM users WHERE ldap_synced = 1") + ->fetchAll(PDO::FETCH_COLUMN); + + $currentDns = []; + + for ($i = 0; $i < $entries['count']; $i++) { + $entry = $entries[$i]; + $dn = $entry['dn']; + $username = $entry['uid'][0] ?? $entry['cn'][0]; + $email = $entry['mail'][0] ?? ''; + + $currentDns[] = $dn; + + // Get user groups and map to role + $groups = $this->getUserGroups($dn); + $role = $this->mapGroupsToRole($groups); + + // Check if user exists + $stmt = $db->prepare("SELECT id FROM users WHERE ldap_dn = ?"); + $stmt->execute([$dn]); + $existing = $stmt->fetch(); + + if ($existing) { + // Update existing user + $stmt = $db->prepare(" + UPDATE users + SET email = ?, role = ?, status = 'active', ldap_synced = 1 + WHERE ldap_dn = ? + "); + $stmt->execute([$email, $role, $dn]); + $synced++; + } else { + // Create new user + $stmt = $db->prepare(" + INSERT INTO users (username, email, password, role, status, ldap_synced, ldap_dn) + VALUES (?, ?, '', ?, 'active', 1, ?) + "); + $stmt->execute([$username, $email, $role, $dn]); + $created++; + } + } + + // Disable users no longer in LDAP + $missingDns = array_diff($existingUsers, $currentDns); + if (!empty($missingDns)) { + $placeholders = str_repeat('?,', count($missingDns) - 1) . '?'; + $stmt = $db->prepare("UPDATE users SET status = 'disabled' WHERE ldap_dn IN ($placeholders)"); + $stmt->execute($missingDns); + $disabled = count($missingDns); + } + + return [ + 'success' => true, + 'synced' => $synced, + 'created' => $created, + 'disabled' => $disabled, + 'total' => $entries['count'] + ]; + + } catch (Exception $e) { + error_log("LDAP sync error: " . $e->getMessage()); + return ['success' => false, 'error' => $e->getMessage()]; + } finally { + $this->disconnect(); + } + } + + /** + * Test LDAP connection + */ + public function testConnection(): array + { + if (!$this->connect()) { + return [ + 'success' => false, + 'message' => 'Failed to connect or bind to LDAP server' + ]; + } + + $this->disconnect(); + + return [ + 'success' => true, + 'message' => 'Successfully connected to LDAP server' + ]; + } + + /** + * Disconnect from LDAP + */ + private function disconnect(): void + { + if ($this->connection) { + @ldap_unbind($this->connection); + $this->connection = null; + } + } +} diff --git a/migrations/011_add_ldap_configs.sql b/migrations/011_add_ldap_configs.sql new file mode 100644 index 0000000..9829443 --- /dev/null +++ b/migrations/011_add_ldap_configs.sql @@ -0,0 +1,38 @@ +-- Migration: Add LDAP configuration and settings +-- Date: 2025-11-10 + +-- LDAP configuration table +CREATE TABLE IF NOT EXISTS ldap_configs ( + id INT PRIMARY KEY AUTO_INCREMENT, + enabled BOOLEAN DEFAULT FALSE, + host VARCHAR(255) NOT NULL, + port INT DEFAULT 389, + use_tls BOOLEAN DEFAULT FALSE, + base_dn VARCHAR(255) NOT NULL, + bind_dn VARCHAR(255) NOT NULL, + bind_password VARCHAR(255) NOT NULL, + user_search_filter VARCHAR(255) DEFAULT '(uid=%s)', + group_search_filter VARCHAR(255) DEFAULT '(memberUid=%s)', + sync_interval INT DEFAULT 30 COMMENT 'Sync interval in minutes', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- LDAP group to role mappings +CREATE TABLE IF NOT EXISTS ldap_group_mappings ( + id INT PRIMARY KEY AUTO_INCREMENT, + ldap_group VARCHAR(255) NOT NULL UNIQUE, + role_name VARCHAR(50) NOT NULL, + description TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- Add ldap_sync flag to users table +ALTER TABLE users +ADD COLUMN ldap_synced BOOLEAN DEFAULT FALSE AFTER status, +ADD COLUMN ldap_dn VARCHAR(255) NULL AFTER ldap_synced, +ADD INDEX idx_ldap_dn (ldap_dn); + +-- Insert default LDAP configuration (disabled by default) +INSERT IGNORE INTO ldap_configs (id, enabled, host, port, base_dn, bind_dn, bind_password) +VALUES (1, FALSE, 'ldap.example.com', 389, 'dc=example,dc=com', 'cn=admin,dc=example,dc=com', ''); diff --git a/migrations/012_add_user_roles.sql b/migrations/012_add_user_roles.sql new file mode 100644 index 0000000..dbb04a5 --- /dev/null +++ b/migrations/012_add_user_roles.sql @@ -0,0 +1,32 @@ +-- Migration: Add user roles and permissions +-- Date: 2025-11-10 + +-- User roles table +CREATE TABLE IF NOT EXISTS user_roles ( + id INT PRIMARY KEY AUTO_INCREMENT, + name VARCHAR(50) NOT NULL UNIQUE, + display_name VARCHAR(100) NOT NULL, + description TEXT, + permissions JSON NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- Add role to users table +ALTER TABLE users +ADD COLUMN role VARCHAR(50) DEFAULT 'viewer' AFTER ldap_dn, +ADD INDEX idx_role (role); + +-- Insert default roles +INSERT IGNORE INTO user_roles (name, display_name, description, permissions) VALUES +('admin', 'Administrator', 'Full access to all features', JSON_ARRAY('*')), +('manager', 'Manager', 'Can manage servers and clients', JSON_ARRAY('servers.view', 'servers.create', 'servers.edit', 'clients.view', 'clients.create', 'clients.edit', 'clients.delete')), +('viewer', 'Viewer', 'Can only view own clients', JSON_ARRAY('clients.view_own', 'clients.download_own')); + +-- Insert default LDAP group mappings (examples) +INSERT IGNORE INTO ldap_group_mappings (ldap_group, role_name, description) VALUES +('vpn-admins', 'admin', 'VPN administrators with full access'), +('vpn-managers', 'manager', 'VPN managers who can create and manage clients'), +('vpn-users', 'viewer', 'Regular VPN users with view-only access'); + +-- Update existing users to admin role (backward compatibility) +UPDATE users SET role = 'admin' WHERE role IS NULL OR role = ''; diff --git a/migrations/013_add_ldap_translations.sql b/migrations/013_add_ldap_translations.sql new file mode 100644 index 0000000..9ca59f2 --- /dev/null +++ b/migrations/013_add_ldap_translations.sql @@ -0,0 +1,61 @@ +-- Migration: Add LDAP translations (English and Russian) +-- Date: 2025-11-10 + +-- English translations +INSERT IGNORE INTO translations (locale, category, key_name, translation) VALUES +('en', 'ldap', 'settings', 'LDAP Settings'), +('en', 'ldap', 'enable_ldap_auth', 'Enable LDAP Authentication'), +('en', 'ldap', 'enable_description', 'Allow users to login using LDAP/Active Directory credentials'), +('en', 'ldap', 'host', 'LDAP Host'), +('en', 'ldap', 'port', 'Port'), +('en', 'ldap', 'use_tls', 'Use TLS/SSL'), +('en', 'ldap', 'base_dn', 'Base DN'), +('en', 'ldap', 'base_dn_description', 'The base distinguished name for LDAP searches (e.g., dc=example,dc=com)'), +('en', 'ldap', 'bind_dn', 'Bind DN'), +('en', 'ldap', 'bind_dn_description', 'The distinguished name of the service account to bind with'), +('en', 'ldap', 'bind_password', 'Bind Password'), +('en', 'ldap', 'user_search_filter', 'User Search Filter'), +('en', 'ldap', 'user_search_filter_description', 'LDAP filter to search for users. %s will be replaced with username'), +('en', 'ldap', 'group_search_filter', 'Group Search Filter'), +('en', 'ldap', 'sync_interval', 'Sync Interval (minutes)'), +('en', 'ldap', 'sync_interval_description', 'How often to automatically synchronize users from LDAP'), +('en', 'ldap', 'test_connection', 'Test Connection'), +('en', 'ldap', 'testing', 'Testing'), +('en', 'ldap', 'connection_test_failed', 'Connection test failed'), +('en', 'ldap', 'group_mappings', 'LDAP Group Mappings'), +('en', 'ldap', 'group', 'LDAP Group'), +('en', 'ldap', 'role', 'Panel Role'), +('en', 'ldap', 'description', 'Description'); + +-- Russian translations +INSERT IGNORE INTO translations (locale, category, key_name, translation) VALUES +('ru', 'ldap', 'settings', 'Настройки LDAP'), +('ru', 'ldap', 'enable_ldap_auth', 'Включить LDAP аутентификацию'), +('ru', 'ldap', 'enable_description', 'Разрешить пользователям входить используя учетные данные LDAP/Active Directory'), +('ru', 'ldap', 'host', 'LDAP Хост'), +('ru', 'ldap', 'port', 'Порт'), +('ru', 'ldap', 'use_tls', 'Использовать TLS/SSL'), +('ru', 'ldap', 'base_dn', 'Base DN'), +('ru', 'ldap', 'base_dn_description', 'Базовое отличительное имя для поиска в LDAP (например, dc=example,dc=com)'), +('ru', 'ldap', 'bind_dn', 'Bind DN'), +('ru', 'ldap', 'bind_dn_description', 'Отличительное имя служебной учетной записи для подключения'), +('ru', 'ldap', 'bind_password', 'Пароль подключения'), +('ru', 'ldap', 'user_search_filter', 'Фильтр поиска пользователей'), +('ru', 'ldap', 'user_search_filter_description', 'LDAP фильтр для поиска пользователей. %s будет заменен на имя пользователя'), +('ru', 'ldap', 'group_search_filter', 'Фильтр поиска групп'), +('ru', 'ldap', 'sync_interval', 'Интервал синхронизации (минуты)'), +('ru', 'ldap', 'sync_interval_description', 'Как часто автоматически синхронизировать пользователей из LDAP'), +('ru', 'ldap', 'test_connection', 'Тест подключения'), +('ru', 'ldap', 'testing', 'Тестирование'), +('ru', 'ldap', 'connection_test_failed', 'Тест подключения не удался'), +('ru', 'ldap', 'group_mappings', 'Связи групп LDAP'), +('ru', 'ldap', 'group', 'Группа LDAP'), +('ru', 'ldap', 'role', 'Роль в панели'), +('ru', 'ldap', 'description', 'Описание'); + +-- Common translations for buttons +INSERT IGNORE INTO translations (locale, category, key_name, translation) VALUES +('en', 'common', 'save', 'Save'), +('en', 'common', 'cancel', 'Cancel'), +('ru', 'common', 'save', 'Сохранить'), +('ru', 'common', 'cancel', 'Отмена'); diff --git a/public/index.php b/public/index.php index 034bf39..e4c5f09 100644 --- a/public/index.php +++ b/public/index.php @@ -1798,6 +1798,36 @@ Router::post('/settings/delete-user/{id}', function ($params) { $controller->deleteUser($params['id']); }); +// LDAP settings page +Router::get('/settings/ldap', function () { + requireAdmin(); + + require_once __DIR__ . '/../controllers/SettingsController.php'; + require_once __DIR__ . '/../inc/LdapSync.php'; + $controller = new SettingsController(); + $controller->ldapSettings(); +}); + +// Save LDAP settings +Router::post('/settings/ldap/save', function () { + requireAdmin(); + + require_once __DIR__ . '/../controllers/SettingsController.php'; + require_once __DIR__ . '/../inc/LdapSync.php'; + $controller = new SettingsController(); + $controller->saveLdapSettings(); +}); + +// Test LDAP connection +Router::post('/settings/ldap/test', function () { + requireAdmin(); + + require_once __DIR__ . '/../controllers/SettingsController.php'; + require_once __DIR__ . '/../inc/LdapSync.php'; + $controller = new SettingsController(); + $controller->testLdapConnection(); +}); + /** * LANGUAGE ROUTES */ diff --git a/templates/settings.twig b/templates/settings.twig index 0c1a803..ae10326 100644 --- a/templates/settings.twig +++ b/templates/settings.twig @@ -47,6 +47,10 @@ class="tab-link border-transparent text-gray-500 hover:text-gray-700 py-4 px-1 border-b-2 font-medium text-sm"> {{ t('settings.users') }} + + LDAP + {% endif %} diff --git a/templates/settings/ldap.twig b/templates/settings/ldap.twig new file mode 100644 index 0000000..7755bf8 --- /dev/null +++ b/templates/settings/ldap.twig @@ -0,0 +1,181 @@ +{% extends "layout.twig" %} + +{% block title %}{{ t('ldap.settings') }}{% endblock %} + +{% block content %} +
+
+
+

{{ t('ldap.settings') }}

+ +
+ +
+
+ +

{{ t('ldap.enable_description') }}

+
+ +
+
+
+ + +
+
+ + +
+
+ +
+ +
+ +
+ + +

{{ t('ldap.base_dn_description') }}

+
+ +
+ + +

{{ t('ldap.bind_dn_description') }}

+
+ +
+ + +
+ +
+ + +

{{ t('ldap.user_search_filter_description') }}

+
+ +
+ + +
+ +
+ + +

{{ t('ldap.sync_interval_description') }}

+
+
+ +
+ + + {{ t('common.cancel') }} + +
+
+
+ +
+

{{ t('ldap.group_mappings') }}

+ +
+ + + + + + + + + + {% for mapping in mappings %} + + + + + + {% endfor %} + +
{{ t('ldap.group') }}{{ t('ldap.role') }}{{ t('ldap.description') }}
{{ mapping.ldap_group }} + + {{ mapping.role_name }} + + {{ mapping.description }}
+
+
+
+ + +{% endblock %}