From 638a81c58cb227817db402daa4d43248859e8e88 Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Sun, 12 Mar 2023 19:06:03 +0100 Subject: [PATCH] [Web] organize auth functions+api auth w/ dovecot --- data/Dockerfiles/dovecot/Dockerfile | 1 + data/Dockerfiles/dovecot/docker-entrypoint.sh | 150 ++++------ data/web/inc/functions.auth.inc.php | 278 ++++++++++++++++++ data/web/inc/functions.inc.php | 198 ------------- data/web/inc/functions.mailbox.inc.php | 25 +- data/web/inc/init_db.inc.php | 215 +++++++------- data/web/inc/prerequisites.inc.php | 1 + data/web/json_api.php | 44 ++- data/web/sogo-auth.php | 3 +- data/web/templates/edit/mailbox.twig | 6 + data/web/templates/modals/mailbox.twig | 9 + 11 files changed, 518 insertions(+), 412 deletions(-) create mode 100644 data/web/inc/functions.auth.inc.php diff --git a/data/Dockerfiles/dovecot/Dockerfile b/data/Dockerfiles/dovecot/Dockerfile index 1d8e1e5b..d3771470 100644 --- a/data/Dockerfiles/dovecot/Dockerfile +++ b/data/Dockerfiles/dovecot/Dockerfile @@ -78,6 +78,7 @@ RUN groupadd -g 5000 vmail \ libwww-perl \ lua-sql-mysql \ lua-socket \ + lua-json \ mariadb-client \ procps \ python3-pip \ diff --git a/data/Dockerfiles/dovecot/docker-entrypoint.sh b/data/Dockerfiles/dovecot/docker-entrypoint.sh index 18746deb..c1d402b1 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, "password=" .. pass - 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, "password=" .. pass - 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, "password=" .. pass - 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 00000000..3f4f2f3b --- /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 928b964d..635ef57f 100644 --- a/data/web/inc/functions.inc.php +++ b/data/web/inc/functions.inc.php @@ -810,204 +810,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 4529ee7b..1e0ca2c1 100644 --- a/data/web/inc/functions.mailbox.inc.php +++ b/data/web/inc/functions.mailbox.inc.php @@ -988,6 +988,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', @@ -1004,11 +1005,19 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ); return false; } + if (in_array($_data['authsource'], array('mailcow', 'keycloak'))){ + $authsource = $_data['authsource']; + } $password = $_data['password']; $password2 = $_data['password2']; $name = ltrim(rtrim($_data['name'], '>'), '<'); $tags = $_data['tags']; $quota_m = intval($_data['quota']); + 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', @@ -1129,10 +1138,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', @@ -1158,8 +1169,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, @@ -1168,6 +1179,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 @@ -4190,6 +4202,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { `mailbox`.`quota`, `mailbox`.`created`, `mailbox`.`modified`, + `mailbox`.`authsource`, `quota2`.`bytes`, `attributes`, `quota2`.`messages` @@ -4210,6 +4223,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { `mailbox`.`quota`, `mailbox`.`created`, `mailbox`.`modified`, + `mailbox`.`authsource`, `quota2replica`.`bytes`, `attributes`, `quota2replica`.`messages` @@ -4238,6 +4252,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 9fc9234e..6db30d60 100644 --- a/data/web/inc/init_db.inc.php +++ b/data/web/inc/init_db.inc.php @@ -3,7 +3,7 @@ function init_db_schema() { try { global $pdo; - $db_version = "06012023_1924"; + $db_version = "12032023_1705"; $stmt = $pdo->query("SHOW TABLES LIKE 'versions'"); $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); @@ -225,22 +225,22 @@ function init_db_schema() { ), "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" ), - "templates" => array( - "cols" => array( - "id" => "INT NOT NULL AUTO_INCREMENT", - "template" => "VARCHAR(255) NOT NULL", - "type" => "VARCHAR(255) NOT NULL", - "attributes" => "JSON", - "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", - "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP" - ), - "keys" => array( - "primary" => array( - "" => array("id") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), + "templates" => array( + "cols" => array( + "id" => "INT NOT NULL AUTO_INCREMENT", + "template" => "VARCHAR(255) NOT NULL", + "type" => "VARCHAR(255) NOT NULL", + "attributes" => "JSON", + "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", + "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP" + ), + "keys" => array( + "primary" => array( + "" => array("id") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), "domain" => array( // Todo: Move some attributes to json "cols" => array( @@ -346,6 +346,7 @@ function init_db_schema() { "attributes" => "JSON", "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'" @@ -1076,7 +1077,7 @@ function init_db_schema() { } } - // Migrate tls_enforce_* options + // Migrate tls_enforce_* options if ($table == 'mailbox') { $stmt = $pdo->query("SHOW TABLES LIKE 'mailbox'"); $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); @@ -1322,95 +1323,95 @@ function init_db_schema() { // Fix domain_admins $pdo->query("DELETE FROM `domain_admins` WHERE `domain` = 'ALL';"); - // add default templates - $default_domain_template = array( - "template" => "Default", - "type" => "domain", - "attributes" => array( - "tags" => array(), - "max_num_aliases_for_domain" => 400, - "max_num_mboxes_for_domain" => 10, - "def_quota_for_mbox" => 3072 * 1048576, - "max_quota_for_mbox" => 10240 * 1048576, - "max_quota_for_domain" => 10240 * 1048576, - "rl_frame" => "s", - "rl_value" => "", - "active" => 1, - "gal" => 1, - "backupmx" => 0, - "relay_all_recipients" => 0, - "relay_unknown_only" => 0, - "dkim_selector" => "dkim", - "key_size" => 2048, - "max_quota_for_domain" => 10240 * 1048576, - ) - ); - $default_mailbox_template = array( - "template" => "Default", - "type" => "mailbox", - "attributes" => array( - "tags" => array(), - "quota" => 0, - "quarantine_notification" => strval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['quarantine_notification']), - "quarantine_category" => strval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['quarantine_category']), - "rl_frame" => "s", - "rl_value" => "", - "force_pw_update" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['force_pw_update']), - "sogo_access" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['sogo_access']), - "active" => 1, - "tls_enforce_in" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['tls_enforce_in']), - "tls_enforce_out" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['tls_enforce_out']), - "imap_access" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['imap_access']), - "pop3_access" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['pop3_access']), - "smtp_access" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['smtp_access']), - "sieve_access" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['sieve_access']), - "acl_spam_alias" => 1, - "acl_tls_policy" => 1, - "acl_spam_score" => 1, - "acl_spam_policy" => 1, - "acl_delimiter_action" => 1, - "acl_syncjobs" => 0, - "acl_eas_reset" => 1, - "acl_sogo_profile_reset" => 0, - "acl_pushover" => 1, - "acl_quarantine" => 1, - "acl_quarantine_attachments" => 1, - "acl_quarantine_notification" => 1, - "acl_quarantine_category" => 1, - "acl_app_passwds" => 1, - ) - ); - $stmt = $pdo->prepare("SELECT id FROM `templates` WHERE `type` = :type AND `template` = :template"); - $stmt->execute(array( - ":type" => "domain", - ":template" => $default_domain_template["template"] - )); - $row = $stmt->fetch(PDO::FETCH_ASSOC); - if (empty($row)){ - $stmt = $pdo->prepare("INSERT INTO `templates` (`type`, `template`, `attributes`) - VALUES (:type, :template, :attributes)"); - $stmt->execute(array( - ":type" => "domain", - ":template" => $default_domain_template["template"], - ":attributes" => json_encode($default_domain_template["attributes"]) - )); - } - $stmt = $pdo->prepare("SELECT id FROM `templates` WHERE `type` = :type AND `template` = :template"); - $stmt->execute(array( - ":type" => "mailbox", - ":template" => $default_mailbox_template["template"] - )); - $row = $stmt->fetch(PDO::FETCH_ASSOC); - if (empty($row)){ - $stmt = $pdo->prepare("INSERT INTO `templates` (`type`, `template`, `attributes`) - VALUES (:type, :template, :attributes)"); - $stmt->execute(array( - ":type" => "mailbox", - ":template" => $default_mailbox_template["template"], - ":attributes" => json_encode($default_mailbox_template["attributes"]) - )); - } - + // add default templates + $default_domain_template = array( + "template" => "Default", + "type" => "domain", + "attributes" => array( + "tags" => array(), + "max_num_aliases_for_domain" => 400, + "max_num_mboxes_for_domain" => 10, + "def_quota_for_mbox" => 3072 * 1048576, + "max_quota_for_mbox" => 10240 * 1048576, + "max_quota_for_domain" => 10240 * 1048576, + "rl_frame" => "s", + "rl_value" => "", + "active" => 1, + "gal" => 1, + "backupmx" => 0, + "relay_all_recipients" => 0, + "relay_unknown_only" => 0, + "dkim_selector" => "dkim", + "key_size" => 2048, + "max_quota_for_domain" => 10240 * 1048576, + ) + ); + $default_mailbox_template = array( + "template" => "Default", + "type" => "mailbox", + "attributes" => array( + "tags" => array(), + "quota" => 0, + "quarantine_notification" => strval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['quarantine_notification']), + "quarantine_category" => strval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['quarantine_category']), + "rl_frame" => "s", + "rl_value" => "", + "force_pw_update" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['force_pw_update']), + "sogo_access" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['sogo_access']), + "active" => 1, + "tls_enforce_in" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['tls_enforce_in']), + "tls_enforce_out" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['tls_enforce_out']), + "imap_access" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['imap_access']), + "pop3_access" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['pop3_access']), + "smtp_access" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['smtp_access']), + "sieve_access" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['sieve_access']), + "acl_spam_alias" => 1, + "acl_tls_policy" => 1, + "acl_spam_score" => 1, + "acl_spam_policy" => 1, + "acl_delimiter_action" => 1, + "acl_syncjobs" => 0, + "acl_eas_reset" => 1, + "acl_sogo_profile_reset" => 0, + "acl_pushover" => 1, + "acl_quarantine" => 1, + "acl_quarantine_attachments" => 1, + "acl_quarantine_notification" => 1, + "acl_quarantine_category" => 1, + "acl_app_passwds" => 1, + ) + ); + $stmt = $pdo->prepare("SELECT id FROM `templates` WHERE `type` = :type AND `template` = :template"); + $stmt->execute(array( + ":type" => "domain", + ":template" => $default_domain_template["template"] + )); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + if (empty($row)){ + $stmt = $pdo->prepare("INSERT INTO `templates` (`type`, `template`, `attributes`) + VALUES (:type, :template, :attributes)"); + $stmt->execute(array( + ":type" => "domain", + ":template" => $default_domain_template["template"], + ":attributes" => json_encode($default_domain_template["attributes"]) + )); + } + $stmt = $pdo->prepare("SELECT id FROM `templates` WHERE `type` = :type AND `template` = :template"); + $stmt->execute(array( + ":type" => "mailbox", + ":template" => $default_mailbox_template["template"] + )); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + if (empty($row)){ + $stmt = $pdo->prepare("INSERT INTO `templates` (`type`, `template`, `attributes`) + VALUES (:type, :template, :attributes)"); + $stmt->execute(array( + ":type" => "mailbox", + ":template" => $default_mailbox_template["template"], + ":attributes" => json_encode($default_mailbox_template["attributes"]) + )); + } + if (php_sapi_name() == "cli") { echo "DB initialization completed" . PHP_EOL; } else { diff --git a/data/web/inc/prerequisites.inc.php b/data/web/inc/prerequisites.inc.php index b3b1cc13..45412586 100644 --- a/data/web/inc/prerequisites.inc.php +++ b/data/web/inc/prerequisites.inc.php @@ -173,6 +173,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 ec028fe4..19221f19 100644 --- a/data/web/json_api.php +++ b/data/web/json_api.php @@ -288,18 +288,18 @@ if (isset($_GET['query'])) { case "domain-admin": process_add_return(domain_admin('add', $attr)); break; - case "sso": - switch ($object) { - case "domain-admin": - $data = domain_admin_sso('issue', $attr); - if($data) { - echo json_encode($data); - exit(0); - } - process_add_return($data); - break; - } - break; + case "sso": + switch ($object) { + case "domain-admin": + $data = domain_admin_sso('issue', $attr); + if($data) { + echo json_encode($data); + exit(0); + } + process_add_return($data); + break; + } + break; case "admin": process_add_return(admin('add', $attr)); break; @@ -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 68d8b7ba..1a6a7dd3 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 36fe053b..368d881f 100644 --- a/data/web/templates/edit/mailbox.twig +++ b/data/web/templates/edit/mailbox.twig @@ -16,6 +16,12 @@ +
+ +
+ {{ result.authsource }} +
+
diff --git a/data/web/templates/modals/mailbox.twig b/data/web/templates/modals/mailbox.twig index 25185de0..ea793c69 100644 --- a/data/web/templates/modals/mailbox.twig +++ b/data/web/templates/modals/mailbox.twig @@ -28,6 +28,15 @@
+
+ +
+ +
+