feat: Add LDAP/Active Directory integration with group-based access control
- Add PHP LDAP extension to Docker container - Implement LdapSync class for authentication and user synchronization - Add automatic user sync via cron (every 30 minutes) - Create role-based access control system (admin, manager, viewer) - Add LDAP configuration UI in settings - Support for both Active Directory and OpenLDAP - Group-to-role mapping with flexible configuration - Add 50+ translations (EN + RU) for LDAP features - Include comprehensive setup documentation - Enhance Auth::login() with LDAP fallback - Add LDAP settings page with connection testing
This commit is contained in:
+7
-3
@@ -1,6 +1,6 @@
|
|||||||
FROM php:8.2-apache
|
FROM php:8.2-apache
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies including LDAP
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y \
|
||||||
git \
|
git \
|
||||||
curl \
|
curl \
|
||||||
@@ -13,7 +13,9 @@ RUN apt-get update && apt-get install -y \
|
|||||||
openssh-client \
|
openssh-client \
|
||||||
qrencode \
|
qrencode \
|
||||||
cron \
|
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 \
|
&& a2enmod rewrite \
|
||||||
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
&& 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
|
# 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 \
|
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 "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 \
|
&& 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 \
|
&& chmod 0644 /etc/cron.d/amnezia-cron \
|
||||||
&& crontab /etc/cron.d/amnezia-cron \
|
&& crontab /etc/cron.d/amnezia-cron \
|
||||||
&& touch /var/log/cron.log \
|
&& touch /var/log/cron.log \
|
||||||
&& touch /var/log/metrics_monitor.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
|
# Make monitor script executable
|
||||||
RUN chmod +x /var/www/html/bin/monitor_metrics.sh
|
RUN chmod +x /var/www/html/bin/monitor_metrics.sh
|
||||||
|
|||||||
@@ -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
|
||||||
+187
@@ -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.
|
||||||
Executable
+38
@@ -0,0 +1,38 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* LDAP User Synchronization Script
|
||||||
|
* Runs periodically to sync users from LDAP/AD
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
require_once __DIR__ . '/../inc/Config.php';
|
||||||
|
require_once __DIR__ . '/../inc/DB.php';
|
||||||
|
require_once __DIR__ . '/../inc/LdapSync.php';
|
||||||
|
|
||||||
|
try {
|
||||||
|
$ldap = new LdapSync();
|
||||||
|
|
||||||
|
if (!$ldap->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);
|
||||||
|
}
|
||||||
@@ -325,4 +325,136 @@ class SettingsController {
|
|||||||
|
|
||||||
return $stats;
|
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()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,35 @@ class Auth {
|
|||||||
public static function login(string $email, string $password): bool {
|
public static function login(string $email, string $password): bool {
|
||||||
$pdo = DB::conn();
|
$pdo = DB::conn();
|
||||||
$email = strtolower(trim($email));
|
$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 = $pdo->prepare('SELECT * FROM users WHERE email = ? LIMIT 1');
|
||||||
$stmt->execute([$email]);
|
$stmt->execute([$email]);
|
||||||
$user = $stmt->fetch();
|
$user = $stmt->fetch();
|
||||||
|
|||||||
@@ -0,0 +1,312 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LdapSync - LDAP integration and user synchronization
|
||||||
|
*
|
||||||
|
* Provides:
|
||||||
|
* - Connection to LDAP/Active Directory
|
||||||
|
* - User authentication via LDAP
|
||||||
|
* - Group-based access control
|
||||||
|
* - Automatic user synchronization
|
||||||
|
*/
|
||||||
|
class LdapSync
|
||||||
|
{
|
||||||
|
private $connection;
|
||||||
|
private array $config;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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', '');
|
||||||
@@ -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 = '';
|
||||||
@@ -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', 'Отмена');
|
||||||
@@ -1798,6 +1798,36 @@ Router::post('/settings/delete-user/{id}', function ($params) {
|
|||||||
$controller->deleteUser($params['id']);
|
$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
|
* LANGUAGE ROUTES
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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">
|
class="tab-link border-transparent text-gray-500 hover:text-gray-700 py-4 px-1 border-b-2 font-medium text-sm">
|
||||||
<i class="fas fa-users mr-2"></i>{{ t('settings.users') }}
|
<i class="fas fa-users mr-2"></i>{{ t('settings.users') }}
|
||||||
</a>
|
</a>
|
||||||
|
<a href="/settings/ldap"
|
||||||
|
class="tab-link border-transparent text-gray-500 hover:text-gray-700 py-4 px-1 border-b-2 font-medium text-sm">
|
||||||
|
<i class="fas fa-network-wired mr-2"></i>LDAP
|
||||||
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,181 @@
|
|||||||
|
{% extends "layout.twig" %}
|
||||||
|
|
||||||
|
{% block title %}{{ t('ldap.settings') }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="max-w-4xl mx-auto">
|
||||||
|
<div class="bg-white rounded-lg shadow-md p-6 mb-6">
|
||||||
|
<div class="flex items-center justify-between mb-6">
|
||||||
|
<h2 class="text-2xl font-bold text-gray-800">{{ t('ldap.settings') }}</h2>
|
||||||
|
<button id="testConnection" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
|
||||||
|
{{ t('ldap.test_connection') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form id="ldapForm" method="POST" action="/settings/ldap/save">
|
||||||
|
<div class="mb-6 p-4 bg-gray-50 rounded-lg">
|
||||||
|
<label class="flex items-center cursor-pointer">
|
||||||
|
<input type="checkbox" name="enabled" value="1"
|
||||||
|
{% if config.enabled %}checked{% endif %}
|
||||||
|
class="w-5 h-5 text-blue-600 rounded focus:ring-2 focus:ring-blue-500">
|
||||||
|
<span class="ml-3 text-lg font-medium text-gray-900">{{ t('ldap.enable_ldap_auth') }}</span>
|
||||||
|
</label>
|
||||||
|
<p class="mt-2 ml-8 text-sm text-gray-600">{{ t('ldap.enable_description') }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
{{ t('ldap.host') }} <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input type="text" name="host" value="{{ config.host }}" required
|
||||||
|
placeholder="ldap.example.com"
|
||||||
|
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
{{ t('ldap.port') }}
|
||||||
|
</label>
|
||||||
|
<input type="number" name="port" value="{{ config.port }}"
|
||||||
|
placeholder="389"
|
||||||
|
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="flex items-center cursor-pointer">
|
||||||
|
<input type="checkbox" name="use_tls" value="1"
|
||||||
|
{% if config.use_tls %}checked{% endif %}
|
||||||
|
class="w-4 h-4 text-blue-600 rounded focus:ring-2 focus:ring-blue-500">
|
||||||
|
<span class="ml-2 text-sm text-gray-700">{{ t('ldap.use_tls') }} (LDAPS)</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
{{ t('ldap.base_dn') }} <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input type="text" name="base_dn" value="{{ config.base_dn }}" required
|
||||||
|
placeholder="dc=example,dc=com"
|
||||||
|
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||||
|
<p class="mt-1 text-xs text-gray-500">{{ t('ldap.base_dn_description') }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
{{ t('ldap.bind_dn') }} <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input type="text" name="bind_dn" value="{{ config.bind_dn }}" required
|
||||||
|
placeholder="cn=admin,dc=example,dc=com"
|
||||||
|
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||||
|
<p class="mt-1 text-xs text-gray-500">{{ t('ldap.bind_dn_description') }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
{{ t('ldap.bind_password') }} <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input type="password" name="bind_password" value="{{ config.bind_password }}" required
|
||||||
|
placeholder="••••••••"
|
||||||
|
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
{{ t('ldap.user_search_filter') }}
|
||||||
|
</label>
|
||||||
|
<input type="text" name="user_search_filter" value="{{ config.user_search_filter }}"
|
||||||
|
placeholder="(uid=%s)"
|
||||||
|
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||||
|
<p class="mt-1 text-xs text-gray-500">{{ t('ldap.user_search_filter_description') }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
{{ t('ldap.group_search_filter') }}
|
||||||
|
</label>
|
||||||
|
<input type="text" name="group_search_filter" value="{{ config.group_search_filter }}"
|
||||||
|
placeholder="(memberUid=%s)"
|
||||||
|
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
{{ t('ldap.sync_interval') }}
|
||||||
|
</label>
|
||||||
|
<input type="number" name="sync_interval" value="{{ config.sync_interval }}"
|
||||||
|
placeholder="30"
|
||||||
|
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||||
|
<p class="mt-1 text-xs text-gray-500">{{ t('ldap.sync_interval_description') }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-6 flex gap-4">
|
||||||
|
<button type="submit" class="px-6 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700">
|
||||||
|
{{ t('common.save') }}
|
||||||
|
</button>
|
||||||
|
<a href="/settings" class="px-6 py-2 bg-gray-300 text-gray-700 rounded-lg hover:bg-gray-400">
|
||||||
|
{{ t('common.cancel') }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white rounded-lg shadow-md p-6">
|
||||||
|
<h3 class="text-xl font-bold text-gray-800 mb-4">{{ t('ldap.group_mappings') }}</h3>
|
||||||
|
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="min-w-full divide-y divide-gray-200">
|
||||||
|
<thead class="bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{{ t('ldap.group') }}</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{{ t('ldap.role') }}</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{{ t('ldap.description') }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="bg-white divide-y divide-gray-200">
|
||||||
|
{% for mapping in mappings %}
|
||||||
|
<tr>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ mapping.ldap_group }}</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
|
<span class="px-2 py-1 text-xs font-semibold rounded-full
|
||||||
|
{% if mapping.role_name == 'admin' %}bg-red-100 text-red-800
|
||||||
|
{% elseif mapping.role_name == 'manager' %}bg-blue-100 text-blue-800
|
||||||
|
{% else %}bg-gray-100 text-gray-800{% endif %}">
|
||||||
|
{{ mapping.role_name }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 text-sm text-gray-500">{{ mapping.description }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.getElementById('testConnection').addEventListener('click', async function() {
|
||||||
|
const btn = this;
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = '{{ t('ldap.testing') }}...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/settings/ldap/test', { method: 'POST' });
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
alert('✓ ' + result.message);
|
||||||
|
} else {
|
||||||
|
alert('✗ ' + result.message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert('{{ t('ldap.connection_test_failed') }}: ' + error.message);
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = '{{ t('ldap.test_connection') }}';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user