mirror of
https://github.com/mailcow/mailcow-dockerized.git
synced 2026-10-28 05:05:41 +00:00
Merge pull request #7561 from mailcow/feat/netfilter-rework
[Netfilter] Rework: robust bans, no restart loops, identity provider outages are no failed logins
This commit is contained in:
40 files changed
+2313
-1240
No files matched your search
@@ -36,7 +36,18 @@ function check_login($user, $pass, $extra = null) {
|
||||
|
||||
// Try validate user
|
||||
if (!isset($role) || $role == "user") {
|
||||
$result = user_login($user, $pass, $extra);
|
||||
try {
|
||||
$result = user_login($user, $pass, $extra);
|
||||
} catch (\Mailcow\Auth\IdpUnavailable $e) {
|
||||
// no failed login, netfilter must not count it
|
||||
error_log("mailcow UI: Identity provider unavailable for " . $user . " by " . $_SERVER['REMOTE_ADDR'] . ": " . $e->getMessage());
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $user, '*', $e->getMessage()),
|
||||
'msg' => 'iam_unavailable'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if ($result !== false) {
|
||||
$real_rip = ($_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR']);
|
||||
set_sasl_log($user, $real_rip, $extra['service']);
|
||||
@@ -516,11 +527,14 @@ function keycloak_mbox_login_rest($user, $pass, $extra = null){
|
||||
return false;
|
||||
}
|
||||
if (!$iam_provider) {
|
||||
return false;
|
||||
throw new \Mailcow\Auth\IdpUnavailable('Keycloak is not configured completely');
|
||||
}
|
||||
|
||||
// get access_token for service account of mailcow client
|
||||
$admin_token = identity_provider("get-keycloak-admin-token");
|
||||
if (!$admin_token) {
|
||||
throw new \Mailcow\Auth\IdpUnavailable('Could not get an admin token from Keycloak');
|
||||
}
|
||||
|
||||
// get the mailcow_password attribute from keycloak user
|
||||
$url = "{$iam_settings['server_url']}/admin/realms/{$iam_settings['realm']}/users";
|
||||
@@ -541,13 +555,9 @@ function keycloak_mbox_login_rest($user, $pass, $extra = null){
|
||||
$users = json_decode(curl_exec($curl), true);
|
||||
$code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
||||
curl_close($curl);
|
||||
// the password is checked below, a failed request is never a wrong password
|
||||
if ($code != 200) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $user, '*', 'Identity Provider returned HTTP ' . $code),
|
||||
'msg' => 'generic_server_error'
|
||||
);
|
||||
return false;
|
||||
throw new \Mailcow\Auth\IdpUnavailable('Keycloak returned HTTP ' . $code);
|
||||
}
|
||||
// exact=true can still return several users with that email
|
||||
if (!is_array($users) || count($users) != 1) {
|
||||
@@ -687,7 +697,8 @@ function ldap_mbox_login($user, $pass, $extra = null){
|
||||
return false;
|
||||
}
|
||||
if (!$iam_provider) {
|
||||
return false;
|
||||
// identity_provider('init') could not connect
|
||||
throw new \Mailcow\Auth\IdpUnavailable('No connection to the LDAP server');
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -700,33 +711,17 @@ function ldap_mbox_login($user, $pass, $extra = null){
|
||||
\Mailcow\Identity\Directory\LdapDirectory::idAttributes($iam_settings['id_attribute'] ?? '')))));
|
||||
|
||||
$user_res = $ldap_query->firstOrFail();
|
||||
} catch (Exception $e) {
|
||||
// clear $_SESSION['return'] to not leak data
|
||||
$_SESSION['return'] = array();
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $user, '*', $e->getMessage()),
|
||||
'msg' => 'generic_server_error'
|
||||
);
|
||||
} catch (\LdapRecord\Query\ObjectNotFoundException $e) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
if (!$iam_provider->auth()->attempt($user_res['dn'], $pass)) {
|
||||
return false;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// clear $_SESSION['return'] to not leak data
|
||||
$_SESSION['return'] = array();
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $user, '*', $e->getMessage()),
|
||||
'msg' => 'generic_server_error'
|
||||
);
|
||||
throw new \Mailcow\Auth\IdpUnavailable('LDAP search failed: ' . $e->getMessage(), 0, $e);
|
||||
}
|
||||
$idp_login = \Mailcow\Auth\AuthModule::fromGlobals()->idp;
|
||||
if (!$idp_login->verifyLdapPassword($iam_provider, (string)$user_res['dn'], (string)$pass)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// get mapped template
|
||||
$idp_login = \Mailcow\Auth\AuthModule::fromGlobals()->idp;
|
||||
$mapped_template = $idp_login->mappedTemplate($user_res[$iam_settings['attribute_field']] ?? array());
|
||||
$external_id = \Mailcow\Identity\Directory\LdapDirectory::entryId($user_res, $iam_settings['id_attribute'] ?? '');
|
||||
|
||||
|
||||
@@ -60,13 +60,14 @@ function fail2ban($_action, $_data = null, $_extra = null) {
|
||||
$queue_unban = $redis->hGetAll('F2B_QUEUE_UNBAN');
|
||||
if (is_array($active_bans)) {
|
||||
foreach ($active_bans as $network => $banned_until) {
|
||||
$queued_for_unban = (isset($queue_unban[$network]) && $queue_unban[$network] == 1) ? 1 : 0;
|
||||
$difference = $banned_until - time();
|
||||
// Expired bans are lifted by netfilter within seconds
|
||||
$difference = max(0, intval($banned_until) - time());
|
||||
$queued_for_unban = ($difference == 0 || (isset($queue_unban[$network]) && $queue_unban[$network] == 1)) ? 1 : 0;
|
||||
$f2b_options['active_bans'][] = array(
|
||||
'queued_for_unban' => $queued_for_unban,
|
||||
'network' => $network,
|
||||
'ip' => strtok($network,'/'),
|
||||
'banned_until' => sprintf('%02dh %02dm %02ds', ($difference/3600), ($difference/60%60), $difference%60)
|
||||
'banned_until' => sprintf('%02dh %02dm %02ds', intdiv($difference, 3600), intdiv($difference, 60) % 60, $difference % 60)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -108,16 +109,15 @@ function fail2ban($_action, $_data = null, $_extra = null) {
|
||||
);
|
||||
return false;
|
||||
}
|
||||
// Rules will also be recreated on log events, but rules may seem empty for a second in the UI
|
||||
docker('post', 'netfilter-mailcow', 'restart');
|
||||
// netfilter recreates the default rules within 10 seconds
|
||||
$fail_count = 0;
|
||||
$regex_result = json_decode($redis->Get('F2B_REGEX'), true);
|
||||
while (empty($regex_result) && $fail_count < 10) {
|
||||
while (empty($regex_result) && $fail_count < 15) {
|
||||
$regex_result = json_decode($redis->Get('F2B_REGEX'), true);
|
||||
$fail_count++;
|
||||
sleep(1);
|
||||
}
|
||||
if ($fail_count >= 10) {
|
||||
if (empty($regex_result)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
@@ -141,7 +141,7 @@ function fail2ban($_action, $_data = null, $_extra = null) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('object_modified', htmlspecialchars($network))
|
||||
'msg' => 'f2b_modified'
|
||||
);
|
||||
return true;
|
||||
}
|
||||
@@ -172,7 +172,7 @@ function fail2ban($_action, $_data = null, $_extra = null) {
|
||||
if (valid_network($network)) {
|
||||
try {
|
||||
$redis->hSet('F2B_WHITELIST', $network, 1);
|
||||
$redis->hDel('F2B_BLACKLIST', $network, 1);
|
||||
$redis->hDel('F2B_BLACKLIST', $network);
|
||||
$redis->hSet('F2B_QUEUE_UNBAN', $network, 1);
|
||||
}
|
||||
catch (RedisException $e) {
|
||||
@@ -199,14 +199,13 @@ function fail2ban($_action, $_data = null, $_extra = null) {
|
||||
if (valid_network($network) && !in_array($network, array(
|
||||
'0.0.0.0',
|
||||
'0.0.0.0/0',
|
||||
getenv('IPV4_NETWORK') . '0/24',
|
||||
getenv('IPV4_NETWORK') . '0',
|
||||
getenv('IPV4_NETWORK') . '.0/24',
|
||||
getenv('IPV4_NETWORK') . '.0',
|
||||
getenv('IPV6_NETWORK')
|
||||
))) {
|
||||
try {
|
||||
$redis->hSet('F2B_BLACKLIST', $network, 1);
|
||||
$redis->hDel('F2B_WHITELIST', $network, 1);
|
||||
//$response = docker('post', 'netfilter-mailcow', 'restart');
|
||||
$redis->hDel('F2B_WHITELIST', $network);
|
||||
}
|
||||
catch (RedisException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
@@ -261,62 +260,60 @@ function fail2ban($_action, $_data = null, $_extra = null) {
|
||||
$f2b_options['ban_time'] = ($ban_time < 60) ? 60 : $ban_time;
|
||||
$f2b_options['ban_time_increment'] = ($ban_time_increment == 1) ? true : false;
|
||||
$f2b_options['max_ban_time'] = ($max_ban_time < 60) ? 60 : $max_ban_time;
|
||||
$f2b_options['netban_ipv4'] = ($netban_ipv4 < 8) ? 8 : $netban_ipv4;
|
||||
$f2b_options['netban_ipv6'] = ($netban_ipv6 < 8) ? 8 : $netban_ipv6;
|
||||
$f2b_options['netban_ipv4'] = ($netban_ipv4 > 32) ? 32 : $netban_ipv4;
|
||||
$f2b_options['netban_ipv6'] = ($netban_ipv6 > 128) ? 128 : $netban_ipv6;
|
||||
$f2b_options['netban_ipv4'] = min(32, max(8, $netban_ipv4));
|
||||
$f2b_options['netban_ipv6'] = min(128, max(8, $netban_ipv6));
|
||||
$f2b_options['max_attempts'] = ($max_attempts < 1) ? 1 : $max_attempts;
|
||||
$f2b_options['retry_window'] = ($retry_window < 1) ? 1 : $retry_window;
|
||||
$f2b_options['banlist_id'] = $is_now['banlist_id'];
|
||||
$f2b_options['banlist_id'] = (!empty($is_now['banlist_id'])) ? $is_now['banlist_id'] : uuid4();
|
||||
$f2b_options['manage_external'] = ($manage_external > 0) ? 1 : 0;
|
||||
$wl_items = array();
|
||||
if (!empty($wl)) {
|
||||
foreach (array_filter(array_map('trim', preg_split("/( |,|;|\n)/", $wl))) as $wl_item) {
|
||||
if (valid_network($wl_item) || valid_hostname($wl_item)) {
|
||||
$wl_items[$wl_item] = 1;
|
||||
}
|
||||
else {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('network_host_invalid', $wl_item)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
$bl_items = array();
|
||||
if (!empty($bl)) {
|
||||
foreach (array_filter(array_map('trim', preg_split("/( |,|;|\n)/", $bl))) as $bl_item) {
|
||||
if (valid_network($bl_item) && !in_array($bl_item, array(
|
||||
'0.0.0.0',
|
||||
'0.0.0.0/0',
|
||||
getenv('IPV4_NETWORK') . '.0/24',
|
||||
getenv('IPV4_NETWORK') . '.0',
|
||||
getenv('IPV6_NETWORK')
|
||||
))) {
|
||||
$bl_items[$bl_item] = 1;
|
||||
}
|
||||
else {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('network_host_invalid', $bl_item)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
// One transaction, netfilter must never see empty lists in between
|
||||
$redis->multi();
|
||||
$redis->Set('F2B_OPTIONS', json_encode($f2b_options));
|
||||
$redis->Del('F2B_WHITELIST');
|
||||
$redis->Del('F2B_BLACKLIST');
|
||||
if(!empty($wl)) {
|
||||
$wl_array = array_map('trim', preg_split( "/( |,|;|\n)/", $wl));
|
||||
$wl_array = array_filter($wl_array);
|
||||
if (is_array($wl_array)) {
|
||||
foreach ($wl_array as $wl_item) {
|
||||
if (valid_network($wl_item) || valid_hostname($wl_item)) {
|
||||
$redis->hSet('F2B_WHITELIST', $wl_item, 1);
|
||||
}
|
||||
else {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('network_host_invalid', $wl_item)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
$redis->Del('F2B_WHITELIST', 'F2B_BLACKLIST');
|
||||
if (!empty($wl_items)) {
|
||||
$redis->hMSet('F2B_WHITELIST', $wl_items);
|
||||
}
|
||||
if(!empty($bl)) {
|
||||
$bl_array = array_map('trim', preg_split( "/( |,|;|\n)/", $bl));
|
||||
$bl_array = array_filter($bl_array);
|
||||
if (is_array($bl_array)) {
|
||||
foreach ($bl_array as $bl_item) {
|
||||
if (valid_network($bl_item) && !in_array($bl_item, array(
|
||||
'0.0.0.0',
|
||||
'0.0.0.0/0',
|
||||
getenv('IPV4_NETWORK') . '0/24',
|
||||
getenv('IPV4_NETWORK') . '0',
|
||||
getenv('IPV6_NETWORK')
|
||||
))) {
|
||||
$redis->hSet('F2B_BLACKLIST', $bl_item, 1);
|
||||
}
|
||||
else {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('network_host_invalid', $bl_item)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!empty($bl_items)) {
|
||||
$redis->hMSet('F2B_BLACKLIST', $bl_items);
|
||||
}
|
||||
$redis->exec();
|
||||
}
|
||||
catch (RedisException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
@@ -348,7 +345,9 @@ function fail2ban($_action, $_data = null, $_extra = null) {
|
||||
if (is_array($_extra)) {
|
||||
$_extra = $_extra[0];
|
||||
}
|
||||
if ($_extra != $f2b_options['banlist_id']){
|
||||
// Timing safe, a missing or empty id never matches
|
||||
$banlist_id = (string)($f2b_options['banlist_id'] ?? '');
|
||||
if ($banlist_id === '' || !is_string($_extra) || !hash_equals($banlist_id, $_extra)) {
|
||||
http_response_code(404);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1292,8 +1292,13 @@ function set_tfa($_data) {
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if ($row) {
|
||||
if ($row['authsource'] == 'ldap'){
|
||||
if (!ldap_mbox_login($username, $_data["confirm_password"], $iam_settings)) $access_denied = true;
|
||||
else $access_denied = false;
|
||||
try {
|
||||
if (!ldap_mbox_login($username, $_data["confirm_password"], $iam_settings)) $access_denied = true;
|
||||
else $access_denied = false;
|
||||
} catch (\Mailcow\Auth\IdpUnavailable $e) {
|
||||
$_SESSION['return'][] = array('type' => 'danger', 'log' => array(__FUNCTION__, $_data_log, $e->getMessage()), 'msg' => 'iam_unavailable');
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (!verify_hash($row['password'], $_data["confirm_password"])) $access_denied = true;
|
||||
else $access_denied = false;
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mailcow\Auth;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* The identity provider could not answer a login (unreachable, timeout, server error). No failed login:
|
||||
* callers report a temporary failure and must not count it as an attempt for netfilter.
|
||||
*/
|
||||
class IdpUnavailable extends RuntimeException
|
||||
{
|
||||
}
|
||||
@@ -4,6 +4,10 @@ declare(strict_types=1);
|
||||
|
||||
namespace Mailcow\Auth\Service;
|
||||
|
||||
use LdapRecord\Auth\BindException;
|
||||
use LdapRecord\Connection;
|
||||
use LdapRecord\LdapRecordException;
|
||||
use Mailcow\Auth\IdpUnavailable;
|
||||
use Mailcow\Identity\Model\Settings;
|
||||
use Mailcow\Identity\Service\LinkService;
|
||||
use Mailcow\MailboxGroup\Service\TemplateService;
|
||||
@@ -19,6 +23,37 @@ class IdpLoginService
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds as the user to check the password, then again as the configured user. False for wrong credentials,
|
||||
* IdpUnavailable when the server cannot answer (client side codes are negative: server down, timeout, ...).
|
||||
*/
|
||||
public function verifyLdapPassword(Connection $ldap, string $dn, string $password): bool
|
||||
{
|
||||
// an empty password would be an unauthenticated bind, which succeeds
|
||||
if ($dn === '' || $password === '') {
|
||||
return false;
|
||||
}
|
||||
$guard = $ldap->auth();
|
||||
try {
|
||||
$guard->bind($dn, $password);
|
||||
$bound = true;
|
||||
} catch (BindException $e) {
|
||||
$code = $e->getDetailedError()?->getErrorCode();
|
||||
if ($code !== null && ($code < 0 || in_array($code, [51, 52], true))) {
|
||||
throw new IdpUnavailable('LDAP bind failed: ' . $e->getMessage(), 0, $e);
|
||||
}
|
||||
$bound = false;
|
||||
} catch (LdapRecordException $e) {
|
||||
throw new IdpUnavailable('LDAP bind failed: ' . $e->getMessage(), 0, $e);
|
||||
}
|
||||
try {
|
||||
$guard->bindAsConfiguredUser();
|
||||
} catch (LdapRecordException $e) {
|
||||
// the login needs no further LDAP access
|
||||
}
|
||||
return $bound;
|
||||
}
|
||||
|
||||
/** Template of the first attribute mapping matching one of the IdP attribute values, null if none */
|
||||
public function mappedTemplate(mixed $values): ?string
|
||||
{
|
||||
|
||||
@@ -395,8 +395,7 @@
|
||||
"iam_host": "Hostitel",
|
||||
"iam_host_info": "Zadejte jeden či více hostitelů, oddělte čárkou.",
|
||||
"iam_import_users": "Importovat uživatele",
|
||||
"iam_auth_flow": "Proces autentizace",
|
||||
"needs_restart": "potřebuje restart"
|
||||
"iam_auth_flow": "Proces autentizace"
|
||||
},
|
||||
"danger": {
|
||||
"access_denied": "Přístup odepřen nebo jsou neplatná data ve formuláři",
|
||||
|
||||
@@ -352,7 +352,6 @@
|
||||
"message": "Nachricht",
|
||||
"message_size": "Nachrichtengröße",
|
||||
"nexthop": "Next Hop",
|
||||
"needs_restart": "benötigt Neustart",
|
||||
"no": "✕",
|
||||
"no_active_bans": "Keine aktiven Banns",
|
||||
"no_new_rows": "Keine weiteren Zeilen vorhanden",
|
||||
@@ -560,6 +559,7 @@
|
||||
"iam_sync_mode_invalid": "Ungültiger Sync-Modus",
|
||||
"iam_sync_unavailable": "Der Identity Provider kann nicht synchronisiert werden (nur Keycloak und LDAP)",
|
||||
"iam_test_connection": "Verbindung fehlgeschlagen",
|
||||
"iam_unavailable": "Der Identity Provider ist nicht erreichbar, bitte später erneut versuchen.",
|
||||
"imagick_exception": "Fataler Bildverarbeitungsfehler",
|
||||
"img_dimensions_exceeded": "Grafik überschreitet die maximale Bildgröße",
|
||||
"img_invalid": "Grafik konnte nicht validiert werden",
|
||||
|
||||
@@ -359,7 +359,6 @@
|
||||
"message": "Message",
|
||||
"message_size": "Message size",
|
||||
"nexthop": "Next hop",
|
||||
"needs_restart": "needs restart",
|
||||
"no": "✕",
|
||||
"no_active_bans": "No active bans",
|
||||
"no_new_rows": "No further rows available",
|
||||
@@ -561,6 +560,7 @@
|
||||
"goto_invalid": "Goto address %s is invalid",
|
||||
"ham_learn_error": "Ham learn error: %s",
|
||||
"iam_test_connection": "Connection failed",
|
||||
"iam_unavailable": "The identity provider is not reachable, please try again later.",
|
||||
"imagick_exception": "Error: Imagick exception while reading image",
|
||||
"img_dimensions_exceeded": "Image exceeds the maximum image size",
|
||||
"img_invalid": "Cannot validate image file",
|
||||
|
||||
@@ -340,7 +340,6 @@
|
||||
"iam_host_info": "Introduzca uno o más hosts de LDAP, separados por comas.",
|
||||
"iam_import_users": "Importar usuarios",
|
||||
"iam_mapping": "Equivalencias de atributos",
|
||||
"needs_restart": "necesita reinicio",
|
||||
"oauth2_apps": "Aplicaciones OAuth2",
|
||||
"oauth2_add_client": "Añadir cliente OAuth2",
|
||||
"oauth2_renew_secret": "Generar nuevo secreto de cliente",
|
||||
|
||||
@@ -395,7 +395,6 @@
|
||||
"iam_host_info": "Saisissez un ou plusieurs hôtes LDAP, séparés par des virgules.",
|
||||
"iam_import_users": "Importer des utilisateurs",
|
||||
"filter": "Filtrer",
|
||||
"needs_restart": "nécessite un redémarrage",
|
||||
"iam": "Fournisseur d'identité"
|
||||
},
|
||||
"danger": {
|
||||
|
||||
@@ -295,8 +295,7 @@
|
||||
"validate_license_now": "GUID érvényesítése a licenszszerverrel szemben",
|
||||
"yes": "✓",
|
||||
"success": "Siker",
|
||||
"login_page": "Belépő oldal",
|
||||
"needs_restart": "újraindítást igényel"
|
||||
"login_page": "Belépő oldal"
|
||||
},
|
||||
"edit": {
|
||||
"active": "Aktív",
|
||||
|
||||
@@ -376,7 +376,6 @@
|
||||
"iam_version": "Versie",
|
||||
"ignore_ssl_error": "Negeer SSL foutmeldingen",
|
||||
"ip_check_opt_in": "Kies ervoor om de externe diensten <strong>ipv4.mailcow.email</strong> en <strong>ipv6.mailcow.email</strong> te gebruiken om externe IP-adressen te bepalen.",
|
||||
"needs_restart": "vereist een herstart",
|
||||
"no": "✕",
|
||||
"password_reset_info": "Als er geen herstel-e-mailadres is opgegeven, kan deze functie niet worden gebruikt.",
|
||||
"password_reset_settings": "Instellingen voor wachtwoordherstel",
|
||||
|
||||
@@ -286,7 +286,6 @@
|
||||
"merged_vars_hint": "Wyszarzone wiersze zostały połączone z <code>vars. (local.) inc.php</code> i nie można ich modyfikować.",
|
||||
"message_size": "Rozmiar wiadomości",
|
||||
"nexthop": "Następny hop",
|
||||
"needs_restart": "potrzebny restart",
|
||||
"no": "✕",
|
||||
"no_active_bans": "Brak aktywnych banów",
|
||||
"no_new_rows": "Brak dostępnych kolejnych wierszy",
|
||||
|
||||
@@ -392,7 +392,6 @@
|
||||
"iam_use_tls_info": "Se habilitar TLS, você deve usar a porta padrão para seu servidor LDAP (389). Portas SSL não podem ser usadas.",
|
||||
"iam_version": "Versão",
|
||||
"ignore_ssl_error": "Ignorar erros SSL",
|
||||
"needs_restart": "precisa reiniciar",
|
||||
"quicklink_text": "Mostrar ou ocultar links rápidos para outras páginas de login abaixo do formulário de login",
|
||||
"task": "Tarefa",
|
||||
"user_link": "Link do usuário",
|
||||
|
||||
@@ -395,8 +395,7 @@
|
||||
"iam_host": "Хост",
|
||||
"iam_host_info": "Укажите один или несколько LDAP-хостов через запятую.",
|
||||
"iam_import_users": "Импорт пользователей",
|
||||
"admin_quicklink": "Скрыть ссылку на вход для администраторов",
|
||||
"needs_restart": "необходим перезапуск"
|
||||
"admin_quicklink": "Скрыть ссылку на вход для администраторов"
|
||||
},
|
||||
"danger": {
|
||||
"access_denied": "Доступ запрещён, или указаны неверные данные",
|
||||
|
||||
@@ -397,7 +397,6 @@
|
||||
"task": "Naloga",
|
||||
"user_link": "Uporabniška povezava",
|
||||
"iam_realm": "Realm",
|
||||
"needs_restart": "potreben je ponovni zagon",
|
||||
"ui_theme": "Tema",
|
||||
"ui_theme_dark": "Vključuje različico s temnim načinom",
|
||||
"ui_theme_default": "Privzeto",
|
||||
|
||||
@@ -266,7 +266,6 @@
|
||||
"message": "Xabar",
|
||||
"message_size": "Xabar oʻlchami",
|
||||
"nexthop": "Keyingi nuqta",
|
||||
"needs_restart": "qayta ishga tushirish kerak",
|
||||
"no": "✕",
|
||||
"no_active_bans": "Faol bloklar yoʻq",
|
||||
"no_new_rows": "Boshqa satrlar mavjud emas",
|
||||
|
||||
@@ -266,7 +266,6 @@
|
||||
"message": "Tin nhắn",
|
||||
"message_size": "Kích thước tin nhắn",
|
||||
"nexthop": "Bước nhảy tiếp theo",
|
||||
"needs_restart": "cần khởi động lại",
|
||||
"no_active_bans": "Không có lệnh cấm đang hoạt động",
|
||||
"no_new_rows": "Không có hàng nào khác",
|
||||
"no_record": "Không có bản ghi",
|
||||
|
||||
@@ -395,7 +395,6 @@
|
||||
"force_sso": "强制要求单点登录(SSO)",
|
||||
"user_link": "自定义链接",
|
||||
"app_hide": "在登入界面隐藏",
|
||||
"needs_restart": "需要重启",
|
||||
"iam_use_tls_info": "如果使用了 TLS,必须使用 LDAP 服务器的默认端口(389)。SSL 的端口不能使用。"
|
||||
},
|
||||
"danger": {
|
||||
|
||||
@@ -119,7 +119,7 @@
|
||||
{% if active_ban.queued_for_unban == 0 %}
|
||||
<a data-action="edit_selected" data-item="{{ active_ban.network }}" data-id="f2b-quick" data-api-url='edit/fail2ban' data-api-attr='{"action":"unban"}' href="#">[{{ lang.admin.queue_unban }}]</a>
|
||||
<a data-action="edit_selected" data-item="{{ active_ban.network }}" data-id="f2b-quick" data-api-url='edit/fail2ban' data-api-attr='{"action":"whitelist"}' href="#">[allowlist]</a>
|
||||
<a data-action="edit_selected" data-item="{{ active_ban.network }}" data-id="f2b-quick" data-api-url='edit/fail2ban' data-api-attr='{"action":"blacklist"}' href="#">[denylist (<b>{{ lang.admin.needs_restart }}</b>)]</a>
|
||||
<a data-action="edit_selected" data-item="{{ active_ban.network }}" data-id="f2b-quick" data-api-url='edit/fail2ban' data-api-attr='{"action":"blacklist"}' href="#">[denylist]</a>
|
||||
{% else %}
|
||||
<i>{{ lang.admin.unban_pending }}</i>
|
||||
{% endif %}
|
||||
|
||||
Reference in new issue
Block a user