1
0
mirror of https://github.com/mailcow/mailcow-dockerized.git synced 2025-07-07 01:06:59 +02:00

[Web] organize auth functions+api auth w/ dovecot

This commit is contained in:
FreddleSpl0it
2023-03-12 19:06:03 +01:00
committed by DerLinkman
parent 50d4d59626
commit 6adad79e5c
11 changed files with 400 additions and 294 deletions

View File

@ -26,6 +26,7 @@ RUN addgroup -g 5000 vmail \
curl \
jq \
lua \
lua-json \
lua-cjson \
lua-socket \
lua-sql-mysql \

View File

@ -129,114 +129,86 @@ iterate_query = SELECT username FROM mailbox WHERE active = '1' OR active = '2';
EOF
cat <<EOF > /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
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")
if req.user == nil then
req.user = ''
end
respbody = {}
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
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(req.service), con:escape(req.user), con:escape(req.real_rip)))
cur:close()
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, ""
end
row = cur:fetch (row, "a")
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 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
cur:close()
con:close()
return dovecot.auth.PASSDB_RESULT_OK, ""
elseif row.has_prot_access == "1" 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)))
cur:close()
end
con:close()
return dovecot.auth.PASSDB_RESULT_OK, ""
end
end
row = cur:fetch (row, "a")
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

View File

@ -0,0 +1,278 @@
<?php
function check_login($user, $pass, $app_passwd_data = false) {
global $pdo;
global $redis;
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
$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;
}

View File

@ -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";

View File

@ -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 ($authsource == 'mailcow'){
if (password_check($password, $password2) !== true) {
return false;
}
$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";

View File

@ -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'"

View File

@ -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

View File

@ -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":

View File

@ -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;

View File

@ -25,6 +25,12 @@
<input type="hidden" value="0" name="force_pw_update">
<input type="hidden" value="0" name="sogo_access">
<input type="hidden" value="0" name="protocol_access">
<div class="row mb-2">
<label class="control-label col-sm-2">{{ lang.edit.full_name }}</label>
<div class="col-sm-10">
<span>{{ result.authsource }}</span>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2" for="name">{{ lang.edit.full_name }}</label>
<div class="col-sm-10">

View File

@ -28,6 +28,15 @@
</select>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2 text-sm-end text-sm-end" for="authsource">{{ lang.add.domain }}</label>
<div class="col-sm-10">
<select class="full-width-select" data-live-search="true" id="addAuthsource" name="authsource" required>
<option selected>mailcow</option>
<option>keycloak</option>
</select>
</div>
</div>
<div class="row mb-4">
<label class="control-label col-sm-2 text-sm-end text-sm-end" for="name">{{ lang.add.full_name }}</label>
<div class="col-sm-10">