diff --git a/data/web/admin/dashboard.php b/data/web/admin/dashboard.php index 443c2ea2a..2867770bc 100644 --- a/data/web/admin/dashboard.php +++ b/data/web/admin/dashboard.php @@ -2,18 +2,7 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php'; -if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') { - header('Location: /domainadmin/mailbox'); - exit(); -} -elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') { - header('Location: /user'); - exit(); -} -elseif (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "admin") { - header('Location: /admin'); - exit(); -} +protect_route(['admin']); require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php'; $_SESSION['return_to'] = $_SERVER['REQUEST_URI']; diff --git a/data/web/admin/index.php b/data/web/admin/index.php index 9ae4a0380..9e0bf1bd7 100644 --- a/data/web/admin/index.php +++ b/data/web/admin/index.php @@ -3,8 +3,11 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php'; if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') { - header('Location: /admin/dashboard'); - exit(); + // Only redirect to dashboard if NO pending actions + if (empty($_SESSION['pending_tfa_setup']) && empty($_SESSION['pending_pw_update'])) { + header('Location: /admin/dashboard'); + exit(); + } } elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') { header('Location: /domainadmin/mailbox'); diff --git a/data/web/admin/mailbox.php b/data/web/admin/mailbox.php index d0073bbd6..7b07d6fa9 100644 --- a/data/web/admin/mailbox.php +++ b/data/web/admin/mailbox.php @@ -2,18 +2,7 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php'; -if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') { - header('Location: /domainadmin/mailbox'); - exit(); -} -elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') { - header('Location: /user'); - exit(); -} -elseif (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "admin") { - header('Location: /admin'); - exit(); -} +protect_route(['admin']); require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php'; $_SESSION['return_to'] = $_SERVER['REQUEST_URI']; diff --git a/data/web/admin/queue.php b/data/web/admin/queue.php index 85ec59401..66682c7ae 100644 --- a/data/web/admin/queue.php +++ b/data/web/admin/queue.php @@ -2,19 +2,7 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php'; -if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') { - header('Location: /domainadmin/mailbox'); - exit(); -} -elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') { - header('Location: /user'); - exit(); -} -elseif (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "admin") { - header('Location: /admin'); - exit(); -} - +protect_route(['admin']); require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php'; $js_minifier->add('/web/js/site/queue.js'); diff --git a/data/web/admin/system.php b/data/web/admin/system.php index 9fd44e0d8..4db40c753 100644 --- a/data/web/admin/system.php +++ b/data/web/admin/system.php @@ -2,18 +2,7 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php'; -if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') { - header('Location: /domainadmin/mailbox'); - exit(); -} -elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') { - header('Location: /user'); - exit(); -} -elseif (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "admin") { - header('Location: /admin'); - exit(); -} +protect_route(['admin']); require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php'; $_SESSION['return_to'] = $_SERVER['REQUEST_URI']; diff --git a/data/web/domainadmin/index.php b/data/web/domainadmin/index.php index 0d70ec3ae..07c62a7d2 100644 --- a/data/web/domainadmin/index.php +++ b/data/web/domainadmin/index.php @@ -3,8 +3,11 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.domainadmin.inc.php'; if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') { - header('Location: /domainadmin/mailbox'); - exit(); + // Only redirect to mailbox if NO pending actions + if (empty($_SESSION['pending_tfa_setup']) && empty($_SESSION['pending_pw_update'])) { + header('Location: /domainadmin/mailbox'); + exit(); + } } elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') { header('Location: /admin/dashboard'); diff --git a/data/web/domainadmin/mailbox.php b/data/web/domainadmin/mailbox.php index bb2ef16f3..8beb5abb9 100644 --- a/data/web/domainadmin/mailbox.php +++ b/data/web/domainadmin/mailbox.php @@ -2,18 +2,7 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.domainadmin.inc.php'; -if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') { - header('Location: /admin/dashboard'); - exit(); -} -elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') { - header('Location: /user'); - exit(); -} -elseif (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "domainadmin") { - header('Location: /domainadmin'); - exit(); -} +protect_route(['domainadmin']); require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php'; $_SESSION['return_to'] = $_SERVER['REQUEST_URI']; diff --git a/data/web/domainadmin/user.php b/data/web/domainadmin/user.php index 7f1b392e0..a45e00dfc 100644 --- a/data/web/domainadmin/user.php +++ b/data/web/domainadmin/user.php @@ -2,41 +2,28 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.domainadmin.inc.php'; -if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') { +/* +/ DOMAIN ADMIN +*/ - /* - / DOMAIN ADMIN - */ +protect_route(['domainadmin']); - require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php'; - $_SESSION['return_to'] = $_SERVER['REQUEST_URI']; - $tfa_data = get_tfa(); - $fido2_data = fido2(array("action" => "get_friendly_names")); - $username = $_SESSION['mailcow_cc_username']; +require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php'; +$_SESSION['return_to'] = $_SERVER['REQUEST_URI']; +$tfa_data = get_tfa(); +$fido2_data = fido2(array("action" => "get_friendly_names")); +$username = $_SESSION['mailcow_cc_username']; - $template = 'domainadmin.twig'; - $template_data = [ - 'acl' => $_SESSION['acl'], - 'acl_json' => json_encode($_SESSION['acl']), - 'user_spam_score' => mailbox('get', 'spam_score', $username), - 'tfa_data' => $tfa_data, - 'fido2_data' => $fido2_data, - 'lang_user' => json_encode($lang['user']), - 'lang_datatables' => json_encode($lang['datatables']), - ]; -} -elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') { - header('Location: /admin/dashboard'); - exit(); -} -elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') { - header('Location: /user'); - exit(); -} -else { - header('Location: /domainadmin'); - exit(); -} +$template = 'domainadmin.twig'; +$template_data = [ + 'acl' => $_SESSION['acl'], + 'acl_json' => json_encode($_SESSION['acl']), + 'user_spam_score' => mailbox('get', 'spam_score', $username), + 'tfa_data' => $tfa_data, + 'fido2_data' => $fido2_data, + 'lang_user' => json_encode($lang['user']), + 'lang_datatables' => json_encode($lang['datatables']), +]; $js_minifier->add('/web/js/site/user.js'); $js_minifier->add('/web/js/site/pwgen.js'); diff --git a/data/web/edit.php b/data/web/edit.php index 57cf24bd2..48f2309c1 100644 --- a/data/web/edit.php +++ b/data/web/edit.php @@ -1,10 +1,8 @@ @$_SESSION['pending_tfa_methods'], 'pending_tfa_authmechs' => $pending_tfa_authmechs, 'pending_mailcow_cc_username' => @$_SESSION['pending_mailcow_cc_username'], + 'pending_tfa_setup' => !empty($_SESSION['pending_tfa_setup']), + 'pending_pw_update_modal' => !empty($_SESSION['pending_pw_update']), 'lang_footer' => json_encode($lang['footer']), 'lang_acl' => json_encode($lang['acl']), 'lang_tfa' => json_encode($lang['tfa']), diff --git a/data/web/inc/functions.admin.inc.php b/data/web/inc/functions.admin.inc.php index 9f42fd721..7bd0af42f 100644 --- a/data/web/inc/functions.admin.inc.php +++ b/data/web/inc/functions.admin.inc.php @@ -121,34 +121,56 @@ function admin($_action, $_data = null) { continue; } } - if (!empty($password)) { - if (password_check($password, $password2) !== true) { - return false; + // Check if this is a self password change via forced update + if ($username == $_SESSION['mailcow_cc_username'] && !empty($_SESSION['pending_pw_update'])) { + // Forced password update: only change password and clear force_pw_update flag + if (!empty($password)) { + if (password_check($password, $_data['password2']) !== true) { + return false; + } + $password_hashed = hash_password($password); + $stmt = $pdo->prepare("UPDATE `admin` SET `password` = :password_hashed, + `attributes` = JSON_SET(COALESCE(`attributes`, '{}'), '$.force_pw_update', '0') + WHERE `username` = :username"); + $stmt->execute(array( + ':password_hashed' => $password_hashed, + ':username' => $username + )); + unset($_SESSION['pending_pw_update']); } - $password_hashed = hash_password($password); - $stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active, `password` = :password_hashed WHERE `username` = :username"); - $stmt->execute(array( - ':password_hashed' => $password_hashed, - ':username_new' => $username_new, - ':username' => $username, - ':active' => $active - )); - if (isset($_data['disable_tfa'])) { - $stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username"); - $stmt->execute(array(':username' => $username)); + } else { + // Normal admin edit: update all attributes + $force_tfa = intval($_data['force_tfa'] ?? 0) ? 1 : 0; + $force_pw_update = intval($_data['force_pw_update'] ?? 0) ? 1 : 0; + if (!empty($password)) { + if (password_check($password, $password2) !== true) { + return false; + } + $password_hashed = hash_password($password); + $stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active, `password` = :password_hashed, + `attributes` = JSON_SET(COALESCE(`attributes`, '{}'), '$.force_tfa', :force_tfa, '$.force_pw_update', :force_pw_update) + WHERE `username` = :username"); + $stmt->execute(array( + ':password_hashed' => $password_hashed, + ':username_new' => $username_new, + ':username' => $username, + ':active' => $active, + ':force_tfa' => strval($force_tfa), + ':force_pw_update' => strval($force_pw_update) + )); } else { - $stmt = $pdo->prepare("UPDATE `tfa` SET `username` = :username_new WHERE `username` = :username"); - $stmt->execute(array(':username_new' => $username_new, ':username' => $username)); + $stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active, + `attributes` = JSON_SET(COALESCE(`attributes`, '{}'), '$.force_tfa', :force_tfa, '$.force_pw_update', :force_pw_update) + WHERE `username` = :username"); + $stmt->execute(array( + ':username_new' => $username_new, + ':username' => $username, + ':active' => $active, + ':force_tfa' => strval($force_tfa), + ':force_pw_update' => strval($force_pw_update) + )); } - } - else { - $stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active WHERE `username` = :username"); - $stmt->execute(array( - ':username_new' => $username_new, - ':username' => $username, - ':active' => $active - )); if (isset($_data['disable_tfa'])) { $stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username"); $stmt->execute(array(':username' => $username)); @@ -223,7 +245,8 @@ function admin($_action, $_data = null) { `tfa`.`active` AS `tfa_active`, `admin`.`username`, `admin`.`created`, - `admin`.`active` AS `active` + `admin`.`active` AS `active`, + `admin`.`attributes` AS `attributes` FROM `admin` LEFT OUTER JOIN `tfa` ON `tfa`.`username`=`admin`.`username` WHERE `admin`.`username`= :admin AND `superadmin` = '1'"); @@ -240,6 +263,7 @@ function admin($_action, $_data = null) { $admindata['active'] = $row['active']; $admindata['active_int'] = $row['active']; $admindata['created'] = $row['created']; + $admindata['attributes'] = json_decode($row['attributes'], true) ?? array('force_tfa' => '0', 'force_pw_update' => '0'); return $admindata; break; } diff --git a/data/web/inc/functions.auth.inc.php b/data/web/inc/functions.auth.inc.php index 3903ba642..91a8c55fa 100644 --- a/data/web/inc/functions.auth.inc.php +++ b/data/web/inc/functions.auth.inc.php @@ -82,7 +82,7 @@ function admin_login($user, $pass){ } $user = strtolower(trim($user)); - $stmt = $pdo->prepare("SELECT `password` FROM `admin` + $stmt = $pdo->prepare("SELECT `password`, `attributes` FROM `admin` WHERE `superadmin` = '1' AND `active` = '1' AND `username` = :user"); @@ -91,6 +91,13 @@ function admin_login($user, $pass){ // verify password if (verify_hash($row['password'], $pass)) { + $admin_attrs = json_decode($row['attributes'], true) ?? []; + + // Check force_pw_update + if (intval($admin_attrs['force_pw_update'] ?? 0) == 1) { + $_SESSION['pending_pw_update'] = true; + } + // check for tfa authenticators $authenticators = get_tfa($user); if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0) { @@ -110,6 +117,10 @@ function admin_login($user, $pass){ // 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)); + // Check force_tfa: only force setup if NO TFA exists at all + if (intval($admin_attrs['force_tfa'] ?? 0) == 1 && !tfa_exists($user)) { + $_SESSION['pending_tfa_setup'] = true; + } $_SESSION['return'][] = array( 'type' => 'success', 'log' => array(__FUNCTION__, $user, '*'), @@ -135,7 +146,7 @@ function domainadmin_login($user, $pass){ return false; } - $stmt = $pdo->prepare("SELECT `password` FROM `admin` + $stmt = $pdo->prepare("SELECT `password`, `attributes` FROM `admin` WHERE `superadmin` = '0' AND `active`='1' AND `username` = :user"); @@ -144,6 +155,13 @@ function domainadmin_login($user, $pass){ // verify password if (verify_hash($row['password'], $pass) !== false) { + $admin_attrs = json_decode($row['attributes'], true) ?? []; + + // Check force_pw_update + if (intval($admin_attrs['force_pw_update'] ?? 0) == 1) { + $_SESSION['pending_pw_update'] = true; + } + // check for tfa authenticators $authenticators = get_tfa($user); if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0) { @@ -163,6 +181,10 @@ function domainadmin_login($user, $pass){ // 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)); + // Check force_tfa: only force setup if NO TFA exists at all + if (intval($admin_attrs['force_tfa'] ?? 0) == 1 && !tfa_exists($user)) { + $_SESSION['pending_tfa_setup'] = true; + } $_SESSION['return'][] = array( 'type' => 'success', 'log' => array(__FUNCTION__, $user, '*'), @@ -286,6 +308,10 @@ function user_login($user, $pass, $extra = null){ // 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)); + // Check force_tfa: only force setup if NO TFA exists at all + if (intval($row['attributes']['force_tfa']) == 1 && !tfa_exists($user)) { + $_SESSION['pending_tfa_setup'] = true; + } $_SESSION['return'][] = array( 'type' => 'success', 'log' => array(__FUNCTION__, $user, '*', 'Provider: Keycloak'), @@ -338,6 +364,10 @@ function user_login($user, $pass, $extra = null){ // 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)); + // Check force_tfa: only force setup if NO TFA exists at all + if (intval($row['attributes']['force_tfa']) == 1 && !tfa_exists($user)) { + $_SESSION['pending_tfa_setup'] = true; + } $_SESSION['return'][] = array( 'type' => 'success', 'log' => array(__FUNCTION__, $user, '*', 'Provider: LDAP'), @@ -381,6 +411,10 @@ function user_login($user, $pass, $extra = null){ // 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)); + // Check force_tfa: only force setup if NO TFA exists at all + if (intval($row['attributes']['force_tfa']) == 1 && !tfa_exists($user)) { + $_SESSION['pending_tfa_setup'] = true; + } $_SESSION['return'][] = array( 'type' => 'success', 'log' => array(__FUNCTION__, $user, '*', 'Provider: mailcow'), diff --git a/data/web/inc/functions.domain_admin.inc.php b/data/web/inc/functions.domain_admin.inc.php index bb88ea34c..46b651b85 100644 --- a/data/web/inc/functions.domain_admin.inc.php +++ b/data/web/inc/functions.domain_admin.inc.php @@ -195,17 +195,23 @@ function domain_admin($_action, $_data = null) { )); } } + $force_tfa = intval($_data['force_tfa'] ?? 0) ? 1 : 0; + $force_pw_update = intval($_data['force_pw_update'] ?? 0) ? 1 : 0; if (!empty($password)) { if (password_check($password, $password2) !== true) { return false; } $password_hashed = hash_password($password); - $stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active, `password` = :password_hashed WHERE `username` = :username"); + $stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active, `password` = :password_hashed, + `attributes` = JSON_SET(COALESCE(`attributes`, '{}'), '$.force_tfa', :force_tfa, '$.force_pw_update', :force_pw_update) + WHERE `username` = :username"); $stmt->execute(array( ':password_hashed' => $password_hashed, ':username_new' => $username_new, ':username' => $username, - ':active' => $active + ':active' => $active, + ':force_tfa' => strval($force_tfa), + ':force_pw_update' => strval($force_pw_update) )); if (isset($_data['disable_tfa'])) { $stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username"); @@ -217,11 +223,15 @@ function domain_admin($_action, $_data = null) { } } else { - $stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active WHERE `username` = :username"); + $stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active, + `attributes` = JSON_SET(COALESCE(`attributes`, '{}'), '$.force_tfa', :force_tfa, '$.force_pw_update', :force_pw_update) + WHERE `username` = :username"); $stmt->execute(array( ':username_new' => $username_new, ':username' => $username, - ':active' => $active + ':active' => $active, + ':force_tfa' => strval($force_tfa), + ':force_pw_update' => strval($force_pw_update) )); if (isset($_data['disable_tfa'])) { $stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username"); @@ -244,31 +254,37 @@ function domain_admin($_action, $_data = null) { // Can only edit itself elseif ($_SESSION['mailcow_cc_role'] == "domainadmin") { $username = $_SESSION['mailcow_cc_username']; - $password_old = $_data['user_old_pass']; + $password_old = $_data['user_old_pass'] ?? ''; $password_new = $_data['user_new_pass']; $password_new2 = $_data['user_new_pass2']; - $stmt = $pdo->prepare("SELECT `password` FROM `admin` - WHERE `username` = :user"); - $stmt->execute(array(':user' => $username)); - $row = $stmt->fetch(PDO::FETCH_ASSOC); - if (!verify_hash($row['password'], $password_old)) { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_action, $_data_log), - 'msg' => 'access_denied' - ); - return false; + // Only verify old password if this is NOT a forced password update + if (empty($_SESSION['pending_pw_update'])) { + $stmt = $pdo->prepare("SELECT `password` FROM `admin` + WHERE `username` = :user"); + $stmt->execute(array(':user' => $username)); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + if (!verify_hash($row['password'], $password_old)) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_data_log), + 'msg' => 'access_denied' + ); + return false; + } } if (password_check($password_new, $password_new2) !== true) { return false; } $password_hashed = hash_password($password_new); - $stmt = $pdo->prepare("UPDATE `admin` SET `password` = :password_hashed WHERE `username` = :username"); + $stmt = $pdo->prepare("UPDATE `admin` SET `password` = :password_hashed, + `attributes` = JSON_SET(COALESCE(`attributes`, '{}'), '$.force_pw_update', '0') + WHERE `username` = :username"); $stmt->execute(array( ':password_hashed' => $password_hashed, ':username' => $username )); + unset($_SESSION['pending_pw_update']); $_SESSION['return'][] = array( 'type' => 'success', 'log' => array(__FUNCTION__, $_action, $_data_log), @@ -360,9 +376,11 @@ function domain_admin($_action, $_data = null) { `tfa`.`active` AS `tfa_active`, `domain_admins`.`username`, `domain_admins`.`created`, - `domain_admins`.`active` AS `active` + `domain_admins`.`active` AS `active`, + `admin`.`attributes` AS `attributes` FROM `domain_admins` LEFT OUTER JOIN `tfa` ON `tfa`.`username`=`domain_admins`.`username` + LEFT OUTER JOIN `admin` ON `admin`.`username`=`domain_admins`.`username` WHERE `domain_admins`.`username`= :domain_admin"); $stmt->execute(array( ':domain_admin' => $_data @@ -377,6 +395,7 @@ function domain_admin($_action, $_data = null) { $domainadmindata['active'] = $row['active']; $domainadmindata['active_int'] = $row['active']; $domainadmindata['created'] = $row['created']; + $domainadmindata['attributes'] = json_decode($row['attributes'], true) ?? array('force_tfa' => '0', 'force_pw_update' => '0'); // GET SELECTED $stmt = $pdo->prepare("SELECT `domain` FROM `domain` WHERE `domain` IN ( diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php index 23b8d701d..89f14b574 100644 --- a/data/web/inc/functions.inc.php +++ b/data/web/inc/functions.inc.php @@ -1033,20 +1033,24 @@ function edit_user_account($_data) { } // edit password - if (!empty($password_old) && !empty($_data['user_new_pass']) && !empty($_data['user_new_pass2'])) { - $stmt = $pdo->prepare("SELECT `password` FROM `mailbox` - WHERE `kind` NOT REGEXP 'location|thing|group' - AND `username` = :user AND authsource = 'mailcow'"); - $stmt->execute(array(':user' => $username)); - $row = $stmt->fetch(PDO::FETCH_ASSOC); + $is_forced_pw_update = !empty($_SESSION['pending_pw_update']); + if (((!empty($password_old) || $is_forced_pw_update) && !empty($_data['user_new_pass']) && !empty($_data['user_new_pass2']))) { + // Only verify old password if this is NOT a forced password update + if (!$is_forced_pw_update) { + $stmt = $pdo->prepare("SELECT `password` FROM `mailbox` + WHERE `kind` NOT REGEXP 'location|thing|group' + AND `username` = :user AND authsource = 'mailcow'"); + $stmt->execute(array(':user' => $username)); + $row = $stmt->fetch(PDO::FETCH_ASSOC); - if (!verify_hash($row['password'], $password_old)) { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_data_log), - 'msg' => 'access_denied' - ); - return false; + if (!verify_hash($row['password'], $password_old)) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_data_log), + 'msg' => 'access_denied' + ); + return false; + } } $password_new = $_data['user_new_pass']; @@ -1210,50 +1214,52 @@ function set_tfa($_data) { global $iam_settings; $_data_log = $_data; - $access_denied = null; !isset($_data_log['confirm_password']) ?: $_data_log['confirm_password'] = '*'; - $username = $_SESSION['mailcow_cc_username']; - // check for empty user and role - if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) $access_denied = true; - - // check admin confirm password - if ($access_denied === null) { - $stmt = $pdo->prepare("SELECT `password` FROM `admin` - WHERE `username` = :username"); - $stmt->execute(array(':username' => $username)); - $row = $stmt->fetch(PDO::FETCH_ASSOC); - if ($row) { - if (!verify_hash($row['password'], $_data["confirm_password"])) $access_denied = true; - else $access_denied = false; + // skip password check if this is a forced TFA enrollment after login + if (!empty($_SESSION['pending_tfa_setup'])) { + $username = $_SESSION['mailcow_cc_username']; + if (empty($username) || !isset($_SESSION['mailcow_cc_role'])) { + $_SESSION['return'][] = array('type' => 'danger', 'log' => array(__FUNCTION__, $_data_log), 'msg' => 'access_denied'); + return false; } - } + } else { + $username = $_SESSION['mailcow_cc_username']; + $access_denied = null; - // check mailbox confirm password - if ($access_denied === null) { - $stmt = $pdo->prepare("SELECT `password`, `authsource` FROM `mailbox` - WHERE `username` = :username"); - $stmt->execute(array(':username' => $username)); - $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; - } else { + if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) $access_denied = true; + + // check admin password + if ($access_denied === null) { + $stmt = $pdo->prepare("SELECT `password` FROM `admin` WHERE `username` = :username"); + $stmt->execute(array(':username' => $username)); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + if ($row) { if (!verify_hash($row['password'], $_data["confirm_password"])) $access_denied = true; else $access_denied = false; } } - } - // set access_denied error - if ($access_denied){ - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_data_log), - 'msg' => 'access_denied' - ); - return false; + // check mailbox password + if ($access_denied === null) { + $stmt = $pdo->prepare("SELECT `password`, `authsource` FROM `mailbox` WHERE `username` = :username"); + $stmt->execute(array(':username' => $username)); + $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; + } else { + if (!verify_hash($row['password'], $_data["confirm_password"])) $access_denied = true; + else $access_denied = false; + } + } + } + + if ($access_denied) { + $_SESSION['return'][] = array('type' => 'danger', 'log' => array(__FUNCTION__, $_data_log), 'msg' => 'access_denied'); + return false; + } } switch ($_data["tfa_method"]) { @@ -1306,6 +1312,7 @@ function set_tfa($_data) { ); return false; } + unset($_SESSION['pending_tfa_setup']); $_SESSION['return'][] = array( 'type' => 'success', 'log' => array(__FUNCTION__, $_data_log), @@ -1319,6 +1326,7 @@ function set_tfa($_data) { //$stmt->execute(array(':username' => $username)); $stmt = $pdo->prepare("INSERT INTO `tfa` (`username`, `key_id`, `authmech`, `secret`, `active`) VALUES (?, ?, 'totp', ?, '1')"); $stmt->execute(array($username, $key_id, $_POST['totp_secret'])); + unset($_SESSION['pending_tfa_setup']); $_SESSION['return'][] = array( 'type' => 'success', 'log' => array(__FUNCTION__, $_data_log), @@ -1347,6 +1355,7 @@ function set_tfa($_data) { 0 )); + unset($_SESSION['pending_tfa_setup']); $_SESSION['return'][] = array( 'type' => 'success', 'log' => array(__FUNCTION__, $_data_log), @@ -1354,6 +1363,25 @@ function set_tfa($_data) { ); break; case "none": + // Block TFA removal if force_tfa policy is active + $is_forced_tfa = false; + if ($_SESSION['mailcow_cc_role'] === 'user') { + $stmt_check = $pdo->prepare("SELECT JSON_EXTRACT(`attributes`, '$.force_tfa') FROM `mailbox` WHERE `username` = ?"); + $stmt_check->execute(array($username)); + $is_forced_tfa = ($stmt_check->fetchColumn() == '1'); + } else { + $stmt_check = $pdo->prepare("SELECT JSON_EXTRACT(`attributes`, '$.force_tfa') FROM `admin` WHERE `username` = ?"); + $stmt_check->execute(array($username)); + $is_forced_tfa = ($stmt_check->fetchColumn() == '1'); + } + if ($is_forced_tfa) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_data_log), + 'msg' => 'tfa_removal_blocked' + ); + return false; + } $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username"); $stmt->execute(array(':username' => $username)); $_SESSION['return'][] = array( @@ -1606,6 +1634,26 @@ function unset_tfa_key($_data) { return false; } + // Block key removal if force_tfa policy is active + $is_forced_tfa = false; + if ($_SESSION['mailcow_cc_role'] === 'user') { + $stmt_check = $pdo->prepare("SELECT JSON_EXTRACT(`attributes`, '$.force_tfa') FROM `mailbox` WHERE `username` = ?"); + $stmt_check->execute(array($username)); + $is_forced_tfa = ($stmt_check->fetchColumn() == '1'); + } else { + $stmt_check = $pdo->prepare("SELECT JSON_EXTRACT(`attributes`, '$.force_tfa') FROM `admin` WHERE `username` = ?"); + $stmt_check->execute(array($username)); + $is_forced_tfa = ($stmt_check->fetchColumn() == '1'); + } + if ($is_forced_tfa) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_data_log), + 'msg' => 'tfa_removal_blocked' + ); + return false; + } + // check if it's last key $stmt = $pdo->prepare("SELECT COUNT(*) AS `keys` FROM `tfa` WHERE `username` = :username AND `active` = '1'"); @@ -1638,6 +1686,15 @@ function unset_tfa_key($_data) { return false; } } +function tfa_exists($username) { + global $pdo; + if (empty($username)) { + return false; + } + $stmt = $pdo->prepare("SELECT COUNT(*) as count FROM `tfa` WHERE `username` = :username"); + $stmt->execute(array(':username' => $username)); + return $stmt->fetch(PDO::FETCH_ASSOC)['count'] > 0; +} function get_tfa($username = null, $id = null) { global $pdo; if (empty($username) && isset($_SESSION['mailcow_cc_username'])) { @@ -3440,6 +3497,49 @@ function set_user_loggedin_session($user) { unset($_SESSION['pending_mailcow_cc_role']); unset($_SESSION['pending_tfa_methods']); } +function protect_route($allowed_roles = ['admin', 'domainadmin', 'user'], $redirects = []) { + // Check if user is authenticated + if (!isset($_SESSION['mailcow_cc_role'])) { + if (isset($redirects['unauthenticated'])) { + header('Location: ' . $redirects['unauthenticated']); + } else { + header('Location: /'); + } + exit(); + } + + // Check for pending actions (2FA setup, password update) + if (!empty($_SESSION['pending_tfa_setup']) || !empty($_SESSION['pending_pw_update'])) { + $pending_redirect = '/'; + if ($_SESSION['mailcow_cc_role'] === 'admin') { + $pending_redirect = '/admin'; + } elseif ($_SESSION['mailcow_cc_role'] === 'domainadmin') { + $pending_redirect = '/domainadmin'; + } + header('Location: ' . $pending_redirect); + exit(); + } + + // Check if user's role is in the allowed roles for the route + if (!in_array($_SESSION['mailcow_cc_role'], $allowed_roles)) { + if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') { + header('Location: /admin/dashboard'); + exit(); + } + elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') { + header('Location: /domainadmin/mailbox'); + exit(); + } + elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') { + header('Location: /user'); + exit(); + } + else { + header('Location: /'); + exit(); + } + } +} function get_logs($application, $lines = false) { if ($lines === false) { $lines = $GLOBALS['LOG_LINES'] - 1; diff --git a/data/web/inc/functions.mailbox.inc.php b/data/web/inc/functions.mailbox.inc.php index 8d2efea3d..e8f3eb3d5 100644 --- a/data/web/inc/functions.mailbox.inc.php +++ b/data/web/inc/functions.mailbox.inc.php @@ -1083,6 +1083,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { } $active = (isset($_data['active'])) ? intval($_data['active']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['active']); $force_pw_update = (isset($_data['force_pw_update'])) ? intval($_data['force_pw_update']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['force_pw_update']); + $force_tfa = (isset($_data['force_tfa'])) ? intval($_data['force_tfa']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['force_tfa']); $tls_enforce_in = (isset($_data['tls_enforce_in'])) ? intval($_data['tls_enforce_in']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_in']); $tls_enforce_out = (isset($_data['tls_enforce_out'])) ? intval($_data['tls_enforce_out']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_out']); $sogo_access = (isset($_data['sogo_access'])) ? intval($_data['sogo_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['sogo_access']); @@ -1099,10 +1100,12 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $attribute_hash = (!empty($_data['attribute_hash'])) ? $_data['attribute_hash'] : ''; if (in_array($authsource, array('keycloak', 'generic-oidc', 'ldap'))){ $force_pw_update = 0; + $force_tfa = 0; } $mailbox_attrs = json_encode( array( 'force_pw_update' => strval($force_pw_update), + 'force_tfa' => strval($force_tfa), 'tls_enforce_in' => strval($tls_enforce_in), 'tls_enforce_out' => strval($tls_enforce_out), 'sogo_access' => strval($sogo_access), @@ -1720,6 +1723,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $attr["rl_frame"] = (!empty($_data['rl_frame'])) ? $_data['rl_frame'] : "s"; $attr["rl_value"] = (!empty($_data['rl_value'])) ? $_data['rl_value'] : ""; $attr["force_pw_update"] = isset($_data['force_pw_update']) ? intval($_data['force_pw_update']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['force_pw_update']); + $attr["force_tfa"] = isset($_data['force_tfa']) ? intval($_data['force_tfa']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['force_tfa']); $attr["sogo_access"] = isset($_data['sogo_access']) ? intval($_data['sogo_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['sogo_access']); $attr["active"] = isset($_data['active']) ? intval($_data['active']) : 1; $attr["tls_enforce_in"] = isset($_data['tls_enforce_in']) ? intval($_data['tls_enforce_in']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_in']); @@ -3065,6 +3069,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { if (!empty($is_now)) { $active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active']; (int)$force_pw_update = (isset($_data['force_pw_update'])) ? intval($_data['force_pw_update']) : intval($is_now['attributes']['force_pw_update']); + (int)$force_tfa = (isset($_data['force_tfa'])) ? intval($_data['force_tfa']) : intval($is_now['attributes']['force_tfa']); (int)$sogo_access = (isset($_data['sogo_access']) && hasACLAccess("sogo_access")) ? intval($_data['sogo_access']) : intval($is_now['attributes']['sogo_access']); (int)$imap_access = (isset($_data['imap_access']) && hasACLAccess("protocol_access")) ? intval($_data['imap_access']) : intval($is_now['attributes']['imap_access']); (int)$pop3_access = (isset($_data['pop3_access']) && hasACLAccess("protocol_access")) ? intval($_data['pop3_access']) : intval($is_now['attributes']['pop3_access']); @@ -3088,6 +3093,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { } if (in_array($authsource, array('keycloak', 'generic-oidc', 'ldap'))){ $force_pw_update = 0; + $force_tfa = 0; } $pw_recovery_email = (isset($_data['pw_recovery_email']) && $authsource == 'mailcow') ? $_data['pw_recovery_email'] : $is_now['attributes']['recovery_email']; } @@ -3359,6 +3365,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { `quota` = :quota_b, `authsource` = :authsource, `attributes` = JSON_SET(`attributes`, '$.force_pw_update', :force_pw_update), + `attributes` = JSON_SET(`attributes`, '$.force_tfa', :force_tfa), `attributes` = JSON_SET(`attributes`, '$.sogo_access', :sogo_access), `attributes` = JSON_SET(`attributes`, '$.imap_access', :imap_access), `attributes` = JSON_SET(`attributes`, '$.sieve_access', :sieve_access), @@ -3376,6 +3383,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ':quota_b' => $quota_b, ':attribute_hash' => $attribute_hash, ':force_pw_update' => $force_pw_update, + ':force_tfa' => $force_tfa, ':sogo_access' => $sogo_access, ':imap_access' => $imap_access, ':pop3_access' => $pop3_access, diff --git a/data/web/inc/init_db.inc.php b/data/web/inc/init_db.inc.php index c64419800..72018a6bc 100644 --- a/data/web/inc/init_db.inc.php +++ b/data/web/inc/init_db.inc.php @@ -4,7 +4,7 @@ function init_db_schema() try { global $pdo; - $db_version = "28012026_1000"; + $db_version = "19022026_1220"; $stmt = $pdo->query("SHOW TABLES LIKE 'versions'"); $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); @@ -76,7 +76,8 @@ function init_db_schema() "superadmin" => "TINYINT(1) NOT NULL DEFAULT '0'", "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", "modified" => "DATETIME ON UPDATE NOW(0)", - "active" => "TINYINT(1) NOT NULL DEFAULT '1'" + "active" => "TINYINT(1) NOT NULL DEFAULT '1'", + "attributes" => "JSON" ), "keys" => array( "primary" => array( @@ -1390,6 +1391,11 @@ function init_db_schema() $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.passwd_update', \"0\") WHERE JSON_VALUE(`attributes`, '$.passwd_update') IS NULL;"); $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.relayhost', \"0\") WHERE JSON_VALUE(`attributes`, '$.relayhost') IS NULL;"); $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.force_pw_update', \"0\") WHERE JSON_VALUE(`attributes`, '$.force_pw_update') IS NULL;"); + $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.force_tfa', \"0\") WHERE JSON_VALUE(`attributes`, '$.force_tfa') IS NULL;"); + // admin attributes + $pdo->query("UPDATE `admin` SET `attributes` = '{}' WHERE `attributes` = '' OR `attributes` IS NULL;"); + $pdo->query("UPDATE `admin` SET `attributes` = JSON_SET(`attributes`, '$.force_tfa', \"0\") WHERE JSON_VALUE(`attributes`, '$.force_tfa') IS NULL;"); + $pdo->query("UPDATE `admin` SET `attributes` = JSON_SET(`attributes`, '$.force_pw_update', \"0\") WHERE JSON_VALUE(`attributes`, '$.force_pw_update') IS NULL;"); $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.sieve_access', \"1\") WHERE JSON_VALUE(`attributes`, '$.sieve_access') IS NULL;"); $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.sogo_access', \"1\") WHERE JSON_VALUE(`attributes`, '$.sogo_access') IS NULL;"); $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.imap_access', \"1\") WHERE JSON_VALUE(`attributes`, '$.imap_access') IS NULL;"); @@ -1449,6 +1455,7 @@ function init_db_schema() "rl_frame" => "s", "rl_value" => "", "force_pw_update" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['force_pw_update']), + "force_tfa" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['force_tfa']), "sogo_access" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['sogo_access']), "active" => 1, "tls_enforce_in" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['tls_enforce_in']), diff --git a/data/web/inc/triggers.admin.inc.php b/data/web/inc/triggers.admin.inc.php index 2a02ba511..92043190e 100644 --- a/data/web/inc/triggers.admin.inc.php +++ b/data/web/inc/triggers.admin.inc.php @@ -9,6 +9,11 @@ if (isset($_POST["verify_tfa_login"])) { unset($_SESSION['pending_mailcow_cc_role']); unset($_SESSION['pending_tfa_methods']); + // If pending actions exist, redirect to /admin to show modal + if (!empty($_SESSION['pending_tfa_setup']) || !empty($_SESSION['pending_pw_update'])) { + header("Location: /admin"); + die(); + } header("Location: /admin/dashboard"); die(); } @@ -42,6 +47,15 @@ if (isset($_GET["cancel_tfa_login"])) { header("Location: /admin"); } +if (isset($_GET["cancel_tfa_setup"])) { + session_regenerate_id(true); + session_unset(); + session_destroy(); + session_write_close(); + header("Location: /admin"); + exit(); +} + if (isset($_POST["login_user"]) && isset($_POST["pass_user"])) { $login_user = strtolower(trim($_POST["login_user"])); $as = check_login($login_user, $_POST["pass_user"], array("role" => "admin", "service" => "MAILCOWUI")); @@ -50,6 +64,11 @@ if (isset($_POST["login_user"]) && isset($_POST["pass_user"])) { session_regenerate_id(true); $_SESSION['mailcow_cc_username'] = $login_user; $_SESSION['mailcow_cc_role'] = "admin"; + // If pending actions exist, redirect to /admin to show modal + if (!empty($_SESSION['pending_tfa_setup']) || !empty($_SESSION['pending_pw_update'])) { + header("Location: /admin"); + die(); + } header("Location: /admin/dashboard"); die(); } diff --git a/data/web/inc/triggers.domainadmin.inc.php b/data/web/inc/triggers.domainadmin.inc.php index 764d9009b..2eee8b993 100644 --- a/data/web/inc/triggers.domainadmin.inc.php +++ b/data/web/inc/triggers.domainadmin.inc.php @@ -20,6 +20,11 @@ if (isset($_POST["verify_tfa_login"])) { unset($_SESSION['pending_mailcow_cc_role']); unset($_SESSION['pending_tfa_methods']); + // If pending actions exist, redirect to /domainadmin to show modal + if (!empty($_SESSION['pending_tfa_setup']) || !empty($_SESSION['pending_pw_update'])) { + header("Location: /domainadmin"); + die(); + } header("Location: /domainadmin/mailbox"); die(); } @@ -53,6 +58,15 @@ if (isset($_GET["cancel_tfa_login"])) { header("Location: /domainadmin"); } +if (isset($_GET["cancel_tfa_setup"])) { + session_regenerate_id(true); + session_unset(); + session_destroy(); + session_write_close(); + header("Location: /domainadmin"); + exit(); +} + if (isset($_POST["login_user"]) && isset($_POST["pass_user"])) { $login_user = strtolower(trim($_POST["login_user"])); $as = check_login($login_user, $_POST["pass_user"], array("role" => "domain_admin", "service" => "MAILCOWUI")); @@ -61,6 +75,11 @@ if (isset($_POST["login_user"]) && isset($_POST["pass_user"])) { session_regenerate_id(true); $_SESSION['mailcow_cc_username'] = $login_user; $_SESSION['mailcow_cc_role'] = "domainadmin"; + // If pending actions exist, redirect to /domainadmin to show modal + if (!empty($_SESSION['pending_tfa_setup']) || !empty($_SESSION['pending_pw_update'])) { + header("Location: /domainadmin"); + die(); + } header("Location: /domainadmin/mailbox"); die(); } diff --git a/data/web/inc/triggers.global.inc.php b/data/web/inc/triggers.global.inc.php index dd88fad56..1f8f92004 100644 --- a/data/web/inc/triggers.global.inc.php +++ b/data/web/inc/triggers.global.inc.php @@ -36,7 +36,26 @@ if (isset($_SESSION['mailcow_cc_role']) && (isset($_SESSION['acl']['login_as']) if (isset($_SESSION['mailcow_cc_role'])) { if (isset($_POST["set_tfa"])) { + $had_pending_tfa_setup = !empty($_SESSION['pending_tfa_setup']); set_tfa($_POST); + // After TFA setup during forced enrollment + if ($had_pending_tfa_setup && empty($_SESSION['pending_tfa_setup'])) { + if ($_SESSION['mailcow_cc_role'] === 'admin') { + header("Location: /admin/dashboard"); + } elseif ($_SESSION['mailcow_cc_role'] === 'domainadmin') { + header("Location: /domainadmin/mailbox"); + } elseif ($_SESSION['mailcow_cc_role'] === 'user') { + // Check if user should go to SOGo or /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 && getenv('SKIP_SOGO') != "y") { + header("Location: /SOGo/so/"); + } else { + header("Location: /user"); + } + } + exit(); + } } if (isset($_POST["unset_tfa_key"])) { unset_tfa_key($_POST); diff --git a/data/web/inc/triggers.user.inc.php b/data/web/inc/triggers.user.inc.php index cc33596f9..5dcde0b18 100644 --- a/data/web/inc/triggers.user.inc.php +++ b/data/web/inc/triggers.user.inc.php @@ -76,6 +76,11 @@ if (isset($_POST["verify_tfa_login"])) { $user_details = mailbox("get", "mailbox_details", $_SESSION['mailcow_cc_username']); $is_dual = (!empty($_SESSION["dual-login"]["username"])) ? true : false; + // If pending actions exist, redirect to / to show modal + if (!empty($_SESSION['pending_tfa_setup']) || !empty($_SESSION['pending_pw_update'])) { + header("Location: /"); + die(); + } if (intval($user_details['attributes']['sogo_access']) == 1 && intval($user_details['attributes']['force_pw_update']) != 1 && getenv('SKIP_SOGO') != "y" && @@ -117,6 +122,15 @@ if (isset($_GET["cancel_tfa_login"])) { header("Location: /"); } +if (isset($_GET["cancel_tfa_setup"])) { + session_regenerate_id(true); + session_unset(); + session_destroy(); + session_write_close(); + header("Location: /"); + exit(); +} + if (isset($_POST["login_user"]) && isset($_POST["pass_user"])) { $login_user = strtolower(trim($_POST["login_user"])); $as = check_login($login_user, $_POST["pass_user"], array("role" => "user", "service" => "MAILCOWUI")); @@ -142,6 +156,11 @@ if (isset($_POST["login_user"]) && isset($_POST["pass_user"])) { $user_details = mailbox("get", "mailbox_details", $login_user); $is_dual = (!empty($_SESSION["dual-login"]["username"])) ? true : false; + // If pending actions exist, redirect to / to show modal + if (!empty($_SESSION['pending_tfa_setup']) || !empty($_SESSION['pending_pw_update'])) { + header("Location: /"); + die(); + } if (intval($user_details['attributes']['sogo_access']) == 1 && intval($user_details['attributes']['force_pw_update']) != 1 && getenv('SKIP_SOGO') != "y" && diff --git a/data/web/inc/vars.inc.php b/data/web/inc/vars.inc.php index 6d1965542..2f4a4b076 100644 --- a/data/web/inc/vars.inc.php +++ b/data/web/inc/vars.inc.php @@ -193,6 +193,9 @@ $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; +// Force 2FA enrollment at next login +$MAILBOX_DEFAULT_ATTRIBUTES['force_tfa'] = false; + // 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; diff --git a/data/web/index.php b/data/web/index.php index a1ff9310f..b037c14f1 100644 --- a/data/web/index.php +++ b/data/web/index.php @@ -9,22 +9,28 @@ if (isset($_SESSION['mailcow_cc_role']) && isset($_SESSION['oauth2_request'])) { exit(); } 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 && getenv('SKIP_SOGO') != "y") { - header("Location: /SOGo/so/"); - } else { - header("Location: /user"); + if (empty($_SESSION['pending_tfa_setup']) && empty($_SESSION['pending_pw_update'])) { + $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 && getenv('SKIP_SOGO') != "y") { + header("Location: /SOGo/so/"); + } else { + header("Location: /user"); + } + exit(); } - exit(); } elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') { - header('Location: /admin/dashboard'); - exit(); + if (empty($_SESSION['pending_tfa_setup']) && empty($_SESSION['pending_pw_update'])) { + header('Location: /admin/dashboard'); + exit(); + } } elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') { - header('Location: /domainadmin/mailbox'); - exit(); + if (empty($_SESSION['pending_tfa_setup']) && empty($_SESSION['pending_pw_update'])) { + header('Location: /domainadmin/mailbox'); + exit(); + } } $host = strtolower($_SERVER['HTTP_HOST'] ?? ''); diff --git a/data/web/json_api.php b/data/web/json_api.php index 5409e65d3..ebfec4353 100644 --- a/data/web/json_api.php +++ b/data/web/json_api.php @@ -169,6 +169,8 @@ if (isset($_GET['query'])) { exit; } fido2(array("action" => "register", "registration" => $data)); + // Release pending_tfa_setup session hold + unset($_SESSION['pending_tfa_setup']); $return = new stdClass(); $return->success = true; echo json_encode($return); diff --git a/data/web/lang/lang.de-de.json b/data/web/lang/lang.de-de.json index b86879ded..06137344d 100644 --- a/data/web/lang/lang.de-de.json +++ b/data/web/lang/lang.de-de.json @@ -536,6 +536,7 @@ "temp_error": "Temporärer Fehler", "text_empty": "Text darf nicht leer sein", "tfa_token_invalid": "TFA-Token ungültig", + "tfa_removal_blocked": "Zwei-Faktor-Authentifizierung kann nicht entfernt werden, da sie für Ihr Konto erforderlich ist.", "tls_policy_map_dest_invalid": "Ziel ist ungültig", "tls_policy_map_entry_exists": "Eine TLS-Richtlinie \"%s\" existiert bereits", "tls_policy_map_parameter_invalid": "Parameter ist ungültig", @@ -685,6 +686,8 @@ "extended_sender_acl_info": "Der DKIM-Domainkey der externen Absenderdomain sollte in diesen Server importiert werden, falls vorhanden.
\r\n Wird SPF verwendet, muss diesem Server der Versand gestattet werden.
\r\n Wird eine Domain oder Alias-Domain zu diesem Server hinzugefügt, die sich mit der externen Absenderadresse überschneidet, wird der externe Absender hier entfernt.
\r\n Ein Eintrag @domain.tld erlaubt den Versand als *@domain.tld", "force_pw_update": "Erzwinge Passwortänderung bei nächstem Login", "force_pw_update_info": "Dem Benutzer wird lediglich der Zugang zur %s ermöglicht, App Passwörter funktionieren weiterhin.", + "force_tfa": "Zwei-Faktor-Authentifizierung beim Login erzwingen", + "force_tfa_info": "Der Benutzer muss Zwei-Faktor-Authentifizierung einrichten, bevor er auf den Bereich zugreifen kann.", "footer_exclude": "von Fußzeile ausschließen", "full_name": "Voller Name", "gal": "Globales Adressbuch", @@ -1233,7 +1236,13 @@ "webauthn": "WebAuthn-Authentifizierung", "waiting_usb_auth": "Warte auf USB-Gerät...

Bitte jetzt den vorgesehenen Taster des USB-Gerätes berühren.", "waiting_usb_register": "Warte auf USB-Gerät...

Bitte zuerst das obere Passwortfeld ausfüllen und erst dann den vorgesehenen Taster des USB-Gerätes berühren.", - "yubi_otp": "Yubico OTP-Authentifizierung" + "yubi_otp": "Yubico OTP-Authentifizierung", + "force_tfa": "Zwei-Faktor-Authentifizierung beim Login erzwingen", + "force_tfa_info": "Der Benutzer muss Zwei-Faktor-Authentifizierung einrichten, bevor er auf den Bereich zugreifen kann.", + "setup_title": "Zwei-Faktor-Authentifizierung erforderlich", + "setup_required": "Ihr Konto erfordert Zwei-Faktor-Authentifizierung. Bitte richten Sie eine 2FA-Methode ein, um fortzufahren.", + "cancel_setup": "Abbrechen und abmelden", + "disable_tfa_blocked": "Die Zwei-Faktor-Authentifizierung kann nicht deaktiviert werden, da sie für dieses Konto erforderlich ist." }, "user": { "action": "Aktion", @@ -1312,6 +1321,7 @@ "never": "Niemals", "new_password": "Neues Passwort", "new_password_repeat": "Neues Passwort (Wiederholung)", + "pw_update_required": "Ihr Konto erfordert eine Passwortänderung. Bitte setzen Sie ein neues Passwort, um fortzufahren.", "no_active_filter": "Kein aktiver Filter vorhanden", "no_last_login": "Keine letzte UI-Anmeldung gespeichert", "no_record": "Kein Eintrag", diff --git a/data/web/lang/lang.en-gb.json b/data/web/lang/lang.en-gb.json index 79175e578..eb26e3fcf 100644 --- a/data/web/lang/lang.en-gb.json +++ b/data/web/lang/lang.en-gb.json @@ -416,6 +416,7 @@ }, "danger": { "access_denied": "Access denied or invalid form data", + "tfa_removal_blocked": "Two-factor authentication cannot be removed because it is required for your account.", "alias_domain_invalid": "Alias domain %s is invalid", "alias_empty": "Alias address must not be empty", "alias_goto_identical": "Alias and goto address must not be identical", @@ -1221,6 +1222,11 @@ "confirm_totp_token": "Please confirm your changes by entering the generated token", "delete_tfa": "Disable TFA", "disable_tfa": "Disable TFA until next successful login", + "force_tfa": "Force 2FA enrollment at login", + "force_tfa_info": "The user will be required to set up two-factor authentication before accessing the panel.", + "setup_title": "Two-Factor Authentication Required", + "setup_required": "Your account requires two-factor authentication. Please set up a 2FA method to continue.", + "cancel_setup": "Cancel and log out", "enter_qr_code": "Your TOTP code if your device cannot scan QR codes", "error_code": "Error code", "init_webauthn": "Initializing, please wait...", @@ -1327,6 +1333,7 @@ "overview": "Overview", "password": "Password", "password_now": "Current password (confirm changes)", + "pw_update_required": "Your account requires a password change. Please set a new password to continue.", "password_repeat": "Password (repeat)", "password_reset_info": "If no email for password recovery is provided, this function cannot be used.", "protocols": "Protocols", diff --git a/data/web/sogo-auth.php b/data/web/sogo-auth.php index 2da28d4d4..07456a7d1 100644 --- a/data/web/sogo-auth.php +++ b/data/web/sogo-auth.php @@ -50,6 +50,11 @@ elseif (isset($_GET['login'])) { (($_SESSION['acl']['login_as'] == "1" && $ALLOW_ADMIN_EMAIL_LOGIN !== 0) || ($is_dual === false && $login == $_SESSION['mailcow_cc_username']))) { if (filter_var($login, FILTER_VALIDATE_EMAIL)) { if (user_get_alias_details($login) !== false) { + // Block SOGo access if pending actions (2FA setup, password update) + if (!empty($_SESSION['pending_tfa_setup']) || !empty($_SESSION['pending_pw_update'])) { + header("Location: /"); + exit; + } // register username in session $_SESSION[$session_var_user_allowed][] = $login; // set dual login @@ -94,7 +99,8 @@ elseif (isset($_SERVER['HTTP_X_ORIGINAL_URI']) && strcasecmp(substr($_SERVER['HT filter_var($email, FILTER_VALIDATE_EMAIL) && is_array($_SESSION[$session_var_user_allowed]) && in_array($email, $_SESSION[$session_var_user_allowed]) && - !$_SESSION['pending_pw_update'] + !$_SESSION['pending_pw_update'] && + !$_SESSION['pending_tfa_setup'] ) { $username = $email; $password = file_get_contents("/etc/sogo-sso/sogo-sso.pass"); diff --git a/data/web/templates/base.twig b/data/web/templates/base.twig index 98fdd86e4..4290edaef 100644 --- a/data/web/templates/base.twig +++ b/data/web/templates/base.twig @@ -377,6 +377,112 @@ function recursiveBase64StrToArrayBuffer(obj) { }); {% endif %} + {% if pending_tfa_setup %} + var setupTFAModal = new bootstrap.Modal(document.getElementById("SetupTFAModal"), { + backdrop: 'static', + keyboard: false + }); + setupTFAModal.show(); + + // Load QR code for TOTP setup in SetupTFAModal + var setupTotpSecret = $('#setup-tfa-qr-img').data('totp-secret'); + if (setupTotpSecret) { + $.ajax({ + type: "GET", + url: "/inc/ajax/qr_gen.php?token=" + encodeURIComponent(setupTotpSecret), + success: function(data) { + $('#setup-tfa-qr-img').attr('src', data); + } + }); + } + + // WebAuthn registration for SetupTFAModal + $('#start_setup_webauthn_register').click(function() { + if (!window.fetch || !navigator.credentials || !navigator.credentials.create) { + window.alert('Browser not supported.'); + return; + } + var keyId = $('#setup_webauthn_reg_form input[name=key_id]').val(); + if (!keyId) { + $('#setup_webauthn_return_code').show().text('Please fill in the key ID first.'); + return; + } + window.fetch('/api/v1/get/webauthn-tfa-registration', {method: 'GET', cache: 'no-cache'}).then(function(response) { + return response.json(); + }).then(function(json) { + if (json.success === false) throw new Error(json.error || 'Registration failed'); + recursiveBase64StrToArrayBuffer(json); + return navigator.credentials.create(json); + }).then(function(cred) { + return { + id: cred.id, + rawId: arrayBufferToBase64(cred.rawId), + response: { + attestationObject: arrayBufferToBase64(cred.response.attestationObject), + clientDataJSON: arrayBufferToBase64(cred.response.clientDataJSON) + }, + type: cred.type + }; + }).then(function(credData) { + $('#setup_webauthn_register_data').val(JSON.stringify(credData)); + $('#setup_webauthn_reg_form input[name=set_tfa]').val('1'); + $('#setup_webauthn_reg_form').submit(); + }).catch(function(err) { + $('#setup_webauthn_return_code').show().text(err.message || 'Registration failed'); + }); + }); + {% endif %} + + {% if pending_pw_update_modal and not pending_tfa_setup and not pending_tfa_methods %} + var changePWModal = new bootstrap.Modal(document.getElementById("ChangePWModal"), { + backdrop: 'static', + keyboard: false + }); + changePWModal.show(); + + $('#changePWModalForm').on('submit', function(e) { + e.preventDefault(); + var newPw = $('#changePWNew').val(); + var newPw2 = $('#changePWNew2').val(); + var role = '{{ mailcow_cc_role }}'; + var username = '{{ mailcow_cc_username }}'; + + var url, attrPayload, itemsPayload; + if (role === 'admin') { + url = '/api/v1/edit/admin'; + attrPayload = {password: newPw, password2: newPw2}; + itemsPayload = [username]; + } else { + url = '/api/v1/edit/self'; + attrPayload = {user_new_pass: newPw, user_new_pass2: newPw2}; + itemsPayload = null; + } + + $('#changePWAlert').hide(); + $.ajax({ + type: 'POST', + url: url, + data: { + attr: JSON.stringify(attrPayload), + items: JSON.stringify(itemsPayload), + csrf_token: '{{ csrf_token }}' + }, + dataType: 'json', + success: function(data) { + if (data && data[0] && data[0].type === 'success') { + window.location.reload(); + } else { + var msg = (data && data[0] && data[0].msg) ? data[0].msg : 'Password change failed.'; + $('#changePWAlert').show().text(msg); + } + }, + error: function() { + $('#changePWAlert').show().text('Request failed. Please try again.'); + } + }); + }); + {% endif %} + // Validate FIDO2 $("#fido2-login").click(function(){ diff --git a/data/web/templates/edit/admin.twig b/data/web/templates/edit/admin.twig index e2c6f66ed..d9cf4aecb 100644 --- a/data/web/templates/edit/admin.twig +++ b/data/web/templates/edit/admin.twig @@ -39,6 +39,24 @@ +
+
+
+ + + {{ lang.tfa.force_tfa_info }} +
+
+
+
+
+
+ + + {{ lang.edit.force_pw_update_info|format(ui_texts.main_name) }} +
+
+
diff --git a/data/web/templates/edit/domainadmin.twig b/data/web/templates/edit/domainadmin.twig index 2c40faaa3..b3e0198c8 100644 --- a/data/web/templates/edit/domainadmin.twig +++ b/data/web/templates/edit/domainadmin.twig @@ -52,6 +52,24 @@
+
+
+
+ + + {{ lang.tfa.force_tfa_info }} +
+
+
+
+
+
+ + + {{ lang.edit.force_pw_update_info|format(ui_texts.main_name) }} +
+
+
diff --git a/data/web/templates/edit/mailbox.twig b/data/web/templates/edit/mailbox.twig index abf0c45ba..34e869ce1 100644 --- a/data/web/templates/edit/mailbox.twig +++ b/data/web/templates/edit/mailbox.twig @@ -24,6 +24,7 @@
+
@@ -317,6 +318,14 @@
+
+
+
+ + {{ lang.tfa.force_tfa_info }} +
+
+
{% if not skip_sogo %}
diff --git a/data/web/templates/modals/footer.twig b/data/web/templates/modals/footer.twig index 8ff112d5d..b39544a63 100644 --- a/data/web/templates/modals/footer.twig +++ b/data/web/templates/modals/footer.twig @@ -311,6 +311,135 @@
{% endif %} +{% if pending_tfa_setup %} + +{% endif %} +{% if pending_pw_update_modal and not pending_tfa_methods %} + +{% endif %} {% if mailcow_cc_role == 'admin' %} +
+
+
+ + {{ lang.tfa.force_tfa_info }} +
+
+
{% if not skip_sogo %}
@@ -237,6 +246,7 @@ + @@ -394,6 +404,14 @@
+
+
+
+ + {{ lang.tfa.force_tfa_info }} +
+
+
{% if not skip_sogo %}
diff --git a/data/web/user.php b/data/web/user.php index 7c34ba953..cadedf4a7 100644 --- a/data/web/user.php +++ b/data/web/user.php @@ -2,90 +2,78 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.user.inc.php'; -if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') { +/* +/ USER +*/ - /* - / USER - */ +// Protect route: user only +protect_route(['user']); - require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php'; - $_SESSION['return_to'] = $_SERVER['REQUEST_URI']; - $username = $_SESSION['mailcow_cc_username']; - $mailboxdata = mailbox('get', 'mailbox_details', $username); - $pushover_data = pushover('get', $username); - $tfa_data = get_tfa(); - $fido2_data = fido2(array("action" => "get_friendly_names")); +require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php'; +$_SESSION['return_to'] = $_SERVER['REQUEST_URI']; +$username = $_SESSION['mailcow_cc_username']; +$mailboxdata = mailbox('get', 'mailbox_details', $username); +$pushover_data = pushover('get', $username); +$tfa_data = get_tfa(); +$fido2_data = fido2(array("action" => "get_friendly_names")); - $clientconfigstr = "host=" . urlencode($mailcow_hostname) . "&email=" . urlencode($username) . "&name=" . urlencode($mailboxdata['name']) . "&ui=" . urlencode(strtok($_SERVER['HTTP_HOST'], ':')) . "&port=" . urlencode($autodiscover_config['caldav']['port']); - if ($autodiscover_config['useEASforOutlook'] == 'yes') - $clientconfigstr .= "&outlookEAS=1"; - if (file_exists('thunderbird-plugins/version.csv')) { - $fh = fopen('thunderbird-plugins/version.csv', 'r'); - if ($fh) { - while (($row = fgetcsv($fh, 1000, ';')) !== FALSE) { - if ($row[0] == 'sogo-connector@inverse.ca') { - $clientconfigstr .= "&connector=" . urlencode($row[1]); - } +$clientconfigstr = "host=" . urlencode($mailcow_hostname) . "&email=" . urlencode($username) . "&name=" . urlencode($mailboxdata['name']) . "&ui=" . urlencode(strtok($_SERVER['HTTP_HOST'], ':')) . "&port=" . urlencode($autodiscover_config['caldav']['port']); +if ($autodiscover_config['useEASforOutlook'] == 'yes') +$clientconfigstr .= "&outlookEAS=1"; +if (file_exists('thunderbird-plugins/version.csv')) { + $fh = fopen('thunderbird-plugins/version.csv', 'r'); + if ($fh) { + while (($row = fgetcsv($fh, 1000, ';')) !== FALSE) { + if ($row[0] == 'sogo-connector@inverse.ca') { + $clientconfigstr .= "&connector=" . urlencode($row[1]); } - fclose($fh); } + fclose($fh); } +} - // Get user information about aliases - $user_get_alias_details = user_get_alias_details($username); - $user_get_alias_details['direct_aliases'] = array_filter($user_get_alias_details['direct_aliases']); - $user_get_alias_details['shared_aliases'] = array_filter($user_get_alias_details['shared_aliases']); - $user_domains[] = mailbox('get', 'mailbox_details', $username)['domain']; - $user_alias_domains = $user_get_alias_details['alias_domains']; - if (!empty($user_alias_domains)) { - $user_domains = array_merge($user_domains, $user_alias_domains); - } +// Get user information about aliases +$user_get_alias_details = user_get_alias_details($username); +$user_get_alias_details['direct_aliases'] = array_filter($user_get_alias_details['direct_aliases']); +$user_get_alias_details['shared_aliases'] = array_filter($user_get_alias_details['shared_aliases']); +$user_domains[] = mailbox('get', 'mailbox_details', $username)['domain']; +$user_alias_domains = $user_get_alias_details['alias_domains']; +if (!empty($user_alias_domains)) { + $user_domains = array_merge($user_domains, $user_alias_domains); +} - // get number of app passwords - $number_of_app_passwords = 0; - foreach (app_passwd("get") as $app_password) - { - $app_password = app_passwd("details", $app_password['id']); - if ($app_password['active']) - { - $number_of_app_passwords++; - } - } +// get number of app passwords +$number_of_app_passwords = 0; +foreach (app_passwd("get") as $app_password) +{ + $app_password = app_passwd("details", $app_password['id']); + if ($app_password['active']) + { + $number_of_app_passwords++; + } +} - $template = 'user.twig'; - $template_data = [ - 'acl' => $_SESSION['acl'], - 'acl_json' => json_encode($_SESSION['acl']), - 'user_spam_score' => mailbox('get', 'spam_score', $username), - 'tfa_data' => $tfa_data, - 'tfa_id' => @$_SESSION['tfa_id'], - 'fido2_data' => $fido2_data, - 'mailboxdata' => $mailboxdata, - 'clientconfigstr' => $clientconfigstr, - 'user_get_alias_details' => $user_get_alias_details, - 'get_tagging_options' => mailbox('get', 'delimiter_action', $username), - 'get_tls_policy' => mailbox('get', 'tls_policy', $username), - 'quarantine_notification' => mailbox('get', 'quarantine_notification', $username), - 'quarantine_category' => mailbox('get', 'quarantine_category', $username), - 'user_domains' => $user_domains, - 'pushover_data' => $pushover_data, - 'lang_user' => json_encode($lang['user']), - 'number_of_app_passwords' => $number_of_app_passwords, - 'lang_datatables' => json_encode($lang['datatables']), - ]; -} -elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') { - header('Location: /admin/dashboard'); - exit(); -} -elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') { - header('Location: /domainadmin/mailbox'); - exit(); -} -else { - header('Location: /'); - exit(); -} +$template = 'user.twig'; +$template_data = [ + 'acl' => $_SESSION['acl'], + 'acl_json' => json_encode($_SESSION['acl']), + 'user_spam_score' => mailbox('get', 'spam_score', $username), + 'tfa_data' => $tfa_data, + 'tfa_id' => @$_SESSION['tfa_id'], + 'fido2_data' => $fido2_data, + 'mailboxdata' => $mailboxdata, + 'clientconfigstr' => $clientconfigstr, + 'user_get_alias_details' => $user_get_alias_details, + 'get_tagging_options' => mailbox('get', 'delimiter_action', $username), + 'get_tls_policy' => mailbox('get', 'tls_policy', $username), + 'quarantine_notification' => mailbox('get', 'quarantine_notification', $username), + 'quarantine_category' => mailbox('get', 'quarantine_category', $username), + 'user_domains' => $user_domains, + 'pushover_data' => $pushover_data, + 'lang_user' => json_encode($lang['user']), + 'number_of_app_passwords' => $number_of_app_passwords, + 'lang_datatables' => json_encode($lang['datatables']), +]; $js_minifier->add('/web/js/site/user.js'); $js_minifier->add('/web/js/site/pwgen.js');