diff --git a/data/Dockerfiles/dovecot/Dockerfile b/data/Dockerfiles/dovecot/Dockerfile index 9433dd2ea..a35589689 100644 --- a/data/Dockerfiles/dovecot/Dockerfile +++ b/data/Dockerfiles/dovecot/Dockerfile @@ -26,6 +26,7 @@ RUN addgroup -g 5000 vmail \ curl \ jq \ lua \ + lua-json \ lua-cjson \ lua-socket \ lua-sql-mysql \ diff --git a/data/Dockerfiles/dovecot/docker-entrypoint.sh b/data/Dockerfiles/dovecot/docker-entrypoint.sh index a9545f338..f146a8a01 100755 --- a/data/Dockerfiles/dovecot/docker-entrypoint.sh +++ b/data/Dockerfiles/dovecot/docker-entrypoint.sh @@ -129,114 +129,86 @@ iterate_query = SELECT username FROM mailbox WHERE active = '1' OR active = '2'; EOF cat < /etc/dovecot/lua/passwd-verify.lua -function auth_password_verify(req, pass) - - if req.domain == nil then +function auth_password_verify(request, password) + if request.domain == nil then return dovecot.auth.PASSDB_RESULT_USER_UNKNOWN, "No such user" end - if cur == nil then - script_init() - end - - if req.user == nil then - req.user = '' - end - - respbody = {} + json = require "json" + ltn12 = require "ltn12" + http = require "socket.http" + http.TIMEOUT = 5 + mysql = require "luasql.mysql" + env = mysql.mysql() + con = env:connect("__DBNAME__","__DBUSER__","__DBPASS__","localhost") + local req = { + username = request.user, + password = password + } + local req_json = json.encode(req) + local res = {} + -- check against mailbox passwds - local cur,errorString = con:execute(string.format([[SELECT password FROM mailbox - WHERE username = '%s' - AND active = '1' - AND domain IN (SELECT domain FROM domain WHERE domain='%s' AND active='1') - AND IFNULL(JSON_UNQUOTE(JSON_VALUE(mailbox.attributes, '$.force_pw_update')), 0) != '1' - AND IFNULL(JSON_UNQUOTE(JSON_VALUE(attributes, '$.%s_access')), 1) = '1']], con:escape(req.user), con:escape(req.domain), con:escape(req.service))) - local row = cur:fetch ({}, "a") - while row do - if req.password_verify(req, row.password, pass) == 1 then - con:execute(string.format([[REPLACE INTO sasl_log (service, app_password, username, real_rip) - VALUES ("%s", 0, "%s", "%s")]], con:escape(req.service), con:escape(req.user), con:escape(req.real_rip))) - cur:close() - con:close() - return dovecot.auth.PASSDB_RESULT_OK, "" - end - row = cur:fetch (row, "a") + local b, c = http.request { + method = "POST", + url = "https://nginx/api/v1/process/login", + source = ltn12.source.string(req_json), + headers = { + ["content-type"] = "application/json", + ["content-length"] = tostring(#req_json) + }, + sink = ltn12.sink.table(res) + } + local api_response = json.decode(table.concat(res)) + if api_response.role == 'user' then + con:execute(string.format([[REPLACE INTO sasl_log (service, app_password, username, real_rip) + VALUES ("%s", 0, "%s", "%s")]], con:escape(request.service), con:escape(request.user), con:escape(request.real_rip))) + con:close() + return dovecot.auth.PASSDB_RESULT_OK, "password=" .. password end + -- check against app passwds for imap and smtp -- app passwords are only available for imap, smtp, sieve and pop3 when using sasl - if req.service == "smtp" or req.service == "imap" or req.service == "sieve" or req.service == "pop3" then - local cur,errorString = con:execute(string.format([[SELECT app_passwd.id, %s_access AS has_prot_access, app_passwd.password FROM app_passwd - INNER JOIN mailbox ON mailbox.username = app_passwd.mailbox - WHERE mailbox = '%s' - AND app_passwd.active = '1' - AND mailbox.active = '1' - AND app_passwd.domain IN (SELECT domain FROM domain WHERE domain='%s' AND active='1')]], con:escape(req.service), con:escape(req.user), con:escape(req.domain))) - local row = cur:fetch ({}, "a") - while row do - if req.password_verify(req, row.password, pass) == 1 then - -- if password is valid and protocol access is 1 OR real_rip matches SOGo, proceed - if tostring(req.real_rip) == "__IPV4_SOGO__" then - cur:close() - con:close() - return dovecot.auth.PASSDB_RESULT_OK, "" - elseif row.has_prot_access == "1" then - con:execute(string.format([[REPLACE INTO sasl_log (service, app_password, username, real_rip) - VALUES ("%s", %d, "%s", "%s")]], con:escape(req.service), row.id, con:escape(req.user), con:escape(req.real_rip))) - cur:close() - con:close() - return dovecot.auth.PASSDB_RESULT_OK, "" - end + if request.service == "smtp" or request.service == "imap" or request.service == "sieve" or request.service == "pop3" then + req.protocol = {} + req.protocol[request.service] = true + req_json = json.encode(req) + + req.protocol.ignore_hasaccess = false + if tostring(req.real_rip) == "__IPV4_SOGO__" then + req.protocol.ignore_hasaccess = true + end + + local b, c = http.request { + method = "POST", + url = "https://nginx/api/v1/process/login", + source = ltn12.source.string(req_json), + headers = { + ["content-type"] = "application/json", + ["content-length"] = tostring(#req_json) + }, + sink = ltn12.sink.table(res) + } + local api_response = json.decode(table.concat(res)) + if api_response.role == 'user' then + if req.protocol.ignore_hasaccess == false then + con:execute(string.format([[REPLACE INTO sasl_log (service, app_password, username, real_rip) + VALUES ("%s", %d, "%s", "%s")]], con:escape(req.service), row.id, con:escape(req.user), con:escape(req.real_rip))) end - row = cur:fetch (row, "a") + con:close() + return dovecot.auth.PASSDB_RESULT_OK, "password=" .. password end end - - cur:close() + con:close() - return dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH, "Failed to authenticate" - - -- PoC - -- local reqbody = string.format([[{ - -- "success":0, - -- "service":"%s", - -- "app_password":false, - -- "username":"%s", - -- "real_rip":"%s" - -- }]], con:escape(req.service), con:escape(req.user), con:escape(req.real_rip)) - -- http.request { - -- method = "POST", - -- url = "http://nginx:8081/sasl_log.php", - -- source = ltn12.source.string(reqbody), - -- headers = { - -- ["content-type"] = "application/json", - -- ["content-length"] = tostring(#reqbody) - -- }, - -- sink = ltn12.sink.table(respbody) - -- } - end function auth_passdb_lookup(req) return dovecot.auth.PASSDB_RESULT_USER_UNKNOWN, "" end - -function script_init() - mysql = require "luasql.mysql" - http = require "socket.http" - http.TIMEOUT = 5 - ltn12 = require "ltn12" - env = mysql.mysql() - con = env:connect("__DBNAME__","__DBUSER__","__DBPASS__","localhost") - return 0 -end - -function script_deinit() - con:close() - env:close() -end EOF # Replace patterns in app-passdb.lua diff --git a/data/web/inc/functions.auth.inc.php b/data/web/inc/functions.auth.inc.php new file mode 100644 index 000000000..3f4f2f3b7 --- /dev/null +++ b/data/web/inc/functions.auth.inc.php @@ -0,0 +1,278 @@ + 'danger', + 'log' => array(__FUNCTION__, $user, '*'), + 'msg' => 'malformed_username' + ); + return false; + } + + // Validate admin + $result = mailcow_admin_login($user, $pass); + if ($result){ + return $result; + } + + // Validate domain admin + $result = mailcow_domainadmin_login($user, $pass); + if ($result){ + return $result; + } + + // Validate mailbox user + // skip log & ldelay if requests comes from dovecot + $is_dovecot = false; + $request_ip = ($_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR']); + if ($request_ip == getenv('IPV4_NETWORK').'.250'){ + $is_dovecot = true; + } + // check authsource + $stmt = $pdo->prepare("SELECT authsource FROM `mailbox` + INNER JOIN domain on mailbox.domain = domain.domain + WHERE `kind` NOT REGEXP 'location|thing|group' + AND `mailbox`.`active`='1' + AND `domain`.`active`='1' + AND `username` = :user"); + $stmt->execute(array(':user' => $user)); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + if ($row['authsource'] == 'keycloak'){ + $result = keycloak_mbox_login($user, $pass, $is_dovecot); + if ($result){ + return $result; + } + } else { + $result = mailcow_mbox_login($user, $pass, $app_passwd_data, $is_dovecot); + if ($result){ + return $result; + } + } + + // skip log and only return false + // netfilter uses dovecot error log for banning + if ($is_dovecot){ + return false; + } + if (!isset($_SESSION['ldelay'])) { + $_SESSION['ldelay'] = "0"; + $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']); + } + elseif (!isset($_SESSION['mailcow_cc_username'])) { + $_SESSION['ldelay'] = $_SESSION['ldelay']+0.5; + $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']); + } + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $user, '*'), + 'msg' => 'login_failed' + ); + + sleep($_SESSION['ldelay']); + return false; +} + +function mailcow_mbox_login($user, $pass, $app_passwd_data = false, $is_internal = false){ + global $pdo; + + $stmt = $pdo->prepare("SELECT * FROM `mailbox` + INNER JOIN domain on mailbox.domain = domain.domain + WHERE `kind` NOT REGEXP 'location|thing|group' + AND `mailbox`.`active`='1' + AND `domain`.`active`='1' + AND (`mailbox`.`authsource`='mailcow' OR `mailbox`.`authsource` IS NULL) + AND `username` = :user"); + $stmt->execute(array(':user' => $user)); + $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + + // check if password is app password + $is_app_passwd = false; + if ($app_passwd_data['eas']){ + $is_app_passwd = 'eas'; + } else if ($app_passwd_data['dav']){ + $is_app_passwd = 'dav'; + } else if ($app_passwd_data['smtp']){ + $is_app_passwd = 'smtp'; + } else if ($app_passwd_data['imap']){ + $is_app_passwd = 'imap'; + } else if ($app_passwd_data['sieve']){ + $is_app_passwd = 'sieve'; + } else if ($app_passwd_data['pop3']){ + $is_app_passwd = 'pop3'; + } + if ($is_app_passwd){ + // fetch app password data + $app_passwd_query = "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`.`mailbox` = :user"; + // check if app password has protocol access + // skip if $app_passwd_data['ignore_hasaccess'] is true and the call is not external + if (!$app_passwd_data['ignore_hasaccess'] || !$is_internal){ + $app_passwd_query = $app_passwd_query . " AND `app_passwd`.`" . $is_app_passwd . "_access` = '1'"; + } + // fetch password data + $stmt = $pdo->prepare($app_passwd_query); + $stmt->execute(array(':user' => $user)); + $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + } + + foreach ($rows as $row) { + // verify password + if (verify_hash($row['password'], $pass) !== false) { + if (!$is_app_passwd){ + // password is not a app password + // check for tfa authenticators + $authenticators = get_tfa($user); + if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0 && !$is_internal) { + // authenticators found, init TFA flow + $_SESSION['pending_mailcow_cc_username'] = $user; + $_SESSION['pending_mailcow_cc_role'] = "user"; + $_SESSION['pending_tfa_methods'] = $authenticators['additional']; + unset($_SESSION['ldelay']); + $_SESSION['return'][] = array( + 'type' => 'success', + 'log' => array(__FUNCTION__, $user, '*'), + 'msg' => array('logged_in_as', $user) + ); + return "pending"; + } else if (!isset($authenticators['additional']) || !is_array($authenticators['additional']) || count($authenticators['additional']) == 0) { + // no authenticators found, login successfull + if (!$is_internal){ + unset($_SESSION['ldelay']); + // Reactivate TFA if it was set to "deactivate TFA for next login" + $stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user"); + $stmt->execute(array(':user' => $user)); + // skip log + $_SESSION['return'][] = array( + 'type' => 'success', + 'log' => array(__FUNCTION__, $user, '*'), + 'msg' => array('logged_in_as', $user) + ); + } + return "user"; + } + } elseif ($is_app_passwd) { + // password is a app password + if ($is_internal){ + // skip log + return "user"; + } + + $service = strtoupper($is_app_passwd); + $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['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR']) + )); + + unset($_SESSION['ldelay']); + return "user"; + } + } + } + + return false; +} +function mailcow_domainadmin_login($user, $pass){ + global $pdo; + + $stmt = $pdo->prepare("SELECT `password` FROM `admin` + WHERE `superadmin` = '0' + AND `active`='1' + AND `username` = :user"); + $stmt->execute(array(':user' => $user)); + $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + foreach ($rows as $row) { + // verify password + if (verify_hash($row['password'], $pass) !== false) { + // check for tfa authenticators + $authenticators = get_tfa($user); + if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0) { + $_SESSION['pending_mailcow_cc_username'] = $user; + $_SESSION['pending_mailcow_cc_role'] = "domainadmin"; + $_SESSION['pending_tfa_methods'] = $authenticators['additional']; + unset($_SESSION['ldelay']); + $_SESSION['return'][] = array( + 'type' => 'info', + 'log' => array(__FUNCTION__, $user, '*'), + 'msg' => 'awaiting_tfa_confirmation' + ); + return "pending"; + } + else { + unset($_SESSION['ldelay']); + // Reactivate TFA if it was set to "deactivate TFA for next login" + $stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user"); + $stmt->execute(array(':user' => $user)); + $_SESSION['return'][] = array( + 'type' => 'success', + 'log' => array(__FUNCTION__, $user, '*'), + 'msg' => array('logged_in_as', $user) + ); + return "domainadmin"; + } + } + } + + return false; +} +function mailcow_admin_login($user, $pass){ + global $pdo; + + $user = strtolower(trim($user)); + $stmt = $pdo->prepare("SELECT `password` FROM `admin` + WHERE `superadmin` = '1' + AND `active` = '1' + AND `username` = :user"); + $stmt->execute(array(':user' => $user)); + $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + foreach ($rows as $row) { + // verify password + if (verify_hash($row['password'], $pass)) { + // check for tfa authenticators + $authenticators = get_tfa($user); + if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0) { + // active tfa authenticators found, set pending user login + $_SESSION['pending_mailcow_cc_username'] = $user; + $_SESSION['pending_mailcow_cc_role'] = "admin"; + $_SESSION['pending_tfa_methods'] = $authenticators['additional']; + unset($_SESSION['ldelay']); + $_SESSION['return'][] = array( + 'type' => 'info', + 'log' => array(__FUNCTION__, $user, '*'), + 'msg' => 'awaiting_tfa_confirmation' + ); + return "pending"; + } else { + unset($_SESSION['ldelay']); + // Reactivate TFA if it was set to "deactivate TFA for next login" + $stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user"); + $stmt->execute(array(':user' => $user)); + $_SESSION['return'][] = array( + 'type' => 'success', + 'log' => array(__FUNCTION__, $user, '*'), + 'msg' => array('logged_in_as', $user) + ); + return "admin"; + } + } + } + + return false; +} + +function keycloak_mbox_login($user, $pass, $is_internal = false){ + return false; +} diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php index ee1aab236..28cdc1797 100644 --- a/data/web/inc/functions.inc.php +++ b/data/web/inc/functions.inc.php @@ -811,204 +811,6 @@ function verify_hash($hash, $password) { } return false; } -function check_login($user, $pass, $app_passwd_data = false) { - global $pdo; - global $redis; - global $imap_server; - - if (!filter_var($user, FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $user))) { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $user, '*'), - 'msg' => 'malformed_username' - ); - return false; - } - - // Validate admin - $user = strtolower(trim($user)); - $stmt = $pdo->prepare("SELECT `password` FROM `admin` - WHERE `superadmin` = '1' - AND `active` = '1' - AND `username` = :user"); - $stmt->execute(array(':user' => $user)); - $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); - foreach ($rows as $row) { - // verify password - if (verify_hash($row['password'], $pass)) { - // check for tfa authenticators - $authenticators = get_tfa($user); - if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0) { - // active tfa authenticators found, set pending user login - $_SESSION['pending_mailcow_cc_username'] = $user; - $_SESSION['pending_mailcow_cc_role'] = "admin"; - $_SESSION['pending_tfa_methods'] = $authenticators['additional']; - unset($_SESSION['ldelay']); - $_SESSION['return'][] = array( - 'type' => 'info', - 'log' => array(__FUNCTION__, $user, '*'), - 'msg' => 'awaiting_tfa_confirmation' - ); - return "pending"; - } else { - unset($_SESSION['ldelay']); - // Reactivate TFA if it was set to "deactivate TFA for next login" - $stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user"); - $stmt->execute(array(':user' => $user)); - $_SESSION['return'][] = array( - 'type' => 'success', - 'log' => array(__FUNCTION__, $user, '*'), - 'msg' => array('logged_in_as', $user) - ); - return "admin"; - } - } - } - - // Validate domain admin - $stmt = $pdo->prepare("SELECT `password` FROM `admin` - WHERE `superadmin` = '0' - AND `active`='1' - AND `username` = :user"); - $stmt->execute(array(':user' => $user)); - $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); - foreach ($rows as $row) { - // verify password - if (verify_hash($row['password'], $pass) !== false) { - // check for tfa authenticators - $authenticators = get_tfa($user); - if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0) { - $_SESSION['pending_mailcow_cc_username'] = $user; - $_SESSION['pending_mailcow_cc_role'] = "domainadmin"; - $_SESSION['pending_tfa_methods'] = $authenticators['additional']; - unset($_SESSION['ldelay']); - $_SESSION['return'][] = array( - 'type' => 'info', - 'log' => array(__FUNCTION__, $user, '*'), - 'msg' => 'awaiting_tfa_confirmation' - ); - return "pending"; - } - else { - unset($_SESSION['ldelay']); - // Reactivate TFA if it was set to "deactivate TFA for next login" - $stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user"); - $stmt->execute(array(':user' => $user)); - $_SESSION['return'][] = array( - 'type' => 'success', - 'log' => array(__FUNCTION__, $user, '*'), - 'msg' => array('logged_in_as', $user) - ); - return "domainadmin"; - } - } - } - - // Validate mailbox user - $stmt = $pdo->prepare("SELECT `password` FROM `mailbox` - INNER JOIN domain on mailbox.domain = domain.domain - WHERE `kind` NOT REGEXP 'location|thing|group' - AND `mailbox`.`active`='1' - AND `domain`.`active`='1' - AND `username` = :user"); - $stmt->execute(array(':user' => $user)); - $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); - 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` - 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`.`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"); - $stmt->execute(array(':user' => $user)); - $rows = array_merge($rows, $stmt->fetchAll(PDO::FETCH_ASSOC)); - } - foreach ($rows as $row) { - // verify password - if (verify_hash($row['password'], $pass) !== false) { - if (!array_key_exists("app_passwd_id", $row)){ - // password is not a app password - // check for tfa authenticators - $authenticators = get_tfa($user); - if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0 && - $app_passwd_data['eas'] !== true && $app_passwd_data['dav'] !== true) { - // authenticators found, init TFA flow - $_SESSION['pending_mailcow_cc_username'] = $user; - $_SESSION['pending_mailcow_cc_role'] = "user"; - $_SESSION['pending_tfa_methods'] = $authenticators['additional']; - unset($_SESSION['ldelay']); - $_SESSION['return'][] = array( - 'type' => 'success', - 'log' => array(__FUNCTION__, $user, '*'), - 'msg' => array('logged_in_as', $user) - ); - return "pending"; - } else if (!isset($authenticators['additional']) || !is_array($authenticators['additional']) || count($authenticators['additional']) == 0) { - unset($_SESSION['ldelay']); - // no authenticators found, login successfull - // Reactivate TFA if it was set to "deactivate TFA for next login" - $stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user"); - $stmt->execute(array(':user' => $user)); - $_SESSION['return'][] = array( - 'type' => 'success', - 'log' => array(__FUNCTION__, $user, '*'), - 'msg' => array('logged_in_as', $user) - ); - return "user"; - } - } elseif ($app_passwd_data['eas'] === true || $app_passwd_data['dav'] === true) { - // password is a app password - $service = ($app_passwd_data['eas'] === true) ? 'EAS' : 'DAV'; - $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['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR']) - )); - - unset($_SESSION['ldelay']); - return "user"; - } - } - } - - if (!isset($_SESSION['ldelay'])) { - $_SESSION['ldelay'] = "0"; - $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']); - } - elseif (!isset($_SESSION['mailcow_cc_username'])) { - $_SESSION['ldelay'] = $_SESSION['ldelay']+0.5; - $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']); - } - - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $user, '*'), - 'msg' => 'login_failed' - ); - - sleep($_SESSION['ldelay']); - return false; -} function formatBytes($size, $precision = 2) { if(!is_numeric($size)) { return "0"; diff --git a/data/web/inc/functions.mailbox.inc.php b/data/web/inc/functions.mailbox.inc.php index 0f48efbd5..a38094022 100644 --- a/data/web/inc/functions.mailbox.inc.php +++ b/data/web/inc/functions.mailbox.inc.php @@ -1002,6 +1002,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $local_part = strtolower(trim($_data['local_part'])); $domain = idn_to_ascii(strtolower(trim($_data['domain'])), 0, INTL_IDNA_VARIANT_UTS46); $username = $local_part . '@' . $domain; + $authsource = 'mailcow'; if (!filter_var($username, FILTER_VALIDATE_EMAIL)) { $_SESSION['return'][] = array( 'type' => 'danger', @@ -1018,6 +1019,9 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ); return false; } + if (in_array($_data['authsource'], array('mailcow', 'keycloak'))){ + $authsource = $_data['authsource']; + } if (empty($name)) { $name = $local_part; } @@ -1035,6 +1039,11 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $name = ltrim(rtrim($_data['name'], '>'), '<'); $tags = (isset($_data['tags'])) ? $_data['tags'] : $MAILBOX_DEFAULT_ATTRIBUTES['tags']; $quota_m = (isset($_data['quota'])) ? intval($_data['quota']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['quota']) / 1024 ** 2; + if ($authsource != 'mailcow'){ + $password = ''; + $password2 = ''; + $password_hashed = ''; + } if ((!isset($_SESSION['acl']['unlimited_quota']) || $_SESSION['acl']['unlimited_quota'] != "1") && $quota_m === 0) { $_SESSION['return'][] = array( 'type' => 'danger', @@ -1153,10 +1162,12 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ); return false; } - if (password_check($password, $password2) !== true) { - return false; + if ($authsource == 'mailcow'){ + if (password_check($password, $password2) !== true) { + return false; + } + $password_hashed = hash_password($password); } - $password_hashed = hash_password($password); if ($MailboxData['count'] >= $DomainData['mailboxes']) { $_SESSION['return'][] = array( 'type' => 'danger', @@ -1182,8 +1193,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ); return false; } - $stmt = $pdo->prepare("INSERT INTO `mailbox` (`username`, `password`, `name`, `quota`, `local_part`, `domain`, `attributes`, `active`) - VALUES (:username, :password_hashed, :name, :quota_b, :local_part, :domain, :mailbox_attrs, :active)"); + $stmt = $pdo->prepare("INSERT INTO `mailbox` (`username`, `password`, `name`, `quota`, `local_part`, `domain`, `attributes`, `authsource`, `active`) + VALUES (:username, :password_hashed, :name, :quota_b, :local_part, :domain, :mailbox_attrs, :authsource, :active)"); $stmt->execute(array( ':username' => $username, ':password_hashed' => $password_hashed, @@ -1192,6 +1203,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ':local_part' => $local_part, ':domain' => $domain, ':mailbox_attrs' => $mailbox_attrs, + ':authsource' => $authsource, ':active' => $active )); $stmt = $pdo->prepare("UPDATE `mailbox` SET @@ -4422,6 +4434,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { `mailbox`.`quota`, `mailbox`.`created`, `mailbox`.`modified`, + `mailbox`.`authsource`, `quota2`.`bytes`, `attributes`, `custom_attributes`, @@ -4443,6 +4456,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { `mailbox`.`quota`, `mailbox`.`created`, `mailbox`.`modified`, + `mailbox`.`authsource`, `quota2replica`.`bytes`, `attributes`, `custom_attributes`, @@ -4472,6 +4486,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $mailboxdata['percent_in_use'] = ($row['quota'] == 0) ? '- ' : round((intval($row['bytes']) / intval($row['quota'])) * 100); $mailboxdata['created'] = $row['created']; $mailboxdata['modified'] = $row['modified']; + $mailboxdata['authsource'] = ($row['authsource']) ? $row['authsource'] : 'mailcow'; if ($mailboxdata['percent_in_use'] === '- ') { $mailboxdata['percent_class'] = "info"; diff --git a/data/web/inc/init_db.inc.php b/data/web/inc/init_db.inc.php index 6fec0c2f8..f7bd86d2d 100644 --- a/data/web/inc/init_db.inc.php +++ b/data/web/inc/init_db.inc.php @@ -362,6 +362,7 @@ function init_db_schema() { "custom_attributes" => "JSON NOT NULL DEFAULT ('{}')", "kind" => "VARCHAR(100) NOT NULL DEFAULT ''", "multiple_bookings" => "INT NOT NULL DEFAULT -1", + "authsource" => "ENUM('mailcow', 'keycloak') DEFAULT 'mailcow'", "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP", "active" => "TINYINT(1) NOT NULL DEFAULT '1'" diff --git a/data/web/inc/prerequisites.inc.php b/data/web/inc/prerequisites.inc.php index 9c5203e7f..1a18ba92c 100644 --- a/data/web/inc/prerequisites.inc.php +++ b/data/web/inc/prerequisites.inc.php @@ -177,6 +177,7 @@ function get_remote_ip() { // Load core functions first require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.inc.php'; +require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.auth.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/sessions.inc.php'; // IMAP lib diff --git a/data/web/json_api.php b/data/web/json_api.php index 28f8cac56..0b80bafd9 100644 --- a/data/web/json_api.php +++ b/data/web/json_api.php @@ -401,6 +401,26 @@ if (isset($_GET['query'])) { ); echo json_encode($return); break; + case "login": + header('Content-Type: application/json'); + $post = trim(file_get_contents('php://input')); + if ($post) { + $post = json_decode($post, true); + } + + $return = array("success" => false, "role" => false); + if(!isset($post['username']) || !isset($post['password'])){ + echo json_encode($return); + return; + } + $result = check_login($post['username'], $post['password'], $post['protocol']); + if ($result) { + $return = array("success" => true, "role" => $result); + } + + echo json_encode($return); + return; + break; } break; case "get": diff --git a/data/web/sogo-auth.php b/data/web/sogo-auth.php index c34a60def..af6f851ba 100644 --- a/data/web/sogo-auth.php +++ b/data/web/sogo-auth.php @@ -11,7 +11,8 @@ $session_var_pass = 'sogo-sso-pass'; // validate credentials for basic auth requests if (isset($_SERVER['PHP_AUTH_USER'])) { // load prerequisites only when required - require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php'; + require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.inc.php'; + require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.auth.inc.php'; $username = $_SERVER['PHP_AUTH_USER']; $password = $_SERVER['PHP_AUTH_PW']; $is_eas = false; diff --git a/data/web/templates/edit/mailbox.twig b/data/web/templates/edit/mailbox.twig index 8960ee938..52b4096cc 100644 --- a/data/web/templates/edit/mailbox.twig +++ b/data/web/templates/edit/mailbox.twig @@ -24,7 +24,13 @@ - + +
+ +
+ {{ result.authsource }} +
+
diff --git a/data/web/templates/modals/mailbox.twig b/data/web/templates/modals/mailbox.twig index 22807c8d3..5d6ea535b 100644 --- a/data/web/templates/modals/mailbox.twig +++ b/data/web/templates/modals/mailbox.twig @@ -28,6 +28,15 @@
+
+ +
+ +
+