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 \ curl \
jq \ jq \
lua \ lua \
lua-json \
lua-cjson \ lua-cjson \
lua-socket \ lua-socket \
lua-sql-mysql \ lua-sql-mysql \

View File

@ -129,114 +129,86 @@ iterate_query = SELECT username FROM mailbox WHERE active = '1' OR active = '2';
EOF EOF
cat <<EOF > /etc/dovecot/lua/passwd-verify.lua cat <<EOF > /etc/dovecot/lua/passwd-verify.lua
function auth_password_verify(req, pass) function auth_password_verify(request, password)
if request.domain == nil then
if req.domain == nil then
return dovecot.auth.PASSDB_RESULT_USER_UNKNOWN, "No such user" return dovecot.auth.PASSDB_RESULT_USER_UNKNOWN, "No such user"
end end
if cur == nil then json = require "json"
script_init() ltn12 = require "ltn12"
end http = require "socket.http"
http.TIMEOUT = 5
if req.user == nil then mysql = require "luasql.mysql"
req.user = '' env = mysql.mysql()
end con = env:connect("__DBNAME__","__DBUSER__","__DBPASS__","localhost")
respbody = {}
local req = {
username = request.user,
password = password
}
local req_json = json.encode(req)
local res = {}
-- check against mailbox passwds -- check against mailbox passwds
local cur,errorString = con:execute(string.format([[SELECT password FROM mailbox local b, c = http.request {
WHERE username = '%s' method = "POST",
AND active = '1' url = "https://nginx/api/v1/process/login",
AND domain IN (SELECT domain FROM domain WHERE domain='%s' AND active='1') source = ltn12.source.string(req_json),
AND IFNULL(JSON_UNQUOTE(JSON_VALUE(mailbox.attributes, '$.force_pw_update')), 0) != '1' headers = {
AND IFNULL(JSON_UNQUOTE(JSON_VALUE(attributes, '$.%s_access')), 1) = '1']], con:escape(req.user), con:escape(req.domain), con:escape(req.service))) ["content-type"] = "application/json",
local row = cur:fetch ({}, "a") ["content-length"] = tostring(#req_json)
while row do },
if req.password_verify(req, row.password, pass) == 1 then sink = ltn12.sink.table(res)
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))) local api_response = json.decode(table.concat(res))
cur:close() if api_response.role == 'user' then
con:close() con:execute(string.format([[REPLACE INTO sasl_log (service, app_password, username, real_rip)
return dovecot.auth.PASSDB_RESULT_OK, "" VALUES ("%s", 0, "%s", "%s")]], con:escape(request.service), con:escape(request.user), con:escape(request.real_rip)))
end con:close()
row = cur:fetch (row, "a") return dovecot.auth.PASSDB_RESULT_OK, "password=" .. password
end end
-- check against app passwds for imap and smtp -- check against app passwds for imap and smtp
-- app passwords are only available for imap, smtp, sieve and pop3 when using sasl -- 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 if request.service == "smtp" or request.service == "imap" or request.service == "sieve" or request.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 req.protocol = {}
INNER JOIN mailbox ON mailbox.username = app_passwd.mailbox req.protocol[request.service] = true
WHERE mailbox = '%s' req_json = json.encode(req)
AND app_passwd.active = '1'
AND mailbox.active = '1' req.protocol.ignore_hasaccess = false
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))) if tostring(req.real_rip) == "__IPV4_SOGO__" then
local row = cur:fetch ({}, "a") req.protocol.ignore_hasaccess = true
while row do end
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 local b, c = http.request {
if tostring(req.real_rip) == "__IPV4_SOGO__" then method = "POST",
cur:close() url = "https://nginx/api/v1/process/login",
con:close() source = ltn12.source.string(req_json),
return dovecot.auth.PASSDB_RESULT_OK, "" headers = {
elseif row.has_prot_access == "1" then ["content-type"] = "application/json",
con:execute(string.format([[REPLACE INTO sasl_log (service, app_password, username, real_rip) ["content-length"] = tostring(#req_json)
VALUES ("%s", %d, "%s", "%s")]], con:escape(req.service), row.id, con:escape(req.user), con:escape(req.real_rip))) },
cur:close() sink = ltn12.sink.table(res)
con:close() }
return dovecot.auth.PASSDB_RESULT_OK, "" local api_response = json.decode(table.concat(res))
end 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 end
row = cur:fetch (row, "a") con:close()
return dovecot.auth.PASSDB_RESULT_OK, "password=" .. password
end end
end end
cur:close()
con:close() con:close()
return dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH, "Failed to authenticate" 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 end
function auth_passdb_lookup(req) function auth_passdb_lookup(req)
return dovecot.auth.PASSDB_RESULT_USER_UNKNOWN, "" return dovecot.auth.PASSDB_RESULT_USER_UNKNOWN, ""
end 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 EOF
# Replace patterns in app-passdb.lua # 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; 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) { function formatBytes($size, $precision = 2) {
if(!is_numeric($size)) { if(!is_numeric($size)) {
return "0"; return "0";

View File

@ -1002,6 +1002,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$local_part = strtolower(trim($_data['local_part'])); $local_part = strtolower(trim($_data['local_part']));
$domain = idn_to_ascii(strtolower(trim($_data['domain'])), 0, INTL_IDNA_VARIANT_UTS46); $domain = idn_to_ascii(strtolower(trim($_data['domain'])), 0, INTL_IDNA_VARIANT_UTS46);
$username = $local_part . '@' . $domain; $username = $local_part . '@' . $domain;
$authsource = 'mailcow';
if (!filter_var($username, FILTER_VALIDATE_EMAIL)) { if (!filter_var($username, FILTER_VALIDATE_EMAIL)) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
@ -1018,6 +1019,9 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
); );
return false; return false;
} }
if (in_array($_data['authsource'], array('mailcow', 'keycloak'))){
$authsource = $_data['authsource'];
}
if (empty($name)) { if (empty($name)) {
$name = $local_part; $name = $local_part;
} }
@ -1035,6 +1039,11 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$name = ltrim(rtrim($_data['name'], '>'), '<'); $name = ltrim(rtrim($_data['name'], '>'), '<');
$tags = (isset($_data['tags'])) ? $_data['tags'] : $MAILBOX_DEFAULT_ATTRIBUTES['tags']; $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; $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) { if ((!isset($_SESSION['acl']['unlimited_quota']) || $_SESSION['acl']['unlimited_quota'] != "1") && $quota_m === 0) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
@ -1153,10 +1162,12 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
); );
return false; return false;
} }
if (password_check($password, $password2) !== true) { if ($authsource == 'mailcow'){
return false; if (password_check($password, $password2) !== true) {
return false;
}
$password_hashed = hash_password($password);
} }
$password_hashed = hash_password($password);
if ($MailboxData['count'] >= $DomainData['mailboxes']) { if ($MailboxData['count'] >= $DomainData['mailboxes']) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
@ -1182,8 +1193,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
); );
return false; return false;
} }
$stmt = $pdo->prepare("INSERT INTO `mailbox` (`username`, `password`, `name`, `quota`, `local_part`, `domain`, `attributes`, `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, :active)"); VALUES (:username, :password_hashed, :name, :quota_b, :local_part, :domain, :mailbox_attrs, :authsource, :active)");
$stmt->execute(array( $stmt->execute(array(
':username' => $username, ':username' => $username,
':password_hashed' => $password_hashed, ':password_hashed' => $password_hashed,
@ -1192,6 +1203,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
':local_part' => $local_part, ':local_part' => $local_part,
':domain' => $domain, ':domain' => $domain,
':mailbox_attrs' => $mailbox_attrs, ':mailbox_attrs' => $mailbox_attrs,
':authsource' => $authsource,
':active' => $active ':active' => $active
)); ));
$stmt = $pdo->prepare("UPDATE `mailbox` SET $stmt = $pdo->prepare("UPDATE `mailbox` SET
@ -4422,6 +4434,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
`mailbox`.`quota`, `mailbox`.`quota`,
`mailbox`.`created`, `mailbox`.`created`,
`mailbox`.`modified`, `mailbox`.`modified`,
`mailbox`.`authsource`,
`quota2`.`bytes`, `quota2`.`bytes`,
`attributes`, `attributes`,
`custom_attributes`, `custom_attributes`,
@ -4443,6 +4456,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
`mailbox`.`quota`, `mailbox`.`quota`,
`mailbox`.`created`, `mailbox`.`created`,
`mailbox`.`modified`, `mailbox`.`modified`,
`mailbox`.`authsource`,
`quota2replica`.`bytes`, `quota2replica`.`bytes`,
`attributes`, `attributes`,
`custom_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['percent_in_use'] = ($row['quota'] == 0) ? '- ' : round((intval($row['bytes']) / intval($row['quota'])) * 100);
$mailboxdata['created'] = $row['created']; $mailboxdata['created'] = $row['created'];
$mailboxdata['modified'] = $row['modified']; $mailboxdata['modified'] = $row['modified'];
$mailboxdata['authsource'] = ($row['authsource']) ? $row['authsource'] : 'mailcow';
if ($mailboxdata['percent_in_use'] === '- ') { if ($mailboxdata['percent_in_use'] === '- ') {
$mailboxdata['percent_class'] = "info"; $mailboxdata['percent_class'] = "info";

View File

@ -362,6 +362,7 @@ function init_db_schema() {
"custom_attributes" => "JSON NOT NULL DEFAULT ('{}')", "custom_attributes" => "JSON NOT NULL DEFAULT ('{}')",
"kind" => "VARCHAR(100) NOT NULL DEFAULT ''", "kind" => "VARCHAR(100) NOT NULL DEFAULT ''",
"multiple_bookings" => "INT NOT NULL DEFAULT -1", "multiple_bookings" => "INT NOT NULL DEFAULT -1",
"authsource" => "ENUM('mailcow', 'keycloak') DEFAULT 'mailcow'",
"created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
"modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP", "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP",
"active" => "TINYINT(1) NOT NULL DEFAULT '1'" "active" => "TINYINT(1) NOT NULL DEFAULT '1'"

View File

@ -177,6 +177,7 @@ function get_remote_ip() {
// Load core functions first // Load core functions first
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.inc.php'; 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'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/sessions.inc.php';
// IMAP lib // IMAP lib

View File

@ -401,6 +401,26 @@ if (isset($_GET['query'])) {
); );
echo json_encode($return); echo json_encode($return);
break; 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; break;
case "get": case "get":

View File

@ -11,7 +11,8 @@ $session_var_pass = 'sogo-sso-pass';
// validate credentials for basic auth requests // validate credentials for basic auth requests
if (isset($_SERVER['PHP_AUTH_USER'])) { if (isset($_SERVER['PHP_AUTH_USER'])) {
// load prerequisites only when required // 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']; $username = $_SERVER['PHP_AUTH_USER'];
$password = $_SERVER['PHP_AUTH_PW']; $password = $_SERVER['PHP_AUTH_PW'];
$is_eas = false; $is_eas = false;

View File

@ -24,7 +24,13 @@
<input type="hidden" value="default" name="sender_acl"> <input type="hidden" value="default" name="sender_acl">
<input type="hidden" value="0" name="force_pw_update"> <input type="hidden" value="0" name="force_pw_update">
<input type="hidden" value="0" name="sogo_access"> <input type="hidden" value="0" name="sogo_access">
<input type="hidden" value="0" name="protocol_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"> <div class="row mb-2">
<label class="control-label col-sm-2" for="name">{{ lang.edit.full_name }}</label> <label class="control-label col-sm-2" for="name">{{ lang.edit.full_name }}</label>
<div class="col-sm-10"> <div class="col-sm-10">

View File

@ -28,6 +28,15 @@
</select> </select>
</div> </div>
</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"> <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> <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"> <div class="col-sm-10">