[Web, Dovecot] Allow to define scope of services for app passwords

master
andryyy 2021-10-28 21:57:19 +02:00
parent f01de1a5c0
commit e13bc242a4
No known key found for this signature in database
GPG Key ID: 8EC34FF2794E25EF
14 changed files with 161 additions and 41 deletions

View File

@ -163,24 +163,26 @@ function auth_password_verify(req, pass)
row = cur:fetch (row, "a") row = cur:fetch (row, "a")
end end
-- check against app passwds -- check against app passwds for imap and smtp
-- removed on 22nd Oct 2021: AND IFNULL(JSON_UNQUOTE(JSON_VALUE(mailbox.attributes, '$.force_pw_update')), 0) != '1' -- app passwords are only available for imap and smtp in dovecot
local cur,errorString = con:execute(string.format([[SELECT app_passwd.id, app_passwd.password FROM app_passwd if req.service == "smtp" or req.service == "imap" then
INNER JOIN mailbox ON mailbox.username = app_passwd.mailbox local cur,errorString = con:execute(string.format([[SELECT app_passwd.id, app_passwd.imap_access, app_passwd.smtp_access, app_passwd.password FROM app_passwd
WHERE mailbox = '%s' INNER JOIN mailbox ON mailbox.username = app_passwd.mailbox
AND IFNULL(JSON_UNQUOTE(JSON_VALUE(mailbox.attributes, '$.%s_access')), 1) = '1' WHERE mailbox = '%s'
AND app_passwd.active = '1' AND app_passwd.%s_access = '1'
AND mailbox.active = '1' AND app_passwd.active = '1'
AND app_passwd.domain IN (SELECT domain FROM domain WHERE domain='%s' AND active='1')]], con:escape(req.user), con:escape(req.service), con:escape(req.domain))) AND mailbox.active = '1'
local row = cur:fetch ({}, "a") AND app_passwd.domain IN (SELECT domain FROM domain WHERE domain='%s' AND active='1')]], con:escape(req.user), con:escape(req.service), con:escape(req.domain)))
while row do local row = cur:fetch ({}, "a")
if req.password_verify(req, row.password, pass) == 1 then while row do
cur:close() if req.password_verify(req, row.password, pass) == 1 then
con:execute(string.format([[REPLACE INTO sasl_log (service, app_password, username, real_rip) cur:close()
VALUES ("%s", %d, "%s", "%s")]], con:escape(req.service), row.id, con:escape(req.user), con:escape(req.real_rip))) con:execute(string.format([[REPLACE INTO sasl_log (service, app_password, username, real_rip)
return dovecot.auth.PASSDB_RESULT_OK, "password=" .. pass VALUES ("%s", %d, "%s", "%s")]], con:escape(req.service), row.id, con:escape(req.user), con:escape(req.real_rip)))
return dovecot.auth.PASSDB_RESULT_OK, "password=" .. pass
end
row = cur:fetch (row, "a")
end end
row = cur:fetch (row, "a")
end end
return dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH, "Failed to authenticate" return dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH, "Failed to authenticate"

View File

@ -11,7 +11,7 @@ $template = 'edit.twig';
$template_data = []; $template_data = [];
$result = null; $result = null;
if (isset($_SESSION['mailcow_cc_role'])) { if (isset($_SESSION['mailcow_cc_role'])) {
if ($_SESSION['mailcow_cc_role'] == "admin" || $_SESSION['mailcow_cc_role'] == "domainadmin") { if ($_SESSION['mailcow_cc_role'] == "admin" || $_SESSION['mailcow_cc_role'] == "domainadmin") {
if (isset($_GET["alias"]) && if (isset($_GET["alias"]) &&
!empty($_GET["alias"])) { !empty($_GET["alias"])) {
$alias = html_entity_decode(rawurldecode($_GET["alias"])); $alias = html_entity_decode(rawurldecode($_GET["alias"]));

View File

@ -27,6 +27,11 @@ function app_passwd($_action, $_data = null) {
$password = $_data['app_passwd']; $password = $_data['app_passwd'];
$password2 = $_data['app_passwd2']; $password2 = $_data['app_passwd2'];
$active = intval($_data['active']); $active = intval($_data['active']);
$protocols = (array)$_data['protocols'];
$imap_access = (in_array('imap_access', $protocols)) ? 1 : 0;
$dav_access = (in_array('dav_access', $protocols)) ? 1 : 0;
$smtp_access = (in_array('smtp_access', $protocols)) ? 1 : 0;
$eas_access = (in_array('eas_access', $protocols)) ? 1 : 0;
$domain = mailbox('get', 'mailbox_details', $username)['domain']; $domain = mailbox('get', 'mailbox_details', $username)['domain'];
if (empty($domain)) { if (empty($domain)) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
@ -61,13 +66,17 @@ function app_passwd($_action, $_data = null) {
); );
return false; return false;
} }
$stmt = $pdo->prepare("INSERT INTO `app_passwd` (`name`, `mailbox`, `domain`, `password`, `active`) $stmt = $pdo->prepare("INSERT INTO `app_passwd` (`name`, `mailbox`, `domain`, `password`, `imap_access`, `smtp_access`, `eas_access`, `dav_access`, `active`)
VALUES (:app_name, :mailbox, :domain, :password, :active)"); VALUES (:app_name, :mailbox, :domain, :password, :imap_access, :smtp_access, :eas_access, :dav_access, :active)");
$stmt->execute(array( $stmt->execute(array(
':app_name' => $app_name, ':app_name' => $app_name,
':mailbox' => $username, ':mailbox' => $username,
':domain' => $domain, ':domain' => $domain,
':password' => $password_hashed, ':password' => $password_hashed,
':imap_access' => $imap_access,
':smtp_access' => $smtp_access,
':eas_access' => $eas_access,
':dav_access' => $dav_access,
':active' => $active ':active' => $active
)); ));
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
@ -84,6 +93,19 @@ function app_passwd($_action, $_data = null) {
$app_name = (!empty($_data['app_name'])) ? $_data['app_name'] : $is_now['name']; $app_name = (!empty($_data['app_name'])) ? $_data['app_name'] : $is_now['name'];
$password = (!empty($_data['password'])) ? $_data['password'] : null; $password = (!empty($_data['password'])) ? $_data['password'] : null;
$password2 = (!empty($_data['password2'])) ? $_data['password2'] : null; $password2 = (!empty($_data['password2'])) ? $_data['password2'] : null;
if (isset($_data['protocols'])) {
$protocols = (array)$_data['protocols'];
$imap_access = (in_array('imap_access', $protocols)) ? 1 : 0;
$dav_access = (in_array('dav_access', $protocols)) ? 1 : 0;
$smtp_access = (in_array('smtp_access', $protocols)) ? 1 : 0;
$eas_access = (in_array('eas_access', $protocols)) ? 1 : 0;
}
else {
$imap_access = $is_now['imap_access'];
$smtp_access = $is_now['smtp_access'];
$dav_access = $is_now['dav_access'];
$eas_access = $is_now['eas_access'];
}
$active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active']; $active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active'];
} }
else { else {
@ -122,14 +144,23 @@ function app_passwd($_action, $_data = null) {
':id' => $id ':id' => $id
)); ));
} }
$stmt = $pdo->prepare("UPDATE `app_passwd` SET $stmt = $pdo->prepare("UPDATE `app_passwd` SET
`name` = :app_name, `name` = :app_name,
`mailbox` = :username, `mailbox` = :username,
`imap_access` = :imap_access,
`smtp_access` = :smtp_access,
`eas_access` = :eas_access,
`dav_access` = :dav_access,
`active` = :active `active` = :active
WHERE `id` = :id"); WHERE `id` = :id");
$stmt->execute(array( $stmt->execute(array(
':app_name' => $app_name, ':app_name' => $app_name,
':username' => $username, ':username' => $username,
':imap_access' => $imap_access,
':smtp_access' => $smtp_access,
':eas_access' => $eas_access,
':dav_access' => $dav_access,
':active' => $active, ':active' => $active,
':id' => $id ':id' => $id
)); ));
@ -180,13 +211,7 @@ function app_passwd($_action, $_data = null) {
break; break;
case 'details': case 'details':
$app_passwd_data = array(); $app_passwd_data = array();
$stmt = $pdo->prepare("SELECT `id`, $stmt = $pdo->prepare("SELECT *
`name`,
`mailbox`,
`domain`,
`created`,
`modified`,
`active`
FROM `app_passwd` FROM `app_passwd`
WHERE `id` = :id"); WHERE `id` = :id");
$stmt->execute(array(':id' => $_data)); $stmt->execute(array(':id' => $_data));

View File

@ -807,10 +807,17 @@ function verify_hash($hash, $password) {
} }
return false; return false;
} }
function check_login($user, $pass, $allow_app_passwords = false) { function check_login($user, $pass, $app_passwd_data = false) {
global $pdo; global $pdo;
global $redis; global $redis;
global $imap_server; global $imap_server;
if ($app_passwd_data === false) {
$app_passwd_data['eas'] = false;
$app_passwd_data['dav'] = false;
$app_passwd_data['proxyauth'] = false;
}
if (!filter_var($user, FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $user))) { if (!filter_var($user, FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $user))) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
@ -819,6 +826,8 @@ function check_login($user, $pass, $allow_app_passwords = false) {
); );
return false; return false;
} }
// Validate admin
$user = strtolower(trim($user)); $user = strtolower(trim($user));
$stmt = $pdo->prepare("SELECT `password` FROM `admin` $stmt = $pdo->prepare("SELECT `password` FROM `admin`
WHERE `superadmin` = '1' WHERE `superadmin` = '1'
@ -854,6 +863,8 @@ function check_login($user, $pass, $allow_app_passwords = false) {
} }
} }
} }
// Validate domain admin
$stmt = $pdo->prepare("SELECT `password` FROM `admin` $stmt = $pdo->prepare("SELECT `password` FROM `admin`
WHERE `superadmin` = '0' WHERE `superadmin` = '0'
AND `active`='1' AND `active`='1'
@ -888,6 +899,8 @@ function check_login($user, $pass, $allow_app_passwords = false) {
} }
} }
} }
// Validate mailbox user
$stmt = $pdo->prepare("SELECT `password` FROM `mailbox` $stmt = $pdo->prepare("SELECT `password` FROM `mailbox`
INNER JOIN domain on mailbox.domain = domain.domain INNER JOIN domain on mailbox.domain = domain.domain
WHERE `kind` NOT REGEXP 'location|thing|group' WHERE `kind` NOT REGEXP 'location|thing|group'
@ -896,7 +909,7 @@ function check_login($user, $pass, $allow_app_passwords = false) {
AND `username` = :user"); AND `username` = :user");
$stmt->execute(array(':user' => $user)); $stmt->execute(array(':user' => $user));
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
if ($allow_app_passwords === true) { if ($app_passwd_data['eas'] === true) {
$stmt = $pdo->prepare("SELECT `app_passwd`.`password` as `password`, `app_passwd`.`id` as `app_passwd_id` FROM `app_passwd` $stmt = $pdo->prepare("SELECT `app_passwd`.`password` as `password`, `app_passwd`.`id` as `app_passwd_id` FROM `app_passwd`
INNER JOIN `mailbox` ON `mailbox`.`username` = `app_passwd`.`mailbox` INNER JOIN `mailbox` ON `mailbox`.`username` = `app_passwd`.`mailbox`
INNER JOIN `domain` ON `mailbox`.`domain` = `domain`.`domain` INNER JOIN `domain` ON `mailbox`.`domain` = `domain`.`domain`
@ -904,6 +917,20 @@ function check_login($user, $pass, $allow_app_passwords = false) {
AND `mailbox`.`active` = '1' AND `mailbox`.`active` = '1'
AND `domain`.`active` = '1' AND `domain`.`active` = '1'
AND `app_passwd`.`active` = '1' AND `app_passwd`.`active` = '1'
AND `app_passwd`.`eas_access` = '1'
AND `app_passwd`.`mailbox` = :user");
$stmt->execute(array(':user' => $user));
$rows = array_merge($rows, $stmt->fetchAll(PDO::FETCH_ASSOC));
}
elseif ($app_passwd_data['dav'] === true) {
$stmt = $pdo->prepare("SELECT `app_passwd`.`password` as `password`, `app_passwd`.`id` as `app_passwd_id` FROM `app_passwd`
INNER JOIN `mailbox` ON `mailbox`.`username` = `app_passwd`.`mailbox`
INNER JOIN `domain` ON `mailbox`.`domain` = `domain`.`domain`
WHERE `mailbox`.`kind` NOT REGEXP 'location|thing|group'
AND `mailbox`.`active` = '1'
AND `domain`.`active` = '1'
AND `app_passwd`.`active` = '1'
AND `app_passwd`.`dav_access` = '1'
AND `app_passwd`.`mailbox` = :user"); AND `app_passwd`.`mailbox` = :user");
$stmt->execute(array(':user' => $user)); $stmt->execute(array(':user' => $user));
$rows = array_merge($rows, $stmt->fetchAll(PDO::FETCH_ASSOC)); $rows = array_merge($rows, $stmt->fetchAll(PDO::FETCH_ASSOC));
@ -916,9 +943,20 @@ function check_login($user, $pass, $allow_app_passwords = false) {
'log' => array(__FUNCTION__, $user, '*'), 'log' => array(__FUNCTION__, $user, '*'),
'msg' => array('logged_in_as', $user) 'msg' => array('logged_in_as', $user)
); );
if ($app_passwd_data['proxyauth'] === true) {
$service = ($app_passwd_data['eas'] === true) ? 'EAS' : (($app_passwd_data['dav'] === true) ? 'DAV' : 'SSO');
$stmt = $pdo->prepare("REPLACE INTO sasl_log (`service`, `app_password`, `username`, `real_rip`) VALUES (:service, :app_id, :username, :remote_addr)");
$stmt->execute(array(
':service' => $service,
':app_id' => $row['app_passwd_id'],
':username' => $user,
':remote_addr' => $_SERVER['REMOTE_ADDR']
));
}
return "user"; return "user";
} }
} }
if (!isset($_SESSION['ldelay'])) { if (!isset($_SESSION['ldelay'])) {
$_SESSION['ldelay'] = "0"; $_SESSION['ldelay'] = "0";
$redis->publish("F2B_CHANNEL", "mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']); $redis->publish("F2B_CHANNEL", "mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
@ -929,11 +967,13 @@ function check_login($user, $pass, $allow_app_passwords = false) {
$redis->publish("F2B_CHANNEL", "mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']); $redis->publish("F2B_CHANNEL", "mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']); error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
} }
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $user, '*'), 'log' => array(__FUNCTION__, $user, '*'),
'msg' => 'login_failed' 'msg' => 'login_failed'
); );
sleep($_SESSION['ldelay']); sleep($_SESSION['ldelay']);
return false; return false;
} }

View File

@ -3,7 +3,7 @@ function init_db_schema() {
try { try {
global $pdo; global $pdo;
$db_version = "23082021_2224"; $db_version = "28102021_1600";
$stmt = $pdo->query("SHOW TABLES LIKE 'versions'"); $stmt = $pdo->query("SHOW TABLES LIKE 'versions'");
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
@ -364,6 +364,10 @@ function init_db_schema() {
"password" => "VARCHAR(255) NOT NULL", "password" => "VARCHAR(255) NOT NULL",
"created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
"modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP", "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP",
"imap_access" => "TINYINT(1) NOT NULL DEFAULT '1'",
"smtp_access" => "TINYINT(1) NOT NULL DEFAULT '1'",
"dav_access" => "TINYINT(1) NOT NULL DEFAULT '1'",
"eas_access" => "TINYINT(1) NOT NULL DEFAULT '1'",
"active" => "TINYINT(1) NOT NULL DEFAULT '1'" "active" => "TINYINT(1) NOT NULL DEFAULT '1'"
), ),
"keys" => array( "keys" => array(

View File

@ -101,8 +101,8 @@ jQuery(function($){
$.each(data.sasl, function (i, item) { $.each(data.sasl, function (i, item) {
var datetime = new Date(item.datetime.replace(/-/g, "/")); var datetime = new Date(item.datetime.replace(/-/g, "/"));
var local_datetime = datetime.toLocaleDateString(undefined, {year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit"}); var local_datetime = datetime.toLocaleDateString(undefined, {year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit"});
item.app_password ? app_password = ' <a href="/edit/app-passwd/' + item.app_password + '">(App)</a>' : app_password = "", item.location ? ip_location = ' <span class="flag-icon flag-icon-' + item.location.toLowerCase() + '"></span>' : ip_location = ""; item.app_password ? app_password = ' <a href="/edit/app-passwd/' + item.app_password + '"><i class="bi bi-pen"></i> App</a>' : app_password = "", item.location ? ip_location = ' <span class="flag-icon flag-icon-' + item.location.toLowerCase() + '"></span>' : ip_location = "";
"smtp" == item.service ? service = '<div class="label label-default">' + item.service.toUpperCase() + '<i class="bi bi-chevron-compact-right"></i></div>' : "imap" == item.service ? service = '<div class="label label-default"><i class="bi bi-chevron-compact-left"></i> ' + item.service.toUpperCase() + "</div>" : service = '<div class="label label-default">' + item.service.toUpperCase() + "</div>"; service = '<div class="label label-default">' + item.service.toUpperCase() + '</div>';
item.real_rip.startsWith("Web") ? real_rip = item.real_rip : real_rip = '<a href="https://bgp.he.net/ip/' + item.real_rip + '" target="_blank">' + item.real_rip + "</a>"; item.real_rip.startsWith("Web") ? real_rip = item.real_rip : real_rip = '<a href="https://bgp.he.net/ip/' + item.real_rip + '" target="_blank">' + item.real_rip + "</a>";
ip_data = real_rip + ip_location + app_password; ip_data = real_rip + ip_location + app_password;
$(".last-login").append('<li class="list-group-item">' + local_datetime + " " + service + " " + lang.from + " " + ip_data + "</li>"); $(".last-login").append('<li class="list-group-item">' + local_datetime + " " + service + " " + lang.from + " " + ip_data + "</li>");
@ -258,6 +258,7 @@ jQuery(function($){
{"name":"chkbox","title":"","style":{"maxWidth":"60px","width":"60px","text-align":"center"},"filterable": false,"sortable": false,"type":"html"}, {"name":"chkbox","title":"","style":{"maxWidth":"60px","width":"60px","text-align":"center"},"filterable": false,"sortable": false,"type":"html"},
{"sorted": true,"name":"id","title":"ID","style":{"maxWidth":"60px","width":"60px","text-align":"center"}}, {"sorted": true,"name":"id","title":"ID","style":{"maxWidth":"60px","width":"60px","text-align":"center"}},
{"name":"name","title":lang.app_name}, {"name":"name","title":lang.app_name},
{"name":"protocols","title":lang.allowed_protocols},
{"name":"active","filterable": false,"style":{"maxWidth":"70px","width":"70px"},"title":lang.active,"formatter": function(value){return 1==value?'<i class="bi bi-check-lg"></i>':0==value&&'<i class="bi bi-x-lg"></i>';}}, {"name":"active","filterable": false,"style":{"maxWidth":"70px","width":"70px"},"title":lang.active,"formatter": function(value){return 1==value?'<i class="bi bi-check-lg"></i>':0==value&&'<i class="bi bi-x-lg"></i>';}},
{"name":"action","filterable": false,"sortable": false,"style":{"text-align":"right","min-width":"220px","width":"220px"},"type":"html","title":lang.action,"breakpoints":"xs sm"} {"name":"action","filterable": false,"sortable": false,"style":{"text-align":"right","min-width":"220px","width":"220px"},"type":"html","title":lang.action,"breakpoints":"xs sm"}
], ],
@ -271,7 +272,13 @@ jQuery(function($){
}, },
success: function (data) { success: function (data) {
$.each(data, function (i, item) { $.each(data, function (i, item) {
item.name = escapeHtml(item.name); item.name = escapeHtml(item.name)
item.protocols = []
if (item.imap_access == 1) { item.protocols.push("<code>IMAP</code>"); }
if (item.smtp_access == 1) { item.protocols.push("<code>SMTP</code>"); }
if (item.eas_access == 1) { item.protocols.push("<code>EAS/ActiveSync</code>"); }
if (item.dav_access == 1) { item.protocols.push("<code>DAV</kbd>"); }
item.protocols = item.protocols.join(" ")
if (acl_data.app_passwds === 1) { if (acl_data.app_passwds === 1) {
item.action = '<div class="btn-group footable-actions">' + item.action = '<div class="btn-group footable-actions">' +
'<a href="/edit/app-passwd/' + item.id + '" class="btn btn-xs btn-xs-half btn-default"><i class="bi bi-pencil-fill"></i> ' + lang.edit + '</a>' + '<a href="/edit/app-passwd/' + item.id + '" class="btn btn-xs btn-xs-half btn-default"><i class="bi bi-pencil-fill"></i> ' + lang.edit + '</a>' +

View File

@ -42,6 +42,7 @@
"alias_domain_info": "<small>Nur gültige Domains. Getrennt durch Komma.</small>", "alias_domain_info": "<small>Nur gültige Domains. Getrennt durch Komma.</small>",
"app_name": "App-Name", "app_name": "App-Name",
"app_password": "App-Passwort hinzufügen", "app_password": "App-Passwort hinzufügen",
"app_passwd_protocols": "Zugelassene Protokolle für App-Passwort",
"automap": "Ordner automatisch mappen (\"Sent items\", \"Sent\" => \"Sent\" etc.)", "automap": "Ordner automatisch mappen (\"Sent items\", \"Sent\" => \"Sent\" etc.)",
"backup_mx_options": "Relay-Optionen", "backup_mx_options": "Relay-Optionen",
"bcc_dest_format": "BCC-Ziel muss eine gültige E-Mail-Adresse sein.", "bcc_dest_format": "BCC-Ziel muss eine gültige E-Mail-Adresse sein.",
@ -508,6 +509,7 @@
"allowed_protocols": "Erlaubte Protokolle", "allowed_protocols": "Erlaubte Protokolle",
"app_name": "App-Name", "app_name": "App-Name",
"app_passwd": "App-Passwörter", "app_passwd": "App-Passwörter",
"app_passwd_protocols": "Zugelassene Protokolle für App-Passwort",
"automap": "Ordner automatisch mappen (\"Sent items\", \"Sent\" => \"Sent\" etc.)", "automap": "Ordner automatisch mappen (\"Sent items\", \"Sent\" => \"Sent\" etc.)",
"backup_mx_options": "Relay-Optionen", "backup_mx_options": "Relay-Optionen",
"bcc_dest_format": "BCC-Ziel muss eine gültige E-Mail-Adresse sein.", "bcc_dest_format": "BCC-Ziel muss eine gültige E-Mail-Adresse sein.",
@ -992,6 +994,7 @@
"aliases_also_send_as": "Darf außerdem versenden als Benutzer", "aliases_also_send_as": "Darf außerdem versenden als Benutzer",
"aliases_send_as_all": "Absender für folgende Domains und zugehörige Alias-Domains nicht prüfen", "aliases_send_as_all": "Absender für folgende Domains und zugehörige Alias-Domains nicht prüfen",
"app_hint": "App-Passwörter sind alternative Passwörter für den IMAP-, SMTP-, CalDAV-, CardDAV- und EAS-Login am Mailserver. Der Benutzername bleibt unverändert.<br>SOGo Webmail ist mit diesem Kennwort nicht verwendbar.", "app_hint": "App-Passwörter sind alternative Passwörter für den IMAP-, SMTP-, CalDAV-, CardDAV- und EAS-Login am Mailserver. Der Benutzername bleibt unverändert.<br>SOGo Webmail ist mit diesem Kennwort nicht verwendbar.",
"allowed_protocols": "Erlaubte Protokolle",
"app_name": "App-Name", "app_name": "App-Name",
"app_passwds": "App-Passwörter", "app_passwds": "App-Passwörter",
"apple_connection_profile": "Apple-Verbindungsprofil", "apple_connection_profile": "Apple-Verbindungsprofil",

View File

@ -42,6 +42,7 @@
"alias_domain_info": "<small>Valid domain names only (comma-separated).</small>", "alias_domain_info": "<small>Valid domain names only (comma-separated).</small>",
"app_name": "App name", "app_name": "App name",
"app_password": "Add app password", "app_password": "Add app password",
"app_passwd_protocols": "Allowed protocols for app password",
"automap": "Try to automap folders (\"Sent items\", \"Sent\" => \"Sent\" etc.)", "automap": "Try to automap folders (\"Sent items\", \"Sent\" => \"Sent\" etc.)",
"backup_mx_options": "Relay options", "backup_mx_options": "Relay options",
"bcc_dest_format": "BCC destination must be a single valid email address.<br>If you need to send a copy to multiple addresses, create an alias and use it here.", "bcc_dest_format": "BCC destination must be a single valid email address.<br>If you need to send a copy to multiple addresses, create an alias and use it here.",
@ -514,6 +515,7 @@
"allowed_protocols": "Allowed protocols", "allowed_protocols": "Allowed protocols",
"app_name": "App name", "app_name": "App name",
"app_passwd": "App password", "app_passwd": "App password",
"app_passwd_protocols": "Allowed protocols for app password",
"automap": "Try to automap folders (\"Sent items\", \"Sent\" => \"Sent\" etc.)", "automap": "Try to automap folders (\"Sent items\", \"Sent\" => \"Sent\" etc.)",
"backup_mx_options": "Relay options", "backup_mx_options": "Relay options",
"bcc_dest_format": "BCC destination must be a single valid email address.<br>If you need to send a copy to multiple addresses, create an alias and use it here.", "bcc_dest_format": "BCC destination must be a single valid email address.<br>If you need to send a copy to multiple addresses, create an alias and use it here.",
@ -1034,6 +1036,7 @@
"aliases_also_send_as": "Also allowed to send as user", "aliases_also_send_as": "Also allowed to send as user",
"aliases_send_as_all": "Do not check sender access for the following domain(s) and its alias domains", "aliases_send_as_all": "Do not check sender access for the following domain(s) and its alias domains",
"app_hint": "App passwords are alternative passwords for your IMAP, SMTP, CalDAV, CardDAV and EAS login. The username remains unchanged. SOGo webmail is not available through app passwords.", "app_hint": "App passwords are alternative passwords for your IMAP, SMTP, CalDAV, CardDAV and EAS login. The username remains unchanged. SOGo webmail is not available through app passwords.",
"allowed_protocols": "Allowed protocols",
"app_name": "App name", "app_name": "App name",
"app_passwds": "App passwords", "app_passwds": "App passwords",
"apple_connection_profile": "Apple connection profile", "apple_connection_profile": "Apple connection profile",

View File

@ -14,8 +14,16 @@ if (isset($_SERVER['PHP_AUTH_USER'])) {
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
$username = $_SERVER['PHP_AUTH_USER']; $username = $_SERVER['PHP_AUTH_USER'];
$password = $_SERVER['PHP_AUTH_PW']; $password = $_SERVER['PHP_AUTH_PW'];
$is_eas = preg_match('/^(\/SOGo|)\/(dav|Microsoft-Server-ActiveSync).*/', $_SERVER['HTTP_X_ORIGINAL_URI']); $is_eas = false;
$login_check = check_login($username, $password, $is_eas); $is_dav = false;
$original_uri = isset($_SERVER['HTTP_X_ORIGINAL_URI']) ? $_SERVER['HTTP_X_ORIGINAL_URI'] : '';
if (preg_match('/^(\/SOGo|)\/dav.*/', $original_uri) === 1) {
$is_dav = true;
}
elseif (preg_match('/^(\/SOGo|)\/Microsoft-Server-ActiveSync.*/', $original_uri) === 1) {
$is_eas = true;
}
$login_check = check_login($username, $password, array('dav' => $is_dav, 'eas' => $is_eas, 'proxyauth' => true));
if ($login_check === 'user') { if ($login_check === 'user') {
header("X-User: $username"); header("X-User: $username");
header("X-Auth: Basic ".base64_encode("$username:$password")); header("X-Auth: Basic ".base64_encode("$username:$password"));
@ -44,6 +52,13 @@ elseif (isset($_GET['login'])) {
// register username and password in session // register username and password in session
$_SESSION[$session_var_user_allowed][] = $login; $_SESSION[$session_var_user_allowed][] = $login;
$_SESSION[$session_var_pass] = $sogo_sso_pass; $_SESSION[$session_var_pass] = $sogo_sso_pass;
// 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)");
$stmt->execute(array(
':username' => $login,
':remote_addr' => $_SERVER['REMOTE_ADDR']
));
// redirect to sogo (sogo will get the correct credentials via nginx auth_request // redirect to sogo (sogo will get the correct credentials via nginx auth_request
header("Location: /SOGo/so/${login}"); header("Location: /SOGo/so/${login}");
exit; exit;
@ -55,9 +70,7 @@ elseif (isset($_GET['login'])) {
exit; exit;
} }
// only check for admin-login on sogo GUI requests // only check for admin-login on sogo GUI requests
elseif ( elseif (isset($_SERVER['HTTP_X_ORIGINAL_URI']) && strcasecmp(substr($_SERVER['HTTP_X_ORIGINAL_URI'], 0, 9), "/SOGo/so/") === 0) {
strcasecmp(substr($_SERVER['HTTP_X_ORIGINAL_URI'], 0, 9), "/SOGo/so/") === 0
) {
// this is an nginx auth_request call, we check for existing sogo-sso session variables // this is an nginx auth_request call, we check for existing sogo-sso session variables
session_start(); session_start();
// extract email address from "/SOGo/so/user@domain/xy" // extract email address from "/SOGo/so/user@domain/xy"

View File

@ -170,7 +170,7 @@ function recursiveBase64StrToArrayBuffer(obj) {
// TFA, CSRF, Alerts in footer.inc.php // TFA, CSRF, Alerts in footer.inc.php
// Other general functions in mailcow.js // Other general functions in mailcow.js
{% for alert_type, alert_msg in alerts %} {% for alert_type, alert_msg in alerts %}
mailcow_alert_box('{{ alert_msg|raw }}', '{{ alert_type }}'); mailcow_alert_box('{{ alert_msg|raw|replace({"\n": "", "\r": "", "\t": "<br>"}) }}', '{{ alert_type }}');
{% endfor %} {% endfor %}
// Confirm TFA modal // Confirm TFA modal

View File

@ -5,6 +5,7 @@
<h4>{{ lang.edit.app_passwd }}</h4> <h4>{{ lang.edit.app_passwd }}</h4>
<form class="form-horizontal" data-pwgen-length="32" data-id="editapp" role="form" method="post"> <form class="form-horizontal" data-pwgen-length="32" data-id="editapp" role="form" method="post">
<input type="hidden" value="0" name="active"> <input type="hidden" value="0" name="active">
<input type="hidden" value="0" name="protocols">
<div class="form-group"> <div class="form-group">
<label class="control-label col-sm-2" for="app_name">{{ lang.edit.app_name }}</label> <label class="control-label col-sm-2" for="app_name">{{ lang.edit.app_name }}</label>
<div class="col-sm-10"> <div class="col-sm-10">
@ -30,6 +31,17 @@
</div> </div>
</div> </div>
</div> </div>
<div class="form-group">
<label class="control-label col-sm-2" for="protocols">{{ lang.edit.app_passwd_protocols }}</label>
<div class="col-sm-10">
<select class="full-width-select" data-live-search="true" id="protocols" name="protocols" multiple>
<option value="imap_access" {% if result.imap_access == 1 %} selected{% endif %}>IMAP</option>
<option value="smtp_access" {% if result.smtp_access == 1 %} selected{% endif %}>SMTP</option>
<option value="eas_access" {% if result.eas_access == 1 %} selected{% endif %}>EAS/ActiveSync</option>
<option value="dav_access" {% if result.dav_access == 1 %} selected{% endif %}>CardDAV/CalDAV</option>
</select>
</div>
</div>
<div class="form-group"> <div class="form-group">
<div class="col-sm-offset-2 col-sm-10"> <div class="col-sm-offset-2 col-sm-10">
<button class="btn btn-xs-lg visible-xs-block visible-sm-inline visible-md-inline visible-lg-inline btn-success" data-action="edit_selected" data-id="editapp" data-item="{{ result.id }}" data-api-url='edit/app-passwd' data-api-attr='{}' href="#">{{ lang.edit.save }}</button> <button class="btn btn-xs-lg visible-xs-block visible-sm-inline visible-md-inline visible-lg-inline btn-success" data-action="edit_selected" data-id="editapp" data-item="{{ result.id }}" data-api-url='edit/app-passwd' data-api-attr='{}' href="#">{{ lang.edit.save }}</button>

View File

@ -213,6 +213,17 @@
<p class="help-block">{{ lang.user.new_password_description }}</p> <p class="help-block">{{ lang.user.new_password_description }}</p>
</div> </div>
</div> </div>
<div class="form-group">
<label class="control-label col-sm-2" for="protocols">{{ lang.add.app_passwd_protocols }}</label>
<div class="col-sm-10">
<select class="full-width-select" data-live-search="true" id="protocols" name="protocols" multiple>
<option value="imap_access" selected>IMAP</option>
<option value="smtp_access" selected>SMTP</option>
<option value="eas_access" selected>EAS/ActiveSync</option>
<option value="dav_access" selected>CardDAV/CalDAV</option>
</select>
</div>
</div>
<div class="form-group"> <div class="form-group">
<div class="col-sm-offset-2 col-sm-10"> <div class="col-sm-offset-2 col-sm-10">
<div class="checkbox"> <div class="checkbox">

View File

@ -11,7 +11,7 @@
<i class="bi bi-inbox-fill"></i> {{ lang.user.open_webmail_sso }} <i class="bi bi-inbox-fill"></i> {{ lang.user.open_webmail_sso }}
</button> </button>
{% else %} {% else %}
<a target="_blank" href="#" disabled role="button" class="btn btn-default btn-block btn-xs-lg"> <a target="_blank" href="/sogo-auth.php?login={{ mailcow_cc_username }}" role="button" class="btn btn-default btn-block btn-xs-lg">
<i class="bi bi-inbox-fill"></i> {{ lang.user.open_webmail_sso }} <i class="bi bi-inbox-fill"></i> {{ lang.user.open_webmail_sso }}
</a> </a>
{% endif %} {% endif %}

View File

@ -211,7 +211,7 @@ services:
- sogo - sogo
dovecot-mailcow: dovecot-mailcow:
image: mailcow/dovecot:1.157 image: mailcow/dovecot:1.158
depends_on: depends_on:
- mysql-mailcow - mysql-mailcow
dns: dns: