1
0
mirror of https://github.com/mailcow/mailcow-dockerized.git synced 2025-01-10 04:18:10 +02:00

[Web] add ldap idp

This commit is contained in:
FreddleSpl0it 2024-02-20 10:31:14 +01:00
parent d479d18507
commit a06c78362a
No known key found for this signature in database
GPG Key ID: 00E14E7634F4BEC5
27 changed files with 907 additions and 237 deletions

View File

@ -18,7 +18,7 @@ try {
$pdo = new PDO($dsn, $database_user, $database_pass, $opt);
}
catch (PDOException $e) {
logMsg("danger", $e->getMessage());
logMsg("err", $e->getMessage());
session_destroy();
exit;
}
@ -68,11 +68,12 @@ $_SESSION['acl']['ratelimit'] = "1";
$_SESSION['acl']['sogo_access'] = "1";
$_SESSION['acl']['protocol_access'] = "1";
$_SESSION['acl']['mailbox_relayhost'] = "1";
$_SESSION['acl']['unlimited_quota'] = "1";
// Init Keycloak Provider
$iam_provider = identity_provider('init');
$iam_settings = identity_provider('get');
if (intval($iam_settings['periodic_sync']) != 1 && $iam_settings['import_users'] != 1) {
if ($iam_settings['authsource'] != "keycloak" || (intval($iam_settings['periodic_sync']) != 1 && intval($iam_settings['import_users']) != 1)) {
session_destroy();
exit;
}
@ -127,18 +128,18 @@ while (true) {
curl_close($ch);
if ($code != 200){
logMsg("danger", "Recieved HTTP {$code}");
logMsg("err", "Recieved HTTP {$code}");
session_destroy();
exit;
}
try {
$response = json_decode($response, true);
} catch (Exception $e) {
logMsg("danger", $e->getMessage());
logMsg("err", $e->getMessage());
break;
}
if (!is_array($response)){
logMsg("danger", "Recieved malformed response from keycloak api");
logMsg("err", "Recieved malformed response from keycloak api");
break;
}
if (count($response) == 0) {
@ -181,7 +182,7 @@ while (true) {
}
}
if (!$mbox_template){
logMsg("warning", "No matching mapper found for mailbox_template");
logMsg("warning", "No matching attribute mapping found for user " . $user['email']);
continue;
}
@ -191,14 +192,16 @@ while (true) {
mailbox('add', 'mailbox_from_template', array(
'domain' => explode('@', $user['email'])[1],
'local_part' => explode('@', $user['email'])[0],
'name' => $user['firstName'] . " " . $user['lastName'],
'authsource' => 'keycloak',
'template' => $mbox_template
));
} else if ($row) {
} else if ($row && intval($iam_settings['periodic_sync']) == 1) {
// mailbox user does exist, sync attribtues...
logMsg("info", "Syncing attributes for user " . $user['email']);
mailbox('edit', 'mailbox_from_template', array(
'username' => $user['email'],
'name' => $user['firstName'] . " " . $user['lastName'],
'template' => $mbox_template
));
} else {

View File

@ -0,0 +1,176 @@
<?php
require_once(__DIR__ . '/../web/inc/vars.inc.php');
if (file_exists(__DIR__ . '/../web/inc/vars.local.inc.php')) {
include_once(__DIR__ . '/../web/inc/vars.local.inc.php');
}
require_once __DIR__ . '/../web/inc/lib/vendor/autoload.php';
// Init database
//$dsn = $database_type . ':host=' . $database_host . ';dbname=' . $database_name;
$dsn = $database_type . ":unix_socket=" . $database_sock . ";dbname=" . $database_name;
$opt = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
try {
$pdo = new PDO($dsn, $database_user, $database_pass, $opt);
}
catch (PDOException $e) {
logMsg("err", $e->getMessage());
session_destroy();
exit;
}
// Init Redis
$redis = new Redis();
try {
if (!empty(getenv('REDIS_SLAVEOF_IP'))) {
$redis->connect(getenv('REDIS_SLAVEOF_IP'), getenv('REDIS_SLAVEOF_PORT'));
}
else {
$redis->connect('redis-mailcow', 6379);
}
}
catch (Exception $e) {
echo "Exiting: " . $e->getMessage();
session_destroy();
exit;
}
function logMsg($priority, $message, $task = "LDAP Sync") {
global $redis;
$finalMsg = array(
"time" => time(),
"priority" => $priority,
"task" => $task,
"message" => $message
);
$redis->lPush('CRON_LOG', json_encode($finalMsg));
}
// Load core functions first
require_once __DIR__ . '/../web/inc/functions.inc.php';
require_once __DIR__ . '/../web/inc/functions.auth.inc.php';
require_once __DIR__ . '/../web/inc/sessions.inc.php';
require_once __DIR__ . '/../web/inc/functions.mailbox.inc.php';
require_once __DIR__ . '/../web/inc/functions.ratelimit.inc.php';
require_once __DIR__ . '/../web/inc/functions.acl.inc.php';
$_SESSION['mailcow_cc_username'] = "admin";
$_SESSION['mailcow_cc_role'] = "admin";
$_SESSION['acl']['tls_policy'] = "1";
$_SESSION['acl']['quarantine_notification'] = "1";
$_SESSION['acl']['quarantine_category'] = "1";
$_SESSION['acl']['ratelimit'] = "1";
$_SESSION['acl']['sogo_access'] = "1";
$_SESSION['acl']['protocol_access'] = "1";
$_SESSION['acl']['mailbox_relayhost'] = "1";
$_SESSION['acl']['unlimited_quota'] = "1";
// Init Provider
$iam_provider = identity_provider('init');
$iam_settings = identity_provider('get');
if ($iam_settings['authsource'] != "ldap" || (intval($iam_settings['periodic_sync']) != 1 && intval($iam_settings['import_users']) != 1)) {
session_destroy();
exit;
}
// Set pagination variables
$start = 0;
$max = 25;
// lock sync if already running
$lock_file = '/tmp/iam-sync.lock';
if (file_exists($lock_file)) {
$lock_file_parts = explode("\n", file_get_contents($lock_file));
$pid = $lock_file_parts[0];
if (count($lock_file_parts) > 1){
$last_execution = $lock_file_parts[1];
$elapsed_time = (time() - $last_execution) / 60;
if ($elapsed_time < intval($iam_settings['sync_interval'])) {
logMsg("warning", "Sync not ready (".number_format((float)$elapsed_time, 2, '.', '')."min / ".$iam_settings['sync_interval']."min)");
session_destroy();
exit;
}
}
if (posix_kill($pid, 0)) {
logMsg("warning", "Sync is already running");
session_destroy();
exit;
} else {
unlink($lock_file);
}
}
$lock_file_handle = fopen($lock_file, 'w');
fwrite($lock_file_handle, getmypid());
fclose($lock_file_handle);
// Get ldap users
$response = $iam_provider->query()
->where($iam_settings['username_field'], "*")
->where($iam_settings['attribute_field'], "*")
->select([$iam_settings['username_field'], $iam_settings['attribute_field'], 'displayname'])
->paginate($max);
// Process the users
foreach ($response as $user) {
$mailcow_template = $user[$iam_settings['attribute_field']][0];
// try get mailbox user
$stmt = $pdo->prepare("SELECT `mailbox`.* FROM `mailbox`
INNER JOIN domain on mailbox.domain = domain.domain
WHERE `kind` NOT REGEXP 'location|thing|group'
AND `domain`.`active`='1'
AND `username` = :user");
$stmt->execute(array(':user' => $user[$iam_settings['username_field']][0]));
$row = $stmt->fetch(PDO::FETCH_ASSOC);
// check if matching attribute mapping exists
$mbox_template = null;
foreach ($iam_settings['mappers'] as $index => $mapper){
if ($mapper == $mailcow_template) {
$mbox_template = $iam_settings['templates'][$index];
break;
}
}
if (!$mbox_template){
logMsg("warning", "No matching attribute mapping found for user " . $user[$iam_settings['username_field']][0]);
continue;
}
if (!$row && intval($iam_settings['import_users']) == 1){
// mailbox user does not exist, create...
logMsg("info", "Creating user " . $user[$iam_settings['username_field']][0]);
mailbox('add', 'mailbox_from_template', array(
'domain' => explode('@', $user[$iam_settings['username_field']][0])[1],
'local_part' => explode('@', $user[$iam_settings['username_field']][0])[0],
'name' => $user['displayname'][0],
'authsource' => 'ldap',
'template' => $mbox_template
));
} else if ($row && intval($iam_settings['periodic_sync']) == 1) {
// mailbox user does exist, sync attribtues...
logMsg("info", "Syncing attributes for user " . $user[$iam_settings['username_field']][0]);
mailbox('edit', 'mailbox_from_template', array(
'username' => $user[$iam_settings['username_field']][0],
'name' => $user['displayname'][0],
'template' => $mbox_template
));
} else {
// skip mailbox user
logMsg("info", "Skipping user " . $user[$iam_settings['username_field']][0]);
}
sleep(0.025);
}
logMsg("info", "DONE!");
// add last execution time to lock file
$lock_file_handle = fopen($lock_file, 'w');
fwrite($lock_file_handle, getmypid() . "\n" . time());
fclose($lock_file_handle);
session_destroy();

View File

@ -1,3 +1,49 @@
// redirect to mailcow login form
document.addEventListener('DOMContentLoaded', function () {
var loginForm = document.forms.namedItem("loginForm");
if (loginForm) {
window.location.href = '/';
}
angularReady = false;
// Wait for the Angular components to be initialized
function waitForAngularComponents(callback) {
const targetNode = document.body;
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.addedNodes.length > 0) {
const toolbarElement = document.body.querySelector('md-toolbar');
if (toolbarElement) {
observer.disconnect();
callback();
}
}
});
});
const config = { childList: true, subtree: true };
observer.observe(targetNode, config);
}
// Usage
waitForAngularComponents(function() {
if (!angularReady){
angularReady = true;
const toolbarElement = document.body.querySelector('.md-toolbar-tools.sg-toolbar-group-last.layout-align-end-center.layout-row');
var htmlCode = '<a class="md-icon-button md-button md-ink-ripple" aria-label="mailcow" href="/user" aria-hidden="false" tabindex="-1">' +
'<md-icon class="material-icons" role="img" aria-label="build">build</md-icon>' +
'</a><a class="md-icon-button md-button md-ink-ripple" aria-label="mailcow" href="#" onclick="logout.submit()" aria-hidden="false" tabindex="-1">' +
'<md-icon class="material-icons" role="img" aria-label="settings_power">settings_power</md-icon>' +
'</a><form action="/" method="post" id="logout"><input type="hidden" name="logout"></form>';
toolbarElement.insertAdjacentHTML('beforeend', htmlCode);
}
});
});
// Custom SOGo JS
// Change the visible font-size in the editor, this does not change the font of a html message by default
@ -5,3 +51,4 @@ CKEDITOR.addCss("body {font-size: 16px !important}");
// Enable scayt by default
//CKEDITOR.config.scayt_autoStartup = true;

View File

@ -4,22 +4,31 @@ function check_login($user, $pass, $app_passwd_data = false, $extra = null) {
global $redis;
$is_internal = $extra['is_internal'];
$role = $extra['role'];
// Try validate admin
$result = admin_login($user, $pass);
if ($result !== false) return $result;
if (!isset($role) || $role == "admin") {
$result = admin_login($user, $pass);
if ($result !== false) return $result;
}
// Try validate domain admin
$result = domainadmin_login($user, $pass);
if ($result !== false) return $result;
if (!isset($role) || $role == "domain_admin") {
$result = domainadmin_login($user, $pass);
if ($result !== false) return $result;
}
// Try validate user
$result = user_login($user, $pass);
if ($result !== false) return $result;
if (!isset($role) || $role == "user") {
$result = user_login($user, $pass);
if ($result !== false) return $result;
}
// Try validate app password
$result = apppass_login($user, $pass, $app_passwd_data);
if ($result !== false) return $result;
if (!isset($role) || $role == "app") {
$result = apppass_login($user, $pass, $app_passwd_data);
if ($result !== false) return $result;
}
// skip log and only return false if it's an internal request
if ($is_internal == true) return false;
@ -175,62 +184,136 @@ function user_login($user, $pass, $extra = null){
$stmt->execute(array(':user' => $user));
$row = $stmt->fetch(PDO::FETCH_ASSOC);
// user does not exist, try call keycloak login and create user if possible via rest flow
// user does not exist, try call idp login and create user if possible via rest flow
if (!$row){
$iam_settings = identity_provider('get');
if ($iam_settings['authsource'] == 'keycloak' && intval($iam_settings['mailpassword_flow']) == 1){
$result = keycloak_mbox_login_rest($user, $pass, $iam_settings, array('is_internal' => $is_internal, 'create' => true));
if ($result !== false) return $result;
} else if ($iam_settings['authsource'] == 'ldap') {
$result = ldap_mbox_login($user, $pass, $iam_settings, array('is_internal' => $is_internal, 'create' => true));
if ($result !== false) return $result;
}
}
if ($row['active'] != 1) {
return false;
}
if ($row['authsource'] == 'keycloak'){
// user authsource is keycloak, try using via rest flow
$iam_settings = identity_provider('get');
if (intval($iam_settings['mailpassword_flow']) == 1){
$result = keycloak_mbox_login_rest($user, $pass, $iam_settings, array('is_internal' => $is_internal));
switch ($row['authsource']) {
case 'keycloak':
// user authsource is keycloak, try using via rest flow
$iam_settings = identity_provider('get');
if (intval($iam_settings['mailpassword_flow']) == 1){
$result = keycloak_mbox_login_rest($user, $pass, $iam_settings, array('is_internal' => $is_internal));
if ($result !== false) {
// check for tfa authenticators
$authenticators = get_tfa($user);
if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0 && !$is_internal) {
// authenticators found, init TFA flow
$_SESSION['pending_mailcow_cc_username'] = $user;
$_SESSION['pending_mailcow_cc_role'] = "user";
$_SESSION['pending_tfa_methods'] = $authenticators['additional'];
unset($_SESSION['ldelay']);
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $user, '*'),
'msg' => array('logged_in_as', $user)
);
return "pending";
} else if (!isset($authenticators['additional']) || !is_array($authenticators['additional']) || count($authenticators['additional']) == 0) {
// no authenticators found, login successfull
if (!$is_internal){
unset($_SESSION['ldelay']);
// Reactivate TFA if it was set to "deactivate TFA for next login"
$stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
$stmt->execute(array(':user' => $user));
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $user, '*'),
'msg' => array('logged_in_as', $user)
);
}
return "user";
}
}
return $result;
} else {
return false;
}
break;
case 'ldap':
// user authsource is ldap
$iam_settings = identity_provider('get');
$result = ldap_mbox_login($user, $pass, $iam_settings, array('is_internal' => $is_internal));
if ($result !== false) {
// check for tfa authenticators
$authenticators = get_tfa($user);
if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0 && !$is_internal) {
// authenticators found, init TFA flow
$_SESSION['pending_mailcow_cc_username'] = $user;
$_SESSION['pending_mailcow_cc_role'] = "user";
$_SESSION['pending_tfa_methods'] = $authenticators['additional'];
unset($_SESSION['ldelay']);
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $user, '*'),
'msg' => array('logged_in_as', $user)
);
return "pending";
} else if (!isset($authenticators['additional']) || !is_array($authenticators['additional']) || count($authenticators['additional']) == 0) {
// no authenticators found, login successfull
if (!$is_internal){
unset($_SESSION['ldelay']);
// Reactivate TFA if it was set to "deactivate TFA for next login"
$stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
$stmt->execute(array(':user' => $user));
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $user, '*'),
'msg' => array('logged_in_as', $user)
);
}
return "user";
}
}
return $result;
} else {
return false;
}
break;
default:
// verify password
if (verify_hash($row['password'], $pass) !== false) {
// check for tfa authenticators
$authenticators = get_tfa($user);
if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0 && !$is_internal) {
// authenticators found, init TFA flow
$_SESSION['pending_mailcow_cc_username'] = $user;
$_SESSION['pending_mailcow_cc_role'] = "user";
$_SESSION['pending_tfa_methods'] = $authenticators['additional'];
unset($_SESSION['ldelay']);
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $user, '*'),
'msg' => array('logged_in_as', $user)
);
return "pending";
} else if (!isset($authenticators['additional']) || !is_array($authenticators['additional']) || count($authenticators['additional']) == 0) {
// no authenticators found, login successfull
if (!$is_internal){
unset($_SESSION['ldelay']);
// Reactivate TFA if it was set to "deactivate TFA for next login"
$stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
$stmt->execute(array(':user' => $user));
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $user, '*'),
'msg' => array('logged_in_as', $user)
);
}
return "user";
}
}
break;
}
// verify password
if (verify_hash($row['password'], $pass) !== false) {
// check for tfa authenticators
$authenticators = get_tfa($user);
if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0 && !$is_internal) {
// authenticators found, init TFA flow
$_SESSION['pending_mailcow_cc_username'] = $user;
$_SESSION['pending_mailcow_cc_role'] = "user";
$_SESSION['pending_tfa_methods'] = $authenticators['additional'];
unset($_SESSION['ldelay']);
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $user, '*'),
'msg' => array('logged_in_as', $user)
);
return "pending";
} else if (!isset($authenticators['additional']) || !is_array($authenticators['additional']) || count($authenticators['additional']) == 0) {
// no authenticators found, login successfull
if (!$is_internal){
unset($_SESSION['ldelay']);
// Reactivate TFA if it was set to "deactivate TFA for next login"
$stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
$stmt->execute(array(':user' => $user));
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $user, '*'),
'msg' => array('logged_in_as', $user)
);
}
return "user";
}
}
return false;
}
function apppass_login($user, $pass, $app_passwd_data, $extra = null){
@ -372,11 +455,6 @@ function keycloak_mbox_login_rest($user, $pass, $iam_settings, $extra = null){
return false;
} else if (!$create) {
// login success - dont create mailbox
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $user, '*'),
'msg' => array('logged_in_as', $user)
);
return 'user';
}
@ -388,16 +466,67 @@ function keycloak_mbox_login_rest($user, $pass, $iam_settings, $extra = null){
$create_res = mailbox('add', 'mailbox_from_template', array(
'domain' => explode('@', $user)[1],
'local_part' => explode('@', $user)[0],
'name' => $user_res['firstName'] . " " . $user_res['lastName'],
'authsource' => 'keycloak',
'template' => $iam_settings['mappers'][$mapper_key]
'template' => $iam_settings['templates'][$mapper_key]
));
if (!$create_res) return false;
return 'user';
}
function ldap_mbox_login($user, $pass, $iam_settings, $extra = null){
global $pdo;
global $iam_provider;
$is_internal = $extra['is_internal'];
$create = $extra['create'];
if (!filter_var($user, FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $user))) {
if (!$is_internal){
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $user, '*'),
'msg' => 'malformed_username'
);
}
return false;
}
try {
$user_res = $iam_provider->query()
->where($iam_settings['username_field'], '=', $user)
->select([$iam_settings['username_field'], $iam_settings['attribute_field'], 'displayname', 'distinguishedname'])
->firstOrFail();
} catch (Exception $e) {
return false;
}
if (!$iam_provider->auth()->attempt($user_res['distinguishedname'][0], $pass)) {
return false;
}
// get mapped template, if not set return false
// also return false if no mappers were defined
$user_template = $user_res[$iam_settings['attribute_field']][0];
if ($create && (empty($iam_settings['mappers']) || !$user_template)){
return false;
} else if (!$create) {
// login success - dont create mailbox
return 'user';
}
// check if matching attribute exist
$mapper_key = array_search($user_template, $iam_settings['mappers']);
if ($mapper_key === false) return false;
// create mailbox
$create_res = mailbox('add', 'mailbox_from_template', array(
'domain' => explode('@', $user)[1],
'local_part' => explode('@', $user)[0],
'name' => $user_res['displayname'][0],
'authsource' => 'ldap',
'template' => $iam_settings['templates'][$mapper_key]
));
if (!$create_res) return false;
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $user, '*'),
'msg' => array('logged_in_as', $user)
);
return 'user';
}

View File

@ -840,6 +840,11 @@ function update_sogo_static_view($mailbox = null) {
}
}
// generate random password for sogo to deny direct login
$random_password = base64_encode(openssl_random_pseudo_bytes(24));
$random_salt = base64_encode(openssl_random_pseudo_bytes(16));
$random_hash = '{SSHA256}' . base64_encode(hash('sha256', base64_decode($password) . $salt, true) . $salt);
$subquery = "GROUP BY mailbox.username";
if ($mailbox_exists) {
$subquery = "AND mailbox.username = :mailbox";
@ -849,13 +854,7 @@ function update_sogo_static_view($mailbox = null) {
mailbox.username,
mailbox.domain,
mailbox.username,
CASE
WHEN mailbox.authsource IS NOT NULL AND mailbox.authsource <> 'mailcow' THEN '{SSHA256}A123A123A321A321A321B321B321B123B123B321B432F123E321123123321321'
ELSE
IF(JSON_UNQUOTE(JSON_VALUE(attributes, '$.force_pw_update')) = '0',
IF(JSON_UNQUOTE(JSON_VALUE(attributes, '$.sogo_access')) = 1, password, '{SSHA256}A123A123A321A321A321B321B321B123B123B321B432F123E321123123321321'),
'{SSHA256}A123A123A321A321A321B321B321B123B123B321B432F123E321123123321321')
END AS c_password,
:random_hash,
mailbox.name,
mailbox.username,
IFNULL(GROUP_CONCAT(ga.aliases ORDER BY ga.aliases SEPARATOR ' '), ''),
@ -886,9 +885,15 @@ function update_sogo_static_view($mailbox = null) {
if ($mailbox_exists) {
$stmt = $pdo->prepare($query);
$stmt->execute(array(':mailbox' => $mailbox));
$stmt->execute(array(
':random_hash' => $random_hash,
':mailbox' => $mailbox
));
} else {
$stmt = $pdo->query($query);
$stmt = $pdo->prepare($query);
$stmt->execute(array(
':random_hash' => $random_hash
));
}
$stmt = $pdo->query("DELETE FROM _sogo_static_view WHERE `c_uid` NOT IN (SELECT `username` FROM `mailbox` WHERE `active` = '1');");
@ -2129,12 +2134,18 @@ function identity_provider($_action, $_data = null, $_extra = null) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $data_log),
'msg' => array('required_data_missing', $setting)
'msg' => array('required_data_missing', '')
);
return false;
}
$available_authsources = array(
"keycloak",
"generic-oidc",
"ldap"
);
$_data['authsource'] = strtolower($_data['authsource']);
if ($_data['authsource'] != "keycloak" && $_data['authsource'] != "generic-oidc"){
if (!in_array($_data['authsource'], $available_authsources)){
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $data_log),
@ -2158,20 +2169,32 @@ function identity_provider($_action, $_data = null, $_extra = null) {
return false;
}
if ($_data['authsource'] == "keycloak") {
$_data['server_url'] = (!empty($_data['server_url'])) ? rtrim($_data['server_url'], '/') : null;
$_data['mailpassword_flow'] = isset($_data['mailpassword_flow']) ? intval($_data['mailpassword_flow']) : 0;
$_data['periodic_sync'] = isset($_data['periodic_sync']) ? intval($_data['periodic_sync']) : 0;
$_data['import_users'] = isset($_data['import_users']) ? intval($_data['import_users']) : 0;
$_data['sync_interval'] = isset($_data['sync_interval']) ? intval($_data['sync_interval']) : 15;
$_data['sync_interval'] = $_data['sync_interval'] < 1 ? 1 : $_data['sync_interval'];
$required_settings = array('authsource', 'server_url', 'realm', 'client_id', 'client_secret', 'redirect_url', 'version', 'mailpassword_flow', 'periodic_sync', 'import_users', 'sync_interval');
} else if ($_data['authsource'] == "generic-oidc") {
$_data['authorize_url'] = (!empty($_data['authorize_url'])) ? $_data['authorize_url'] : null;
$_data['token_url'] = (!empty($_data['token_url'])) ? $_data['token_url'] : null;
$_data['userinfo_url'] = (!empty($_data['userinfo_url'])) ? $_data['userinfo_url'] : null;
$_data['client_scopes'] = (!empty($_data['client_scopes'])) ? $_data['client_scopes'] : "openid profile email";
$required_settings = array('authsource', 'authorize_url', 'token_url', 'client_id', 'client_secret', 'redirect_url', 'userinfo_url', 'client_scopes');
switch ($_data['authsource']) {
case "keycloak":
$_data['server_url'] = (!empty($_data['server_url'])) ? rtrim($_data['server_url'], '/') : null;
$_data['mailpassword_flow'] = isset($_data['mailpassword_flow']) ? intval($_data['mailpassword_flow']) : 0;
$_data['periodic_sync'] = isset($_data['periodic_sync']) ? intval($_data['periodic_sync']) : 0;
$_data['import_users'] = isset($_data['import_users']) ? intval($_data['import_users']) : 0;
$_data['sync_interval'] = (!empty($_data['sync_interval'])) ? intval($_data['sync_interval']) : 15;
$_data['sync_interval'] = $_data['sync_interval'] < 1 ? 1 : $_data['sync_interval'];
$required_settings = array('authsource', 'server_url', 'realm', 'client_id', 'client_secret', 'redirect_url', 'version', 'mailpassword_flow', 'periodic_sync', 'import_users', 'sync_interval');
break;
case "generic-oidc":
$_data['authorize_url'] = (!empty($_data['authorize_url'])) ? $_data['authorize_url'] : null;
$_data['token_url'] = (!empty($_data['token_url'])) ? $_data['token_url'] : null;
$_data['userinfo_url'] = (!empty($_data['userinfo_url'])) ? $_data['userinfo_url'] : null;
$_data['client_scopes'] = (!empty($_data['client_scopes'])) ? $_data['client_scopes'] : "openid profile email";
$required_settings = array('authsource', 'authorize_url', 'token_url', 'client_id', 'client_secret', 'redirect_url', 'userinfo_url', 'client_scopes');
break;
case "ldap":
$_data['port'] = (!empty($_data['port'])) ? intval($_data['port']) : 389;
$_data['username_field'] = (!empty($_data['username_field'])) ? $_data['username_field'] : "mail";
$_data['periodic_sync'] = isset($_data['periodic_sync']) ? intval($_data['periodic_sync']) : 0;
$_data['import_users'] = isset($_data['import_users']) ? intval($_data['import_users']) : 0;
$_data['sync_interval'] = (!empty($_data['sync_interval'])) ? intval($_data['sync_interval']) : 15;
$_data['sync_interval'] = $_data['sync_interval'] < 1 ? 1 : $_data['sync_interval'];
$required_settings = array('authsource', 'host', 'port', 'basedn', 'username_field', 'attribute_field', 'binddn', 'bindpass', 'periodic_sync', 'import_users', 'sync_interval');
break;
}
$pdo->beginTransaction();
@ -2234,30 +2257,56 @@ function identity_provider($_action, $_data = null, $_extra = null) {
return false;
}
if ($_data['authsource'] == 'keycloak') {
$url = "{$_data['server_url']}/realms/{$_data['realm']}/protocol/openid-connect/token";
} else {
$url = $_data['token_url'];
}
$req = http_build_query(array(
'grant_type' => 'client_credentials',
'client_id' => $_data['client_id'],
'client_secret' => $_data['client_secret']
));
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_TIMEOUT, 7);
curl_setopt($curl, CURLOPT_POST, 1);
curl_setopt($curl, CURLOPT_POSTFIELDS, $req);
curl_setopt($curl, CURLOPT_HTTPHEADER, array('Content-Type: application/x-www-form-urlencoded'));
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
$res = curl_exec($curl);
$code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
curl_close ($curl);
if ($code != 200) {
return false;
switch ($_data['authsource']) {
case 'keycloak':
case 'generic-oidc':
if ($_data['authsource'] == 'keycloak') {
$url = "{$_data['server_url']}/realms/{$_data['realm']}/protocol/openid-connect/token";
} else {
$url = $_data['token_url'];
}
$req = http_build_query(array(
'grant_type' => 'client_credentials',
'client_id' => $_data['client_id'],
'client_secret' => $_data['client_secret']
));
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_TIMEOUT, 7);
curl_setopt($curl, CURLOPT_POST, 1);
curl_setopt($curl, CURLOPT_POSTFIELDS, $req);
curl_setopt($curl, CURLOPT_HTTPHEADER, array('Content-Type: application/x-www-form-urlencoded'));
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
$res = curl_exec($curl);
$code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
curl_close ($curl);
if ($code != 200) {
return false;
}
break;
case 'ldap':
if (!$_data['host'] || !$_data['port'] || !$_data['basedn'] ||
!$_data['binddn'] || !$_data['bindpass']){
return false;
}
$provider = new \LdapRecord\Connection([
'hosts' => [$_data['host']],
'port' => $_data['port'],
'base_dn' => $_data['basedn'],
'username' => $_data['binddn'],
'password' => $_data['bindpass']
]);
try {
$provider->connect();
} catch (TypeError $e) {
return false;
} catch (\LdapRecord\Auth\BindException $e) {
return false;
}
break;
}
return true;
break;
case "delete":
@ -2295,40 +2344,70 @@ function identity_provider($_action, $_data = null, $_extra = null) {
case "init":
$iam_settings = identity_provider('get');
$provider = null;
if ($iam_settings['authsource'] == 'keycloak'){
if ($iam_settings['server_url'] && $iam_settings['realm'] && $iam_settings['client_id'] &&
switch ($iam_settings['authsource']) {
case "keycloak":
if ($iam_settings['server_url'] && $iam_settings['realm'] && $iam_settings['client_id'] &&
$iam_settings['client_secret'] && $iam_settings['redirect_url'] && $iam_settings['version']){
$provider = new Stevenmaguire\OAuth2\Client\Provider\Keycloak([
'authServerUrl' => $iam_settings['server_url'],
'realm' => $iam_settings['realm'],
'clientId' => $iam_settings['client_id'],
'clientSecret' => $iam_settings['client_secret'],
'redirectUri' => $iam_settings['redirect_url'],
'version' => $iam_settings['version'],
// 'encryptionAlgorithm' => 'RS256', // optional
// 'encryptionKeyPath' => '../key.pem' // optional
// 'encryptionKey' => 'contents_of_key_or_certificate' // optional
]);
}
}
else if ($iam_settings['authsource'] == 'generic-oidc'){
if ($iam_settings['client_id'] && $iam_settings['client_secret'] && $iam_settings['redirect_url'] &&
$provider = new Stevenmaguire\OAuth2\Client\Provider\Keycloak([
'authServerUrl' => $iam_settings['server_url'],
'realm' => $iam_settings['realm'],
'clientId' => $iam_settings['client_id'],
'clientSecret' => $iam_settings['client_secret'],
'redirectUri' => $iam_settings['redirect_url'],
'version' => $iam_settings['version'],
// 'encryptionAlgorithm' => 'RS256', // optional
// 'encryptionKeyPath' => '../key.pem' // optional
// 'encryptionKey' => 'contents_of_key_or_certificate' // optional
]);
}
break;
case "generic-oidc":
if ($iam_settings['client_id'] && $iam_settings['client_secret'] && $iam_settings['redirect_url'] &&
$iam_settings['authorize_url'] && $iam_settings['token_url'] && $iam_settings['userinfo_url']){
$provider = new \League\OAuth2\Client\Provider\GenericProvider([
'clientId' => $iam_settings['client_id'],
'clientSecret' => $iam_settings['client_secret'],
'redirectUri' => $iam_settings['redirect_url'],
'urlAuthorize' => $iam_settings['authorize_url'],
'urlAccessToken' => $iam_settings['token_url'],
'urlResourceOwnerDetails' => $iam_settings['userinfo_url'],
'scopes' => $iam_settings['client_scopes']
]);
}
$provider = new \League\OAuth2\Client\Provider\GenericProvider([
'clientId' => $iam_settings['client_id'],
'clientSecret' => $iam_settings['client_secret'],
'redirectUri' => $iam_settings['redirect_url'],
'urlAuthorize' => $iam_settings['authorize_url'],
'urlAccessToken' => $iam_settings['token_url'],
'urlResourceOwnerDetails' => $iam_settings['userinfo_url'],
'scopes' => $iam_settings['client_scopes']
]);
}
break;
case "ldap":
if ($iam_settings['host'] && $iam_settings['port'] && $iam_settings['basedn'] &&
$iam_settings['binddn'] && $iam_settings['bindpass']){
$provider = new \LdapRecord\Connection([
'hosts' => [$iam_settings['host']],
'port' => $iam_settings['port'],
'base_dn' => $iam_settings['basedn'],
'username' => $iam_settings['binddn'],
'password' => $iam_settings['bindpass']
]);
try {
$provider->connect();
} catch (TypeError $e) {
$provider = null;
}
}
break;
}
return $provider;
break;
case "verify-sso":
$provider = $_data['iam_provider'];
$iam_settings = identity_provider('get');
if ($iam_settings['authsource'] != 'keycloak' && $iam_settings['authsource'] != 'generic-oidc'){
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__),
'msg' => array('login_failed', "no OIDC provider configured")
);
return false;
}
try {
$token = $provider->getAccessToken('authorization_code', ['code' => $_GET['code']]);
@ -2358,8 +2437,7 @@ function identity_provider($_action, $_data = null, $_extra = null) {
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if ($row){
// success
$_SESSION['mailcow_cc_username'] = $info['email'];
$_SESSION['mailcow_cc_role'] = "user";
set_user_loggedin_session($info['email']);
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role']),
@ -2370,9 +2448,8 @@ function identity_provider($_action, $_data = null, $_extra = null) {
// get mapped template, if not set return false
// also return false if no mappers were defined
$provider = identity_provider('get');
$user_template = $info['mailcow_template'];
if (empty($provider['mappers']) || empty($user_template)){
if (empty($iam_settings['mappers']) || empty($user_template)){
clear_session();
$_SESSION['return'][] = array(
'type' => 'danger',
@ -2383,7 +2460,7 @@ function identity_provider($_action, $_data = null, $_extra = null) {
}
// check if matching attribute exist
$mapper_key = array_search($user_template, $provider['mappers']);
$mapper_key = array_search($user_template, $iam_settings['mappers']);
if ($mapper_key === false) {
clear_session();
$_SESSION['return'][] = array(
@ -2398,8 +2475,9 @@ function identity_provider($_action, $_data = null, $_extra = null) {
$create_res = mailbox('add', 'mailbox_from_template', array(
'domain' => explode('@', $info['email'])[1],
'local_part' => explode('@', $info['email'])[0],
'authsource' => identity_provider('get')['authsource'],
'template' => $provider['templates'][$mapper_key]
'name' => $info['firstName'] . " " . $info['lastName'],
'authsource' => $iam_settings['authsource'],
'template' => $iam_settings['templates'][$mapper_key]
));
if (!$create_res){
clear_session();
@ -2411,8 +2489,7 @@ function identity_provider($_action, $_data = null, $_extra = null) {
return false;
}
$_SESSION['mailcow_cc_username'] = $info['email'];
$_SESSION['mailcow_cc_role'] = "user";
set_user_loggedin_session($info['email']);
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role']),
@ -2429,10 +2506,11 @@ function identity_provider($_action, $_data = null, $_extra = null) {
$_SESSION['iam_refresh_token'] = $token->getRefreshToken();
$info = $provider->getResourceOwner($token)->toArray();
} catch (Throwable $e) {
clear_session();
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__),
'msg' => array('login_failed', $e->getMessage())
'msg' => array('refresh_login_failed', $e->getMessage())
);
return false;
}
@ -2452,6 +2530,9 @@ function identity_provider($_action, $_data = null, $_extra = null) {
return true;
break;
case "get-redirect":
$iam_settings = identity_provider('get');
if ($iam_settings['authsource'] != 'keycloak' && $iam_settings['authsource'] != 'generic-oidc')
return false;
$provider = $_data['iam_provider'];
$authUrl = $provider->getAuthorizationUrl();
$_SESSION['oauth2state'] = $provider->getState();
@ -2522,7 +2603,16 @@ function clear_session(){
session_destroy();
session_write_close();
}
function set_user_loggedin_session($user) {
$_SESSION['mailcow_cc_username'] = $user;
$_SESSION['mailcow_cc_role'] = 'user';
$sogo_sso_pass = file_get_contents("/etc/sogo-sso/sogo-sso.pass");
$_SESSION['sogo-sso-user-allowed'][] = $user;
$_SESSION['sogo-sso-pass'] = $sogo_sso_pass;
unset($_SESSION['pending_mailcow_cc_username']);
unset($_SESSION['pending_mailcow_cc_role']);
unset($_SESSION['pending_tfa_methods']);
}
function get_logs($application, $lines = false) {
if ($lines === false) {
$lines = $GLOBALS['LOG_LINES'] - 1;

View File

@ -1019,7 +1019,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
);
return false;
}
if (in_array($_data['authsource'], array('mailcow', 'keycloak', 'generic-oidc'))){
if (in_array($_data['authsource'], array('mailcow', 'keycloak', 'generic-oidc', 'ldap'))){
$authsource = $_data['authsource'];
}
if (empty($name)) {
@ -2944,7 +2944,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$tags = (is_array($_data['tags']) ? $_data['tags'] : array());
$attribute_hash = (!empty($_data['attribute_hash'])) ? $_data['attribute_hash'] : '';
$authsource = $is_now['authsource'];
if (in_array($_data['authsource'], array('mailcow', 'keycloak', 'generic-oidc'))){
if (in_array($_data['authsource'], array('mailcow', 'keycloak', 'generic-oidc', 'ldap'))){
$authsource = $_data['authsource'];
}
}
@ -3285,11 +3285,13 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$attribute_hash = sha1(json_encode($mbox_template_data["attributes"]));
$is_now = mailbox('get', 'mailbox_details', $_data['username']);
if ($is_now['attributes']['attribute_hash'] == $attribute_hash)
$name = ltrim(rtrim($_data['name'], '>'), '<');
if ($is_now['attributes']['attribute_hash'] == $attribute_hash && $is_now['name'] == $name)
return true;
$mbox_template_data = json_decode($mbox_template_data["attributes"], true);
$mbox_template_data['attribute_hash'] = $attribute_hash;
$mbox_template_data['name'] = $name;
$quarantine_attributes = array('username' => $_data['username']);
$tls_attributes = array('username' => $_data['username']);
$ratelimit_attributes = array('object' => $_data['username']);

View File

@ -362,7 +362,7 @@ function init_db_schema() {
"custom_attributes" => "JSON NOT NULL DEFAULT ('{}')",
"kind" => "VARCHAR(100) NOT NULL DEFAULT ''",
"multiple_bookings" => "INT NOT NULL DEFAULT -1",
"authsource" => "ENUM('mailcow', 'keycloak', 'generic-oidc') DEFAULT 'mailcow'",
"authsource" => "ENUM('mailcow', 'keycloak', 'generic-oidc', 'ldap') DEFAULT 'mailcow'",
"created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
"modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP",
"active" => "TINYINT(1) NOT NULL DEFAULT '1'"

View File

@ -94,6 +94,8 @@ if (isset($_POST["logout"])) {
if (isset($_SESSION["dual-login"])) {
$_SESSION["mailcow_cc_username"] = $_SESSION["dual-login"]["username"];
$_SESSION["mailcow_cc_role"] = $_SESSION["dual-login"]["role"];
unset($_SESSION['sogo-sso-user-allowed']);
unset($_SESSION['sogo-sso-pass']);
unset($_SESSION["dual-login"]);
header("Location: /mailbox");
exit();

View File

@ -4,6 +4,7 @@ if ($iam_provider){
if (isset($_GET['iam_sso'])){
// redirect for sso
$redirect_uri = identity_provider('get-redirect', array('iam_provider' => $iam_provider));
$redirect_uri = !empty($redirect_uri) ? $redirect_uri : '/';
header('Location: ' . $redirect_uri);
die();
}
@ -12,9 +13,9 @@ if ($iam_provider){
$isRefreshed = identity_provider('refresh-token', array('iam_provider' => $iam_provider));
if (!$isRefreshed){
// Session could not be refreshed, clear and redirect to provider
clear_session();
// Session could not be refreshed, redirect to provider
$redirect_uri = identity_provider('get-redirect', array('iam_provider' => $iam_provider));
$redirect_uri = !empty($redirect_uri) ? $redirect_uri : '/';
header('Location: ' . $redirect_uri);
die();
}
@ -39,13 +40,16 @@ if (!empty($_GET['sso_token'])) {
if (isset($_POST["verify_tfa_login"])) {
if (verify_tfa_login($_SESSION['pending_mailcow_cc_username'], $_POST)) {
$_SESSION['mailcow_cc_username'] = $_SESSION['pending_mailcow_cc_username'];
$_SESSION['mailcow_cc_role'] = $_SESSION['pending_mailcow_cc_role'];
unset($_SESSION['pending_mailcow_cc_username']);
unset($_SESSION['pending_mailcow_cc_role']);
unset($_SESSION['pending_tfa_methods']);
header("Location: /user");
set_user_loggedin_session($_SESSION['pending_mailcow_cc_username']);
$user_details = mailbox("get", "mailbox_details", $_SESSION['mailcow_cc_username']);
$is_dual = (!empty($_SESSION["dual-login"]["username"])) ? true : false;
if (intval($user_details['attributes']['sogo_access']) == 1 && !$is_dual) {
header("Location: /SOGo/so/{$_SESSION['mailcow_cc_username']}");
die();
} else {
header("Location: /user");
die();
}
} else {
unset($_SESSION['pending_mailcow_cc_username']);
unset($_SESSION['pending_mailcow_cc_role']);
@ -70,10 +74,10 @@ if (isset($_POST["quick_delete"])) {
}
if (isset($_POST["login_user"]) && isset($_POST["pass_user"])) {
$login_user = strtolower(trim($_POST["login_user"]));
$as = check_login($login_user, $_POST["pass_user"]);
$login_user = strtolower(trim($_POST["login_user"]));
$as = check_login($login_user, $_POST["pass_user"]);
if ($as == "admin") {
if ($as == "admin") {
$_SESSION['mailcow_cc_username'] = $login_user;
$_SESSION['mailcow_cc_role'] = "admin";
header("Location: /admin");
@ -84,19 +88,27 @@ if (isset($_POST["login_user"]) && isset($_POST["pass_user"])) {
header("Location: /mailbox");
}
elseif ($as == "user") {
$_SESSION['mailcow_cc_username'] = $login_user;
$_SESSION['mailcow_cc_role'] = "user";
$http_parameters = explode('&', $_SESSION['index_query_string']);
unset($_SESSION['index_query_string']);
if (in_array('mobileconfig', $http_parameters)) {
if (in_array('only_email', $http_parameters)) {
header("Location: /mobileconfig.php?only_email");
die();
}
header("Location: /mobileconfig.php");
set_user_loggedin_session($login_user);
$http_parameters = explode('&', $_SESSION['index_query_string']);
unset($_SESSION['index_query_string']);
if (in_array('mobileconfig', $http_parameters)) {
if (in_array('only_email', $http_parameters)) {
header("Location: /mobileconfig.php?only_email");
die();
}
header("Location: /user");
header("Location: /mobileconfig.php");
die();
}
$user_details = mailbox("get", "mailbox_details", $login_user);
$is_dual = (!empty($_SESSION["dual-login"]["username"])) ? true : false;
if (intval($user_details['attributes']['sogo_access']) == 1 && !$is_dual) {
header("Location: /SOGo/so/{$login_user}");
die();
} else {
header("Location: /user");
die();
}
}
elseif ($as != "pending") {
unset($_SESSION['pending_mailcow_cc_username']);

View File

@ -122,8 +122,8 @@ $SHOW_DKIM_PRIV_KEYS = false;
$MAILCOW_APPS = array(
array(
'name' => 'Webmail',
'link' => '/SOGo/so/',
'user_link' => '/sogo-auth.php?login=%u',
'link' => '/SOGo/so',
'user_link' => '/SOGo/so',
'hide' => true
)
);
@ -179,7 +179,7 @@ $MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_out'] = false;
// Force password change on next login (only allows login to mailcow UI)
$MAILBOX_DEFAULT_ATTRIBUTES['force_pw_update'] = false;
// Enable SOGo access (set to false to disable access by default)
// Enable SOGo access - Users will be redirected to SOGo after login (set to false to disable redirect by default)
$MAILBOX_DEFAULT_ATTRIBUTES['sogo_access'] = true;
// Send notification when quarantine is not empty (never, hourly, daily, weekly)

View File

@ -15,8 +15,14 @@ elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == '
header('Location: /mailbox');
exit();
}
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
header('Location: /user');
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
$user_details = mailbox("get", "mailbox_details", $_SESSION['mailcow_cc_username']);
$is_dual = (!empty($_SESSION["dual-login"]["username"])) ? true : false;
if (intval($user_details['attributes']['sogo_access']) == 1 && !$is_dual) {
header("Location: /SOGo/so/{$_SESSION['mailcow_cc_username']}");
} else {
header("Location: /user");
}
exit();
}
@ -24,12 +30,18 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
$_SESSION['index_query_string'] = $_SERVER['QUERY_STRING'];
$has_iam_sso = false;
if ($iam_provider){
$has_iam_sso = identity_provider("get-redirect", array('iam_provider' => $iam_provider)) ? true : false;
}
$template = 'index.twig';
$template_data = [
'oauth2_request' => @$_SESSION['oauth2_request'],
'is_mobileconfig' => str_contains($_SESSION['index_query_string'], 'mobileconfig'),
'login_delay' => @$_SESSION['ldelay'],
'has_iam_sso' => ($iam_provider) ? true : false
'has_iam_sso' => $has_iam_sso
];
$js_minifier->add('/web/js/site/index.js');

View File

@ -808,6 +808,26 @@ jQuery(function($){
$(this).parent().parent().parent().remove();
});
});
$('.iam_rolemap_add_ldap').click(async function(e){
e.preventDefault();
var parent = $('#iam_ldap_mapping_list')
$(parent).children().last().clone().appendTo(parent);
var newChild = $(parent).children().last();
$(newChild).find('input').val('');
$(newChild).find('.dropdown-toggle').remove();
$(newChild).find('.dropdown-menu').remove();
$(newChild).find('.bs-title-option').remove();
$(newChild).find('select').selectpicker('destroy');
$(newChild).find('select').selectpicker();
$('.iam_ldap_rolemap_del').off('click');
$('.iam_ldap_rolemap_del').click(async function(e){
e.preventDefault();
if ($(this).parent().parent().parent().parent().children().length > 1)
$(this).parent().parent().parent().remove();
});
});
$('.iam_keycloak_rolemap_del').click(async function(e){
e.preventDefault();
if ($(this).parent().parent().parent().parent().children().length > 1)
@ -818,15 +838,26 @@ jQuery(function($){
if ($(this).parent().parent().parent().parent().children().length > 1)
$(this).parent().parent().parent().remove();
});
$('.iam_ldap_rolemap_del').click(async function(e){
e.preventDefault();
if ($(this).parent().parent().parent().parent().children().length > 1)
$(this).parent().parent().parent().remove();
});
// selecting identity provider
$('#iam_provider').on('change', function(){
// toggle password fields
if (this.value === 'keycloak'){
$('#keycloak_settings').removeClass('d-none');
$('#generic_oidc_settings').addClass('d-none');
$('#ldap_settings').addClass('d-none');
} else if (this.value === 'generic-oidc') {
$('#keycloak_settings').addClass('d-none');
$('#generic_oidc_settings').removeClass('d-none');
$('#keycloak_settings').addClass('d-none');
$('#ldap_settings').addClass('d-none');
} else if (this.value === 'ldap') {
$('#ldap_settings').removeClass('d-none');
$('#generic_oidc_settings').addClass('d-none');
$('#keycloak_settings').addClass('d-none');
}
});
});

View File

@ -648,8 +648,8 @@
"sieve_desc": "Krátký popis",
"sieve_type": "Typ filtru",
"skipcrossduplicates": "Přeskočit duplicitní zprávy (\"první přijde, první mele\")",
"sogo_access": "Udělit přímý přihlašovací přístup do služby SOGo",
"sogo_access_info": "Jednotné přihlášení (SSO) z mail UI zůstává funkční. Toto nastavení neovlivňuje přístup ke všem ostatním službám ani neodstraňuje či nemění stávající profil uživatele SOGo.",
"sogo_access": "Přímé předání na SOGo",
"sogo_access_info": "Po přihlášení je uživatel automaticky přesměrován do služby SOGo.",
"sogo_visible": "Alias dostupný v SOGo",
"sogo_visible_info": "Tato volba určuje objekty, jež lze zobrazit v SOGo (sdílené nebo nesdílené aliasy, jež ukazuje alespoň na jednu schránku).",
"spam_alias": "Vytvořit nebo změnit dočasné aliasy",
@ -1143,7 +1143,7 @@
"delete_ays": "Potvrďte odstranění.",
"direct_aliases": "Přímé aliasy",
"direct_aliases_desc": "Na přímé aliasy se uplatňuje filtr spamu a nastavení pravidel TLS",
"direct_protocol_access": "Tento uživatel mailové schránky má <b>přímý externí přístup</b> k následujícím protokolům a aplikacím. Toto nastavení je řízeno správcem. Pro udělení přístupu k jednotlivým protokolům a aplikacím lze vytvořit hesla aplikací.<br>Tlačítko \" Přihlaste se do webmailu\" zajišťuje jednotné přihlášení k SOGo a je vždy k dispozici.",
"direct_protocol_access": "Tento uživatel mailové schránky má <b>přímý externí přístup</b> k následujícím protokolům a aplikacím. Toto nastavení je řízeno správcem. Pro udělení přístupu k jednotlivým protokolům a aplikacím lze vytvořit hesla aplikací.<br>Tlačítko \"Webmailu\" zajišťuje jednotné přihlášení k SOGo a je vždy k dispozici.",
"eas_reset": "Smazat mezipaměť zařízení ActiveSync",
"eas_reset_help": "Obnovení mezipaměti zařízení pomůže zpravidla obnovit poškozený profil služby ActiveSync.<br><b>Upozornění:</b> Všechna data budou opětovně stažena!",
"eas_reset_now": "Smazat",
@ -1184,7 +1184,7 @@
"no_last_login": "Žádný záznam o přihlášení",
"no_record": "Žádný záznam",
"open_logs": "Otevřít záznam",
"open_webmail_sso": "Přihlaste se do webmailu",
"open_webmail_sso": "Webmailu",
"password": "Heslo",
"password_now": "Současné heslo (pro potvrzení změny)",
"password_repeat": "Heslo (znovu)",

View File

@ -675,8 +675,8 @@
"sieve_desc": "Kurze Beschreibung",
"sieve_type": "Filtertyp",
"skipcrossduplicates": "Duplikate auch über Ordner hinweg überspringen (\"first come, first serve\")",
"sogo_access": "Direktes Einloggen an SOGo erlauben",
"sogo_access_info": "Single-Sign-On Zugriff über die UI wird hierdurch NICHT deaktiviert. Diese Einstellung hat weder Einfluss auf den Zugang sonstiger Dienste noch entfernt sie ein vorhandenes SOGo-Benutzerprofil.",
"sogo_access": "Direktes weiterleiten an SOGo",
"sogo_access_info": "Nach dem Einloggen wird der Benutzer automatisch an SOGo weitergeleitet.",
"sogo_visible": "Alias in SOGo sichtbar",
"sogo_visible_info": "Diese Option hat lediglich Einfluss auf Objekte, die in SOGo darstellbar sind (geteilte oder nicht-geteilte Alias-Adressen mit dem Ziel mindestens einer lokalen Mailbox).",
"spam_alias": "Anpassen temporärer Alias-Adressen",
@ -1161,7 +1161,7 @@
"delete_ays": "Soll der Löschvorgang wirklich ausgeführt werden?",
"direct_aliases": "Direkte Alias-Adressen",
"direct_aliases_desc": "Nur direkte Alias-Adressen werden für benutzerdefinierte Einstellungen berücksichtigt.",
"direct_protocol_access": "Der Hauptbenutzer hat <b>direkten, externen Zugriff</b> auf folgende Protokolle und Anwendungen. Diese Einstellung wird vom Administrator gesteuert. App-Passwörter können verwendet werden, um individuelle Zugänge für Protokolle und Anwendungen zu erstellen.<br>Der Button \"In Webmail einloggen\" kann unabhängig der Einstellung immer verwendet werden.",
"direct_protocol_access": "Der Hauptbenutzer hat <b>direkten, externen Zugriff</b> auf folgende Protokolle und Anwendungen. Diese Einstellung wird vom Administrator gesteuert. App-Passwörter können verwendet werden, um individuelle Zugänge für Protokolle und Anwendungen zu erstellen.<br>Der Button \"Webmail\" kann unabhängig der Einstellung immer verwendet werden.",
"eas_reset": "ActiveSync-Geräte-Cache zurücksetzen",
"eas_reset_help": "In vielen Fällen kann ein ActiveSync-Profil durch das Zurücksetzen des Caches repariert werden.<br><b>Vorsicht:</b> Alle Elemente werden erneut heruntergeladen!",
"eas_reset_now": "Jetzt zurücksetzen",
@ -1201,7 +1201,7 @@
"no_active_filter": "Kein aktiver Filter vorhanden",
"no_last_login": "Keine letzte UI-Anmeldung gespeichert",
"no_record": "Kein Eintrag",
"open_webmail_sso": "In Webmail einloggen",
"open_webmail_sso": "Webmail",
"overview": "Übersicht",
"password": "Passwort",
"password_now": "Aktuelles Passwort (Änderungen bestätigen)",

View File

@ -212,17 +212,22 @@
"host": "Host",
"html": "HTML",
"iam": "Identity Provider",
"iam_attribute_field": "Attribute Field",
"iam_authorize_url": "Authorization endpoint",
"iam_auth_flow": "Authentication Flow",
"iam_auth_flow_info": "In addition to the Authorization Code Flow (Standard Flow in Keycloak), which is used for Single-Sign On login, mailcow also supports Authentication Flow with direct Credentials. The Mailpassword Flow attempts to validate the user's credentials by using the Keycloak Admin REST API. mailcow retrieves the hashed password from the <code>mailcow_password</code> attribute, which is mapped in Keycloak.",
"iam_basedn": "Base DN",
"iam_client_id": "Client ID",
"iam_client_secret": "Client Secret",
"iam_client_scopes": "Client Scopes",
"iam_description": "Configure an external OIDC Provider for Authentication<br>User's mailboxes will be automatically created upon their first login, provided that an attribute mapping has been set.",
"iam_description": "Configure an external Provider for Authentication<br>User's mailboxes will be automatically created upon their first login, provided that an attribute mapping has been set.",
"iam_extra_permission": "For the following settings to work, the mailcow client in Keycloak needs a <code>Service account</code> and the permission to <code>view-users</code>.",
"iam_host": "Host",
"iam_import_users": "Import Users",
"iam_mapping": "Attribute Mapping",
"iam_bindpass": "Bind Password",
"iam_periodic_full_sync": "Periodic Full Sync",
"iam_port": "Port",
"iam_realm": "Realm",
"iam_redirect_url": "Redirect Url",
"iam_rest_flow": "Mailpassword Flow",
@ -232,6 +237,8 @@
"iam_test_connection": "Test Connection",
"iam_token_url": "Token endpoint",
"iam_userinfo_url": "User info endpoint",
"iam_username_field": "Username Field",
"iam_binddn": "Bind DN",
"iam_version": "Version",
"import": "Import",
"import_private_key": "Import private key",
@ -702,8 +709,8 @@
"sieve_desc": "Short description",
"sieve_type": "Filter type",
"skipcrossduplicates": "Skip duplicate messages across folders (first come, first serve)",
"sogo_access": "Grant direct login access to SOGo",
"sogo_access_info": "Single-sign-on from within the mail UI remains working. This setting does neither affect access to all other services nor does it delete or change a user's existing SOGo profile.",
"sogo_access": "Direct forwarding to SOGo",
"sogo_access_info": "After logging in, the user is automatically redirected to SOGo.",
"sogo_visible": "Alias is visible in SOGo",
"sogo_visible_info": "This option only affects objects, that can be displayed in SOGo (shared or non-shared alias addresses pointing to at least one local mailbox). If hidden, an alias will not appear as selectable sender in SOGo.",
"spam_alias": "Create or change time limited alias addresses",
@ -1196,7 +1203,7 @@
"delete_ays": "Please confirm the deletion process.",
"direct_aliases": "Direct alias addresses",
"direct_aliases_desc": "Direct alias addresses are affected by spam filter and TLS policy settings.",
"direct_protocol_access": "This mailbox user has <b>direct, external access</b> to the following protocols and applications. This setting is controlled by your administrator. App passwords can be created to grant access to individual protocols and applications.<br>The \"Login to webmail\" button provides single-sign-on to SOGo and is always available.",
"direct_protocol_access": "This mailbox user has <b>direct, external access</b> to the following protocols and applications. This setting is controlled by your administrator. App passwords can be created to grant access to individual protocols and applications.<br>The \"Webmail\" button provides single-sign-on to SOGo and is always available.",
"eas_reset": "Reset ActiveSync device cache",
"eas_reset_help": "In many cases a device cache reset will help to recover a broken ActiveSync profile.<br><b>Attention:</b> All elements will be redownloaded!",
"eas_reset_now": "Reset now",
@ -1237,7 +1244,7 @@
"no_last_login": "No last UI login information",
"no_record": "No record",
"open_logs": "Open logs",
"open_webmail_sso": "Login to webmail",
"open_webmail_sso": "Webmail",
"overview": "Overview",
"password": "Password",
"password_now": "Current password (confirm changes)",

View File

@ -620,9 +620,9 @@
"username": "Nome utente",
"validate_save": "Convalida e salva",
"pushover": "Pushover",
"sogo_access_info": "Il single-sign-on dall'interno dell'interfaccia di posta rimane funzionante. Questa impostazione non influisce sull'accesso a tutti gli altri servizi né cancella o modifica il profilo SOGo esistente dell'utente.",
"none_inherit": "Nessuno / Eredita",
"sogo_access": "Concedere l'accesso diretto a SOGo",
"sogo_access": "Inoltro diretto a SOGo",
"sogo_access_info": "Dopo aver effettuato il login, l'utente viene automaticamente reindirizzato a SOGo.",
"acl": "ACL (autorizzazione)",
"app_passwd_protocols": "Protocolli consentiti per la password dell'app",
"last_modified": "Ultima modifica",
@ -1121,7 +1121,7 @@
"no_active_filter": "No active filter available",
"no_last_login": "No last UI login information",
"no_record": "Nessun elemento",
"open_webmail_sso": "Accedi alla webmail",
"open_webmail_sso": "Webmail",
"password": "Password",
"password_now": "Password attuale (conferma modifiche)",
"password_repeat": "Conferma password (riscrivi)",
@ -1209,7 +1209,7 @@
"syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Impossibile connettersi al server remoto",
"syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Nome utente o password errati",
"with_app_password": "con password dell'app",
"direct_protocol_access": "Questo utente della mailbox ha <b>accesso diretto ed esterno</b> ai seguenti protocolli e applicazioni. Questa impostazione è controllata dal tuo amministratore. Le password delle applicazioni possono essere create per garantire l'accesso ai singoli protocolli e applicazioni.<br>Il pulsante \"Accedi alla webmail\" fornisce un singolo accesso a SOGo ed è sempre disponibile.",
"direct_protocol_access": "Questo utente della mailbox ha <b>accesso diretto ed esterno</b> ai seguenti protocolli e applicazioni. Questa impostazione è controllata dal tuo amministratore. Le password delle applicazioni possono essere create per garantire l'accesso ai singoli protocolli e applicazioni.<br>Il pulsante \"Webmail\" fornisce un singolo accesso a SOGo ed è sempre disponibile.",
"pushover_sound": "Suono"
},
"warning": {

View File

@ -669,8 +669,8 @@
"sieve_desc": "Breve descrição",
"sieve_type": "Tipo de filtro",
"skipcrossduplicates": "Ignore mensagens duplicadas entre pastas (primeiro a chegar, primeiro a ser servido)",
"sogo_access": "Conceder acesso de login direto ao SoGo",
"sogo_access_info": "O login único de dentro da interface do usuário de e-mail continua funcionando. Essa configuração não afeta o acesso a todos os outros serviços nem exclui ou altera o perfil SoGo existente de um usuário.",
"sogo_access": "Encaminhamento direto para o SOGoo",
"sogo_access_info": "Depois de fazer login, o usuário é automaticamente redirecionado para o SOGo.",
"sogo_visible": "O alias é visível no SoGo",
"sogo_visible_info": "Essa opção afeta somente objetos, que podem ser exibidos no SoGo (endereços de alias compartilhados ou não compartilhados apontando para pelo menos uma caixa de correio local). Se estiver oculto, um alias não aparecerá como remetente selecionável no SoGo.",
"spam_alias": "Crie ou altere endereços de alias com limite de tempo",
@ -1160,7 +1160,7 @@
"delete_ays": "Confirme o processo de exclusão.",
"direct_aliases": "Endereços de alias diretos",
"direct_aliases_desc": "Os endereços de alias diretos são afetados pelo filtro de spam e pelas configurações da política TLS.",
"direct_protocol_access": "Esse usuário da caixa de correio tem <b>acesso externo direto</b> aos seguintes protocolos e aplicativos. Essa configuração é controlada pelo administrador. As senhas de aplicativos podem ser criadas para conceder acesso a protocolos e aplicativos individuais. <br>O botão “Login no webmail” fornece login único no SoGo e está sempre disponível.",
"direct_protocol_access": "Esse usuário da caixa de correio tem <b>acesso externo direto</b> aos seguintes protocolos e aplicativos. Essa configuração é controlada pelo administrador. As senhas de aplicativos podem ser criadas para conceder acesso a protocolos e aplicativos individuais. <br>O botão “Webmail” fornece login único no SoGo e está sempre disponível.",
"eas_reset": "Redefinir o cache do dispositivo ActiveSync",
"eas_reset_help": "Em muitos casos, uma redefinição do cache do dispositivo ajudará a recuperar um perfil quebrado do ActiveSync. <br><b>Atenção:</b> Todos os elementos serão baixados novamente!",
"eas_reset_now": "Reinicie agora",
@ -1201,7 +1201,7 @@
"no_last_login": "Nenhuma informação de login da última interface",
"no_record": "Sem registro",
"open_logs": "Registros abertos",
"open_webmail_sso": "Faça login no webmail",
"open_webmail_sso": "Webmail",
"password": "Senha",
"password_now": "Senha atual (confirme as alterações)",
"password_repeat": "Senha (repetição)",

View File

@ -607,8 +607,8 @@
"sieve_desc": "Descriere scurtă",
"sieve_type": "Tip filtru",
"skipcrossduplicates": "Sari peste mesajele duplicate din toate folderele (primul venit, primul servit)",
"sogo_access": "Acordați acces direct de conectare la SOGo",
"sogo_access_info": "Conectarea unică din interfața de administrare a e-mailului continuă să funcționeze. Această setare nu afectează accesul la toate celelalte servicii și nici nu șterge sau modifică profilul SOGo existent al unui utilizator.",
"sogo_access": "Redirecționare directă către SOGo",
"sogo_access_info": "După logare, utilizatorul este redirecționat automat către SOGo.",
"sogo_visible": "Aliasul este vizibil în SOGo",
"sogo_visible_info": "Această opțiune afectează doar obiecte, care pot fi afișate în SOGo (adrese alias partajate sau ne-partajate cu cel puțin o căsuță poștală locală). Dacă este ascuns, un alias nu va apărea ca expeditor selectabil în SOGo.",
"spam_alias": "Crează sau modifică adrese alias limitate în funcție de timp",
@ -1067,7 +1067,7 @@
"delete_ays": "Vă rugăm să confirmați stergerea.",
"direct_aliases": "Adrese alias directe",
"direct_aliases_desc": "Adresele alias directe sunt afectate de setările filtrului de spam și ale politicii TLS.",
"direct_protocol_access": "Acest utilizator are <b>acces direct, extern</b> la următoarele protocoale și aplicații. Această setare este controlată de administratorul dvs. Parolele pentru aplicații pot fi create pentru a acorda acces la protocoale și aplicații individuale.<br>Butonul \"Conectați-vă la webmail\" oferă conectare unică la SOGo și este întotdeauna disponibil.",
"direct_protocol_access": "Acest utilizator are <b>acces direct, extern</b> la următoarele protocoale și aplicații. Această setare este controlată de administratorul dvs. Parolele pentru aplicații pot fi create pentru a acorda acces la protocoale și aplicații individuale.<br>Butonul \"Webmail\" oferă conectare unică la SOGo și este întotdeauna disponibil.",
"eas_reset": "Resetează memoria cache a dispozitivului ActiveSync",
"eas_reset_help": "În multe cazuri, resetarea cache-ului dispozitivului va ajuta la recuperarea unui profil ActiveSync defect.<br><b>Atenţie:</b> Toate elementele vor fi descărcate din nou!",
"eas_reset_now": "Resetează acum",
@ -1108,7 +1108,7 @@
"no_last_login": "Nu există informații despre ultima autentificare în interfață",
"no_record": "Nici o înregistrare",
"open_logs": "Deschide jurnalele",
"open_webmail_sso": "Autentificare în webmail",
"open_webmail_sso": "Webmail",
"password": "Parolă",
"password_now": "Parola curentă (confirmă modificările)",
"password_repeat": "Parolă (repetă)",

View File

@ -621,7 +621,8 @@
"unchanged_if_empty": "Если без изменений - оставьте пустым",
"username": "Имя пользователя",
"validate_save": "Подтвердить и сохранить",
"sogo_access_info": "Единый вход из интерфейса почты продолжает работать. Эта настройка не влияет на доступ ко всем другим службам, а также не удаляет или изменяет существующий профиль пользователя SOGo.",
"sogo_access": "Прямая переадресация в SOGo",
"sogo_access_info": "После входа в систему пользователь автоматически перенаправляется в SOGo.",
"app_passwd_protocols": "Разрешенные протоколы для пароля приложения",
"domain_footer_info": "Нижние колонтитулы на уровне домена добавляются ко всем исходящим электронным письмам, связанным с адресом в этом домене. <br> Для нижнего колонтитула можно использовать следующие переменные:",
"domain_footer_info_vars": {
@ -1119,7 +1120,7 @@
"no_last_login": "Информация о последнем входе в пользовательский интерфейс отсутствует",
"no_record": "Записи отсутствуют",
"open_logs": "Показать журнал",
"open_webmail_sso": "Вход в веб-почту",
"open_webmail_sso": "веб-почту",
"password": "Пароль",
"password_now": "Текущий пароль (подтверждение изменения)",
"password_repeat": "Подтверждение пароля (повтор)",
@ -1204,7 +1205,7 @@
"years": "лет",
"allowed_protocols": "Разрешенные протоколы",
"apple_connection_profile_with_app_password": "Новый пароль приложения генерируется и добавляется в профиль, поэтому при настройке устройства не требуется вводить пароль. Не предоставляйте доступ к файлу, поскольку он предоставляет полный доступ к вашему почтовому ящику.",
"direct_protocol_access": "Этот пользователь почтового ящика имеет <b>прямой, внешний доступ</b> к следующим протоколам и приложениям. Эта настройка контролируется вашим администратором. Для предоставления доступа к отдельным протоколам и приложениям могут быть созданы пароли приложений.<br> Кнопка \"Вход в веб-почту\" обеспечивает единый вход в SOGo и всегда доступна.",
"direct_protocol_access": "Этот пользователь почтового ящика имеет <b>прямой, внешний доступ</b> к следующим протоколам и приложениям. Эта настройка контролируется вашим администратором. Для предоставления доступа к отдельным протоколам и приложениям могут быть созданы пароли приложений.<br> Кнопка \"веб-почту\" обеспечивает единый вход в SOGo и всегда доступна.",
"with_app_password": "с паролем приложения",
"change_password_hint_app_passwords": "В вашей учетной записи есть {{number_of_app_passwords}} паролей приложений, которые не будут изменены. Чтобы управлять ими, перейдите на вкладку \"Пароли приложений\".",
"attribute": "Атрибут",

View File

@ -625,8 +625,8 @@
"sieve_desc": "Krátky popis",
"sieve_type": "Typ filtru",
"skipcrossduplicates": "Preskočiť duplikované správy naprieč priečinkami (akceptuje sa prvý nález)",
"sogo_access": "Prideliť priame prihlásenie do SOGo",
"sogo_access_info": "Jednotné prihlásenie z používateľského mail rozhrania zostáva funkčné. Toto nastavenie nemá vplyv na prístup k ostatným službám, ani neodstraňuje alebo nemení existujúci profil používateľa SOGo.",
"sogo_access": "Priame presmerovanie na SOGo",
"sogo_access_info": "Po prihlásení je používateľ automaticky presmerovaný na službu SOGo.",
"sogo_visible": "Alias je viditeľný v SOGo",
"sogo_visible_info": "Táto voľba ovplyvňuje len objekty, ktoré dokážu byť zobrazené v SOGo (zdieľané alebo nezdieľané alias adresy ukazujúc na minimálne jednu lokálnu mailovú schránku). Ak je skrytý, alias nebude prezentovaný ako voliteľný odosielateľ v SOGo.",
"spam_alias": "Vytvoriť alebo zmeniť časovo limitované alias adresy",
@ -1119,7 +1119,7 @@
"delete_ays": "Potvrďte zmazanie.",
"direct_aliases": "Priame alias adresy",
"direct_aliases_desc": "Priame aliasy sú ovplyvnené spam filtrom a nastavením TLS pravidiel.",
"direct_protocol_access": "Tento používateľ mailovej schránky má <b>priamy, externý prístup</b> k nasledujúcim protokolom a aplikáciám. Toto nastavenie kontroluje administrátor. Na udelenie prístupu k jednotlivým protokolom a aplikáciám je možné vytvoriť heslá aplikácií.<br>Tlačidlo \"Prihlásenie do webmailu\" poskytuje jednotné prihlásenie do systému SOGo a je vždy k dispozícii.",
"direct_protocol_access": "Tento používateľ mailovej schránky má <b>priamy, externý prístup</b> k nasledujúcim protokolom a aplikáciám. Toto nastavenie kontroluje administrátor. Na udelenie prístupu k jednotlivým protokolom a aplikáciám je možné vytvoriť heslá aplikácií.<br>Tlačidlo \"Webmailu\" poskytuje jednotné prihlásenie do systému SOGo a je vždy k dispozícii.",
"eas_reset": "Resetovať medzipamäť u ActiveSync zariadení",
"eas_reset_help": "Vo väčšine prípadov, reset medzipamäte ActiveSync pomôže opravit nefunkčný profil.<br><b>Pozor:</b> Všetky potrebné dáta budú opäť stiahnuté!",
"eas_reset_now": "Reset ActiveSync",
@ -1160,7 +1160,7 @@
"no_last_login": "Žiadny záznam o prihlásení cez web",
"no_record": "Žiaden záznam",
"open_logs": "Otvoriť záznam",
"open_webmail_sso": "Prihláste sa do webmailu",
"open_webmail_sso": "Webmailu",
"password": "Heslo",
"password_now": "Aktuálne heslo (potvrdiť zmeny)",
"password_repeat": "Heslo (opakovať)",

View File

@ -607,8 +607,8 @@
"sieve_desc": "Короткий опис",
"sieve_type": "Тип фільтра",
"skipcrossduplicates": "Пропускати в папках повідомлення, що повторюються",
"sogo_access": "Надати прямий доступ до SOGo",
"sogo_access_info": "Єдиний вхід із інтерфейсу пошти продовжує працювати. Це налаштування не впливає на доступ до всіх інших служб, а також не видаляє чи змінює наявний профіль користувача SOGo.",
"sogo_access": "Пряме перенаправлення до SOGo",
"sogo_access_info": "Після входу користувач автоматично перенаправляється на SOGo.",
"sogo_visible": "Відображати псевдонім у SOGo",
"spam_alias": "Створити або змінити тимчасові (спам) псевдоніми",
"spam_filter": "Спам фільтр",
@ -1230,13 +1230,13 @@
"client_configuration": "Показати посібники з налаштування поштових клієнтів та смартфонів",
"direct_aliases": "Особисті псевдоніми",
"direct_aliases_desc": "На особисті псевдоніми розповсюджуються фільтри небажаної пошти та параметри політики TLS.",
"direct_protocol_access": "Цей користувач поштової скриньки має <b>прямий, зовнішній доступ</b> до наведених нижче протоколів і програм. Цими параметрами керує ваш адміністратор. Паролі додатків можна створювати для надання доступу до окремих протоколів і програм.<br>Кнопка \"Увійти до веб-пошти\" забезпечує єдиний вхід в SOGo і завжди доступна.",
"direct_protocol_access": "Цей користувач поштової скриньки має <b>прямий, зовнішній доступ</b> до наведених нижче протоколів і програм. Цими параметрами керує ваш адміністратор. Паролі додатків можна створювати для надання доступу до окремих протоколів і програм.<br>Кнопка \"веб-пошти\" забезпечує єдиний вхід в SOGo і завжди доступна.",
"force_pw_update": "Ви <b>повинні</b> встановити новий пароль для доступу до сервісів, що належать до цього облікового запису.",
"is_catch_all": "Catch-all для домена/ів",
"last_ui_login": "Останній вхід до особистого кабінету",
"months": "місяців",
"new_password_repeat": "Підтвердження пароля (повтор)",
"open_webmail_sso": "Вхід до веб-пошти",
"open_webmail_sso": "веб-пошти",
"pushover_evaluate_x_prio": "Встановити високий пріоритет повідомлень для листів із високим пріоритетом [<code>X-Priority: 1</code>]",
"pushover_info": "Налаштування Push-повідомлень будуть застосовуватися до всієї пошти <b>%s</b> (за винятком спаму), включаючи псевдоніми (особисті, загальні та теговані).",
"pushover_title": "Заголовок сповіщення",

View File

@ -594,8 +594,8 @@
"sieve_desc": "简短描述",
"sieve_type": "过滤器类型",
"skipcrossduplicates": "跳过其他文件夹中已存在的邮件(保留已经存在的邮件)",
"sogo_access": "允许直接登录 SOGo",
"sogo_access_info": "在邮箱的用户界面内的单点登录仍然有效。这一设置既不影响对所有其他服务的访问,也不删除或改变用户现有的 SOGo 的配置文件。",
"sogo_access": "直接转发给 SOGo",
"sogo_access_info": "登录后,用户会自动跳转到 SOGo。",
"sogo_visible": "SOGo 显示的别名",
"sogo_visible_info": "此设置只影响 SOGo 上可显示的对象 (指向本地邮箱的共享或非共享别名地址)。如果设置为隐藏,则别名地址不会作为可选发件人的下拉项显示。",
"spam_alias": "添加或更改临时别名地址",
@ -1053,7 +1053,7 @@
"delete_ays": "请确认删除。",
"direct_aliases": "直接别名",
"direct_aliases_desc": "垃圾邮件过滤和 TLS 策略会作用于直接别名。",
"direct_protocol_access": "该邮箱用户可以<b>直接</b>从<b>外部</b>访问以下的协议和应用程序。该选项由你的管理员进行设置。并可以创建应用密码,以授予对个别协议和应用的访问权限。<br>\"登录到 Webmail\" 按钮提供到 SOGo 的单点登录方式,并且始终可用。",
"direct_protocol_access": "该邮箱用户可以<b>直接</b>从<b>外部</b>访问以下的协议和应用程序。该选项由你的管理员进行设置。并可以创建应用密码,以授予对个别协议和应用的访问权限。<br>\"Webmail\" 按钮提供到 SOGo 的单点登录方式,并且始终可用。",
"eas_reset": "重置 ActiveSync 设备缓存",
"eas_reset_help": "在许多情况下,重置设备缓存可以帮助恢复错误的 ActiveSync 资料。<br><b>注意:</b> 所有元素将会被重新下载!",
"eas_reset_now": "立即重置",
@ -1094,7 +1094,7 @@
"no_last_login": "没有最后一次 UI 登录信息",
"no_record": "没有记录",
"open_logs": "打开日志",
"open_webmail_sso": "登录到 Webmail",
"open_webmail_sso": "Webmail",
"password": "密码",
"password_now": "当前密码 (确认更改)",
"password_repeat": "确认密码 (重复)",

View File

@ -628,8 +628,8 @@
"sieve_desc": "簡短描述",
"sieve_type": "過濾器類型",
"skipcrossduplicates": "跳過在其他資料夾中重複的郵件 (優先使用先找到的郵件)",
"sogo_access": "授權 SOGo 的直接存取權",
"sogo_access_info": "mail UI 內的 SSO 仍可以運作。此設定既不會影響到存取其他服務的權限,也不會刪除或更改使用者既有的個人檔案。",
"sogo_access": "直接轉寄至SOGo",
"sogo_access_info": "登入後,使用者會自動重新導向至SOGo。",
"sogo_visible": "別名會在 SOGo 中顯示",
"sogo_visible_info": "此選項只會影響可以顯示於 SOGo 上物件 (指向至少一個本地信箱的共享或非共享別名地址)。如果設為隱藏,別名地址將不會顯示於寄件人下拉選項中。",
"spam_alias": "新增或更改臨時別名地址",
@ -1105,7 +1105,7 @@
"delete_ays": "請確認刪除。",
"direct_aliases": "直接別名",
"direct_aliases_desc": "直接別名會受到垃圾郵件過濾器和 TLS 規則限制。",
"direct_protocol_access": "此信箱使用者有 <b>直接、外部存取</b> 至下列協定及應用程式。此設定是由管理者設定。應用程式密碼則可以用於授權單獨的協定及應用程式存取權限。<br>\"登入至網頁信箱\" 使用 SSO (single-sign-on) 來登入至 SOGO 且不受前面的限制。",
"direct_protocol_access": "此信箱使用者有 <b>直接、外部存取</b> 至下列協定及應用程式。此設定是由管理者設定。應用程式密碼則可以用於授權單獨的協定及應用程式存取權限。<br>\"网络邮件\" 使用 SSO (single-sign-on) 來登入至 SOGO 且不受前面的限制。",
"eas_reset": "重設 ActiveSync 裝置快取",
"eas_reset_help": "在許多情況下,重設裝置快取可以幫助恢復錯誤的 ActiveSync 資料。<br><b>注意:</b> 所有元素都會被重新下載!",
"eas_reset_now": "立即重設",
@ -1146,7 +1146,7 @@
"no_last_login": "沒有最後 UI 登入訊息",
"no_record": "沒有紀錄",
"open_logs": "開啟日誌",
"open_webmail_sso": "登入至網頁信箱",
"open_webmail_sso": "网络邮件",
"password": "密碼",
"password_now": "目前密碼 (確認更改)",
"password_repeat": "密碼 (再次輸入)",

View File

@ -52,6 +52,13 @@ elseif (isset($_GET['login'])) {
// register username and password in session
$_SESSION[$session_var_user_allowed][] = $login;
$_SESSION[$session_var_pass] = $sogo_sso_pass;
// set dual login
if ($_SESSION['acl']['login_as'] == "1" && $ALLOW_ADMIN_EMAIL_LOGIN !== 0 && $is_dual === false && $_SESSION['mailcow_cc_role'] != "user"){
$_SESSION["dual-login"]["username"] = $_SESSION['mailcow_cc_username'];
$_SESSION["dual-login"]["role"] = $_SESSION['mailcow_cc_role'];
$_SESSION['mailcow_cc_username'] = $login;
$_SESSION['mailcow_cc_role'] = "user";
}
// update sasl logs
$service = ($app_passwd_data['eas'] === true) ? 'EAS' : 'DAV';
$stmt = $pdo->prepare("REPLACE INTO sasl_log (`service`, `app_password`, `username`, `real_rip`) VALUES ('SSO', 0, :username, :remote_addr)");

View File

@ -18,6 +18,7 @@
name="iam_provider" id="iam_provider" class="full-width-select form-control" required>
<option value="keycloak" {% if not iam_settings.authsource or iam_settings.authsource == 'keycloak' %}selected{% endif %}>Keycloak</option>
<option value="generic-oidc" {% if iam_settings.authsource == 'generic-oidc' %}selected{% endif %}>Generic-OIDC</option>
<option value="ldap" {% if iam_settings.authsource == 'ldap' %}selected{% endif %}>LDAP</option>
</select>
</div>
</div>
@ -286,6 +287,143 @@
</div>
</form>
</div>
<div id="ldap_settings" class="{% if not iam_settings.authsource or iam_settings.authsource != 'ldap' %}d-none{% endif %}">
<form class="form-horizontal" autocapitalize="none" data-id="iam_ldap" autocorrect="off" role="form" method="post">
<input type="hidden" name="authsource" value="ldap">
<div class="row mb-2">
<label class="control-label col-md-3 text-sm-end" for="iam_ldap_host">{{ lang.admin.iam_host }}:</label>
<div class="col-12 col-md-9 col-lg-4">
<input type="text" class="form-control" id="iam_ldap_host" name="host" value="{{ iam_settings.host }}" required>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-md-3 text-sm-end" for="iam_ldap_port">{{ lang.admin.iam_port }}:</label>
<div class="col-12 col-md-9 col-lg-4">
<input type="number" class="form-control" id="iam_ldap_port" name="port" value="{{ iam_settings.port }}" required>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-md-3 text-sm-end" for="iam_ldap_basedn">{{ lang.admin.iam_basedn }}:</label>
<div class="col-12 col-md-9 col-lg-4">
<input type="text" class="form-control" id="iam_ldap_basedn" name="basedn" value="{{ iam_settings.basedn }}" required>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-md-3 text-sm-end" for="iam_ldap_username_field">{{ lang.admin.iam_username_field }}:</label>
<div class="col-12 col-md-9 col-lg-4">
<input type="text" class="form-control" placeholder="mail" id="iam_ldap_username_field" name="username_field" value="{{ iam_settings.username_field }}">
</div>
</div>
<div class="row mb-2">
<label class="control-label col-md-3 text-sm-end" for="iam_ldap_attribute_field">{{ lang.admin.iam_attribute_field }}:</label>
<div class="col-12 col-md-9 col-lg-4">
<input type="text" class="form-control" id="iam_ldap_attribute_field" name="attribute_field" value="{{ iam_settings.attribute_field }}" required>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-md-3 text-sm-end" for="iam_ldap_binddn">{{ lang.admin.iam_binddn }}:</label>
<div class="col-12 col-md-9 col-lg-4">
<input type="text" class="form-control" id="iam_ldap_binddn" name="binddn" value="{{ iam_settings.binddn }}" required>
</div>
</div>
<div class="row mb-4">
<label class="control-label col-md-3 text-sm-end" for="iam_ldap_bindpass">{{ lang.admin.iam_bindpass }}:</label>
<div class="col-12 col-md-9 col-lg-4">
<div class="reveal-password-input input-group">
<input type="password" class="password-field form-control" id="iam_ldap_bindpass" name="bindpass" value="{{ iam_settings.bindpass }}" required>
<button class="toggle-password btn btn-secondary" type="button"><i class="bi bi-eye"></i></button>
</div>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-md-3 text-sm-end">{{ lang.admin.iam_mapping }}:</label>
<div class="col-12 col-md-9 col-lg-4">
<div class="row px-2 align-items-center">
<span class="col-5 p-0 pe-2">Attribute</span>
<span class="col-5 p-0 pe-2">{{ lang.mailbox.template }}</span>
<div class="col-2 p-0 d-flex">
<button class="btn btn-sm d-block d-sm-inline btn-secondary ms-auto iam_rolemap_add_ldap"><i class="bi bi-plus-lg"></i></button>
</div>
</div>
</div>
</div>
<div class="row mb-2" id="iam_ldap_mapping_list">
{% for key, role in iam_settings.mappers %}
<div class="offset-md-3 col-12 col-md-9 col-lg-4 mb-2">
<div class="row px-2">
<div class="col-5 p-0 pe-2">
<input type="text" class="form-control me-2" name="mappers" value="{{ iam_settings.mappers[key] }}" required>
</div>
<div class="col-5 p-0 pe-2">
<select data-live-search="true" name="templates" class="form-control" title="{{ lang.mailbox.template }}" required>
{% for mbox_template in mbox_templates %}
<option{% if mbox_template.template == iam_settings.templates[key] %} selected{% endif %}>
{{ mbox_template.template }}
</option>
{% endfor %}
</select>
</div>
<div class="col-2 p-0 d-flex">
<button class="iam_ldap_rolemap_del btn btn-sm d-block d-sm-inline btn-secondary ms-auto"><i class="bi bi-x-lg"></i></button>
</div>
</div>
</div>
{% endfor %}
{% if not iam_settings.mappers %}
<div class="offset-md-3 col-12 col-md-9 col-lg-4 mb-2">
<div class="row px-2">
<div class="col-5 p-0 pe-2">
<input type="text" class="form-control me-2" name="mappers" value="" required>
</div>
<div class="col-5 p-0 pe-2">
<select data-live-search="true" name="templates" class="form-control" title="{{ lang.mailbox.template }}" required>
{% for mbox_template in mbox_templates %}
<option>
{{ mbox_template.template }}
</option>
{% endfor %}
</select>
</div>
<div class="col-2 p-0 d-flex">
<button class="iam_ldap_rolemap_del btn btn-sm d-block d-sm-inline btn-secondary ms-auto"><i class="bi bi-x-lg"></i></button>
</div>
</div>
</div>
{% endif %}
</div>
<div class="row mb-2">
<label class="control-label col-md-3 text-sm-end">{{ lang.admin.iam_periodic_full_sync }}</label>
<div class="col-12 col-md-9">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" name="periodic_sync" value="1" {% if iam_settings.periodic_sync == 1 %}checked{% endif %}>
</div>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-md-3 text-sm-end">{{ lang.admin.iam_import_users }}</label>
<div class="col-12 col-md-9">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" name="import_users" value="1" {% if iam_settings.import_users == 1 %}checked{% endif %}>
</div>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-md-3 text-sm-end">{{ lang.admin.iam_sync_interval }}</label>
<div class="col-12 col-md-9 col-lg-4">
<input class="form-control" type="number" min="1" name="sync_interval" style="width: 80px;" {% if iam_settings.sync_interval %}value="{{ iam_settings.sync_interval }}"{% else %}value="15"{% endif %}>
</div>
</div>
<div class="row mt-4 mb-2">
<div class="offset-md-3 col-12 col-md-9 d-flex flex-wrap">
<div class="btn-group mb-2">
<button class="btn btn-sm d-block d-sm-inline btn-secondary iam_test_connection iam_test_connection" data-id="iam_ldap"><i class="bi bi-play"></i> {{ lang.admin.iam_test_connection }}</button>
<button class="btn btn-sm d-block d-sm-inline btn-success" data-item="identity-provider" data-action="edit_selected" data-id="iam_ldap" data-api-url='edit/identity-provider' data-api-attr='{}'><i class="bi bi-check-lg"></i> {{ lang.admin.save }}</button>
</div>
<button class="btn btn-sm d-block d-sm-inline btn-danger ms-auto mb-2" data-item="identity-provider" data-action="delete_selected" data-id="iam_ldap" data-api-url='delete/identity-provider'><i class="bi bi-trash"></i> {{ lang.mailbox.remove }}</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>

View File

@ -38,6 +38,9 @@
{% if iam_settings.authsource == 'generic-oidc' %}
<option value="generic-oidc" {% if result.authsource == "generic-oidc" %}selected{% endif %}>Generic-OIDC</option>
{% endif %}
{% if iam_settings.authsource == 'ldap' %}
<option value="ldap" {% if result.authsource == "ldap" %}selected{% endif %}>LDAP</option>
{% endif %}
</select>
</div>
</div>
@ -207,6 +210,7 @@
</div>
</div>
</div>
{% if not result.authsource or result.authsource == 'mailcow' %}
<div class="row">
<label class="control-label col-sm-2" for="password">{{ lang.edit.password }} (<a href="#" class="generate_password">{{ lang.edit.generate }}</a>)</label>
<div class="col-sm-10">
@ -231,6 +235,7 @@
{% endif %}
</div>
</div>
{% endif %}
<div class="row">
<label class="control-label col-sm-2" for="protocol_access">{{ lang.edit.allowed_protocols }}</label>
<div class="col-sm-10">

View File

@ -19,8 +19,12 @@
<button disabled class="btn btn-secondary btn-block btn-xs-lg w-100">
{{ lang.user.open_webmail_sso }} <i class="bi bi-arrow-right"></i>
</button>
{% elseif dual_login %}
<a target="_blank" href="/sogo-auth.php?login={{ mailcow_cc_username }}" role="button" class="btn btn-primary btn-lg btn-block btn-xs-lg w-100">
{{ lang.user.open_webmail_sso }} <i class="bi bi-arrow-right"></i>
</a>
{% else %}
<a target="_blank" href="/sogo-auth.php?login={{ mailcow_cc_username }}" role="button" class="btn btn-primary btn-lg btn-block btn-xs-lg w-100">
<a target="_blank" href="/SOGo/so" role="button" class="btn btn-primary btn-lg btn-block btn-xs-lg w-100">
{{ lang.user.open_webmail_sso }} <i class="bi bi-arrow-right"></i>
</a>
{% endif %}
@ -51,7 +55,6 @@
{% if mailboxdata.attributes.smtp_access == 1 %}<div class="badge fs-6 bg-success m-2">SMTP <i class="bi bi-check-lg"></i></div>{% else %}<div class="badge fs-6 bg-danger m-2">SMTP <i class="bi bi-x-lg"></i></div>{% endif %}
{% if mailboxdata.attributes.sieve_access == 1 %}<div class="badge fs-6 bg-success m-2">Sieve <i class="bi bi-check-lg"></i></div>{% else %}<div class="badge fs-6 bg-danger m-2">Sieve <i class="bi bi-x-lg"></i></div>{% endif %}
{% if mailboxdata.attributes.pop3_access == 1 %}<div class="badge fs-6 bg-success m-2">POP3 <i class="bi bi-check-lg"></i></div>{% else %}<div class="badge fs-6 bg-danger m-2">POP3 <i class="bi bi-x-lg"></i></div>{% endif %}
{% if not skip_sogo %}{% if mailboxdata.attributes.sogo_access == 1 %}<div class="badge fs-6 bg-success m-2">SOGo <i class="bi bi-check-lg"></i></div>{% else %}<div class="badge fs-6 bg-danger m-2">SOGo <i class="bi bi-x-lg"></i></div>{% endif %}{% endif %}
</div>
</div>
</div>
@ -96,16 +99,19 @@
</div>
</div>
{# TFA #}
{% if mailboxdata.authsource == "mailcow" %}
<legend class="mt-4">{{ lang.user.authentication }}</legend>
<hr>
{# Password Change #}
{% if mailboxdata.authsource == "mailcow" %}
<div class="row">
<div class="col-12 col-md-3 d-flex"></div>
<div class="col-12 col-md-9 d-flex flex-wrap justify-content-center justify-content-sm-start">
<a class="btn btn-secondary" href="#pwChangeModal" data-bs-toggle="modal"><i class="bi bi-pencil-fill"></i> {{ lang.user.change_password }}</a>
</div>
</div>
{% endif %}
{# TFA #}
{% if mailboxdata.authsource == "mailcow" or mailboxdata.authsource == "ldap" %}
<div class="row mt-4">
<div class="col-12 col-md-3 d-flex">
<span class="mt-2 w-100 text-md-end">{{ lang.tfa.tfa }}:</span>
@ -129,7 +135,9 @@
</select>
</div>
</div>
{% endif %}
{# FIDO2 #}
{% if mailboxdata.authsource == "mailcow" %}
<div class="row mt-4">
<div class="col-sm-3 col-12 text-sm-end text-start">
<p><i class="bi bi-shield-fill-check"></i> {{ lang.fido2.fido2_auth }}</p>