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
|
||||
|
||||
# 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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
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 {
|
||||
$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();
|
||||
|
||||
@@ -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']);
|
||||
});
|
||||
|
||||
// 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
|
||||
*/
|
||||
|
||||
@@ -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">
|
||||
<i class="fas fa-users mr-2"></i>{{ t('settings.users') }}
|
||||
</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 %}
|
||||
</nav>
|
||||
</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