From 1e6a032d1e38149ace8ff922b70ca55496eb9801 Mon Sep 17 00:00:00 2001 From: heavygale Date: Sun, 29 Sep 2019 17:45:36 +0200 Subject: [PATCH 01/11] displaying assigned domain quota adding the amount of assigned domain quota to the disk usage display as potentially used disk space --- data/web/css/site/debug.css | 12 +++++++++++- data/web/debug.php | 10 ++++++++-- data/web/inc/functions.domain_admin.inc.php | 21 +++++++++++++++++---- data/web/lang/lang.de.php | 15 +++++++++++---- data/web/lang/lang.en.php | 5 ++++- 5 files changed, 51 insertions(+), 12 deletions(-) diff --git a/data/web/css/site/debug.css b/data/web/css/site/debug.css index 89eaf41a..9b45f028 100644 --- a/data/web/css/site/debug.css +++ b/data/web/css/site/debug.css @@ -44,4 +44,14 @@ tbody { .container-indicator { width: 15px; height: 15px; -} \ No newline at end of file +} +.disk_space { + margin-top: 20px; +} +.progress-bar-committed { + background-color: #B6E1F2; +} +.usage-info { + display: inline-block; + margin-right: 5px; +} diff --git a/data/web/debug.php b/data/web/debug.php index 67417bc2..09bd44b5 100644 --- a/data/web/debug.php +++ b/data/web/debug.php @@ -43,6 +43,10 @@ else { 'system', 'task' => 'df', 'dir' => '/var/vmail'); $vmail_df = explode(',', json_decode(docker('post', 'dovecot-mailcow', 'exec', $exec_fields), true)); + $domainQuota = round(domain_admin('total_quota')/1024); + $quotaPercent1 = round(($domainQuota/substr($vmail_df[3], 0, -1))*100); + $quotaPercent2 = round((($domainQuota-substr($vmail_df[2], 0, -1))/substr($vmail_df[3], 0, -1))*100); + $quotaPercent2 = ($quotaPercent2+substr($vmail_df[4], 0, -1)>100) ? 100-substr($vmail_df[4], 0, -1) : $quotaPercent2; //handling overcommitment ?>
@@ -53,12 +57,15 @@ else {

/var/vmail on

-

/ ()

+

B

+
+

  B ()

+

  GB (%)

@@ -344,7 +351,6 @@ $lang_admin = json_encode($lang['admin']); echo "var lang = ". $lang_admin . ";\n"; echo "var csrf_token = '". $_SESSION['CSRF']['TOKEN'] . "';\n"; echo "var log_pagination_size = '". $LOG_PAGINATION_SIZE . "';\n"; - ?> execute(array(':username' => $username)); $num_results[] = count($stmt->fetchAll(PDO::FETCH_ASSOC)); - + $stmt = $pdo->prepare("SELECT `username` FROM `admin` WHERE `username` = :username"); $stmt->execute(array(':username' => $username)); $num_results[] = count($stmt->fetchAll(PDO::FETCH_ASSOC)); - + $stmt = $pdo->prepare("SELECT `username` FROM `domain_admins` WHERE `username` = :username"); $stmt->execute(array(':username' => $username)); @@ -373,7 +373,7 @@ function domain_admin($_action, $_data = null) { } $stmt = $pdo->query("SELECT DISTINCT `username` - FROM `domain_admins` + FROM `domain_admins` WHERE `username` IN ( SELECT `username` FROM `admin` WHERE `superadmin`!='1' @@ -409,7 +409,7 @@ function domain_admin($_action, $_data = null) { ':domain_admin' => $_data )); $row = $stmt->fetch(PDO::FETCH_ASSOC); - if (empty($row)) { + if (empty($row)) { return false; } $domainadmindata['username'] = $row['username']; @@ -444,5 +444,18 @@ function domain_admin($_action, $_data = null) { return $domainadmindata; break; + case 'total_quota': + if ($_SESSION['mailcow_cc_role'] != "admin") { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_data_log), + 'msg' => 'access_denied' + ); + return false; + } + $stmt = $pdo->query("SELECT SUM(`quota`) AS `quota` FROM `domain`"); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + return $row['quota']; + break; } } diff --git a/data/web/lang/lang.de.php b/data/web/lang/lang.de.php index 1f671b3d..e0f859a1 100644 --- a/data/web/lang/lang.de.php +++ b/data/web/lang/lang.de.php @@ -150,7 +150,7 @@ $lang['success']['resource_removed'] = 'Ressource %s wurde entfernt'; $lang['warning']['cannot_delete_self'] = 'Kann derzeit eingeloggten Benutzer nicht entfernen'; $lang['warning']['no_active_admin'] = 'Kann letzten aktiven Administrator nicht deaktivieren'; $lang['danger']['max_quota_in_use'] = 'Mailbox Speicherplatzlimit muss größer oder gleich %d MiB sein'; -$lang['danger']['domain_quota_m_in_use'] = 'Domain Speicherplatzlimit muss größer oder gleich %d MiB sein'; +$lang['danger']['domain_quota_m_in_use'] = 'Domain-Speicherplatzlimit muss größer oder gleich %d MiB sein'; $lang['danger']['mailboxes_in_use'] = 'Maximale Anzahl an Mailboxen muss größer oder gleich %d sein'; $lang['danger']['aliases_in_use'] = 'Maximale Anzahl an Aliassen muss größer oder gleich %d sein'; $lang['danger']['sender_acl_invalid'] = 'Sender ACL %s ist ungültig'; @@ -375,7 +375,7 @@ $lang['edit']['mailbox'] = 'Mailbox bearbeiten'; $lang['edit']['description'] = 'Beschreibung'; $lang['edit']['max_aliases'] = 'Max. Aliasse'; $lang['edit']['max_quota'] = 'Max. Größe per Mailbox (MiB)'; -$lang['edit']['domain_quota'] = 'Domain Speicherplatz gesamt (MiB)'; +$lang['edit']['domain_quota'] = 'Domain-Speicherplatz gesamt (MiB)'; $lang['edit']['backup_mx_options'] = 'Backup MX Optionen'; $lang['edit']['relay_domain'] = 'Diese Domain relayen'; $lang['edit']['relay_all'] = 'Alle Empfänger-Adressen relayen'; @@ -474,7 +474,7 @@ $lang['add']['description'] = 'Beschreibung'; $lang['add']['max_aliases'] = 'Max. mögliche Aliasse'; $lang['add']['max_mailboxes'] = 'Max. mögliche Mailboxen'; $lang['add']['mailbox_quota_m'] = 'Max. Speicherplatz pro Mailbox (MiB)'; -$lang['add']['domain_quota_m'] = 'Domain Speicherplatz gesamt (MiB)'; +$lang['add']['domain_quota_m'] = 'Domain-Speicherplatz gesamt (MiB)'; $lang['add']['backup_mx_options'] = 'Backup MX Optionen'; $lang['add']['relay_all'] = 'Alle Empfänger-Adressen relayen'; $lang['add']['relay_domain'] = 'Relay Domain'; @@ -545,7 +545,7 @@ $lang['tfa']['confirm_totp_token'] = "Bitte bestätigen Sie die Änderung durch $lang['admin']['rspamd-com_settings'] = 'Rspamd docs - Ein Name wird automatisch generiert. Beispielinhalte zur Einsicht stehen nachstehend bereit.'; - + $lang['admin']['no_new_rows'] = 'Keine weiteren Zeilen vorhanden'; $lang['admin']['additional_rows'] = ' zusätzliche Zeilen geladen'; // parses to 'n additional rows were added' $lang['admin']['private_key'] = 'Private Key'; @@ -791,6 +791,13 @@ $lang['debug']['solr_last_modified'] = 'Zuletzt geändert'; $lang['debug']['solr_size'] = 'Größe'; $lang['debug']['solr_docs'] = 'Dokumente'; +$lang['debug']['disk_usage'] = 'Festplattenauslastung'; +$lang['debug']['containers_info'] = "Container-Informationen"; + +$lang['debug']['disk_space'] = "Verfügbarer Speicherplatz:"; +$lang['debug']['disk_used'] = "Von Mailboxen belegter Speicherplatz:"; +$lang['debug']['total_quota'] = "Zugewiesener Domain-Speicherplatz:"; + $lang['quarantine']['release_body'] = "Die ursprüngliche Nachricht wurde als EML-Datei im Anhang hinterlegt."; $lang['danger']['release_send_failed'] = "Die Nachricht konnte nicht versendet werden: %s"; $lang['quarantine']['release_subject'] = "Potentiell schädliche Nachricht aus Quarantäne: %s"; diff --git a/data/web/lang/lang.en.php b/data/web/lang/lang.en.php index ec0d2019..be7033db 100644 --- a/data/web/lang/lang.en.php +++ b/data/web/lang/lang.en.php @@ -820,6 +820,10 @@ $lang['debug']['disk_usage'] = 'Disk usage'; $lang['debug']['containers_info'] = "Container information"; $lang['debug']['restart_container'] = 'Restart'; +$lang['debug']['disk_space'] = "Available disk space:"; +$lang['debug']['disk_used'] = "Disk space occupied by mailboxes:"; +$lang['debug']['total_quota'] = "Total amount of assigned domain quota:"; + $lang['quarantine']['release_body'] = "We have attached your message as eml file to this message."; $lang['danger']['release_send_failed'] = "Message could not be released: %s"; $lang['quarantine']['release_subject'] = "Potentially damaging quarantine item %s"; @@ -915,4 +919,3 @@ $lang['mailbox']['alias_domain_backupmx'] = 'Alias domain inactive for relay dom $lang['danger']['extra_acl_invalid'] = 'External sender address "%s" is invalid'; $lang['danger']['extra_acl_invalid_domain'] = 'External sender "%s" uses an invalid domain'; - From f01e3e59c58f09bfaf31810fd062241b5271a416 Mon Sep 17 00:00:00 2001 From: heavygale Date: Sun, 29 Sep 2019 19:04:00 +0200 Subject: [PATCH 02/11] don't count assigned domain quota for inactive domains now only summing up the assigned domain quota for active domains, and not subtracting disc base occupied by mailboxes of inactive domains (inactive_bytes) --- data/web/debug.php | 3 +- data/web/inc/functions.domain_admin.inc.php | 2 +- data/web/inc/functions.mailbox.inc.php | 49 +++++++++++++-------- 3 files changed, 34 insertions(+), 20 deletions(-) diff --git a/data/web/debug.php b/data/web/debug.php index 09bd44b5..eaa4b9a9 100644 --- a/data/web/debug.php +++ b/data/web/debug.php @@ -44,8 +44,9 @@ else { $exec_fields = array('cmd' => 'system', 'task' => 'df', 'dir' => '/var/vmail'); $vmail_df = explode(',', json_decode(docker('post', 'dovecot-mailcow', 'exec', $exec_fields), true)); $domainQuota = round(domain_admin('total_quota')/1024); + $inactive_bytes = round(mailbox('get','inactive_bytes')/1024/1024/1024); $quotaPercent1 = round(($domainQuota/substr($vmail_df[3], 0, -1))*100); - $quotaPercent2 = round((($domainQuota-substr($vmail_df[2], 0, -1))/substr($vmail_df[3], 0, -1))*100); + $quotaPercent2 = round((($domainQuota-substr($vmail_df[2], 0, -1)+$inactive_bytes)/substr($vmail_df[3], 0, -1))*100); $quotaPercent2 = ($quotaPercent2+substr($vmail_df[4], 0, -1)>100) ? 100-substr($vmail_df[4], 0, -1) : $quotaPercent2; //handling overcommitment ?>
diff --git a/data/web/inc/functions.domain_admin.inc.php b/data/web/inc/functions.domain_admin.inc.php index 2bd2b807..7fe4b3fa 100644 --- a/data/web/inc/functions.domain_admin.inc.php +++ b/data/web/inc/functions.domain_admin.inc.php @@ -453,7 +453,7 @@ function domain_admin($_action, $_data = null) { ); return false; } - $stmt = $pdo->query("SELECT SUM(`quota`) AS `quota` FROM `domain`"); + $stmt = $pdo->query("SELECT SUM(`quota`) AS `quota` FROM `domain` WHERE `active`=1"); $row = $stmt->fetch(PDO::FETCH_ASSOC); return $row['quota']; break; diff --git a/data/web/inc/functions.mailbox.inc.php b/data/web/inc/functions.mailbox.inc.php index 99fda2d8..0b781a6c 100644 --- a/data/web/inc/functions.mailbox.inc.php +++ b/data/web/inc/functions.mailbox.inc.php @@ -46,7 +46,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $stmt = $pdo->prepare("SELECT `domain` FROM `mailbox` WHERE `username` = :username"); $stmt->execute(array(':username' => $_SESSION['mailcow_cc_username'])); $domain = $stmt->fetch(PDO::FETCH_ASSOC)['domain']; - $validity = strtotime("+".$_data["validity"]." hour"); + $validity = strtotime("+".$_data["validity"]." hour"); $letters = 'abcefghijklmnopqrstuvwxyz1234567890'; $random_name = substr(str_shuffle($letters), 0, 24); $stmt = $pdo->prepare("INSERT INTO `spamalias` (`address`, `goto`, `validity`) VALUES @@ -485,7 +485,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { 'msg' => 'comment_too_long' ); return false; - } + } if (empty($addresses[0])) { $_SESSION['return'][] = array( 'type' => 'danger', @@ -841,7 +841,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { WHERE `domain` = :domain"); $stmt->execute(array(':domain' => $domain)); $DomainData = $stmt->fetch(PDO::FETCH_ASSOC); - $stmt = $pdo->prepare("SELECT + $stmt = $pdo->prepare("SELECT COUNT(*) as count, COALESCE(ROUND(SUM(`quota`)/1048576), 0) as `quota` FROM `mailbox` @@ -945,7 +945,7 @@ 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`) + $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->execute(array( ':username' => $username, @@ -1073,7 +1073,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ); return false; } - $stmt = $pdo->prepare("INSERT INTO `mailbox` (`username`, `password`, `name`, `quota`, `local_part`, `domain`, `active`, `multiple_bookings`, `kind`) + $stmt = $pdo->prepare("INSERT INTO `mailbox` (`username`, `password`, `name`, `quota`, `local_part`, `domain`, `active`, `multiple_bookings`, `kind`) VALUES (:name, 'RESOURCE', :description, 0, :local_part, :domain, :active, :multiple_bookings, :kind)"); $stmt->execute(array( ':name' => $name, @@ -1249,7 +1249,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { 'msg' => 'access_denied' ); continue; - } + } $stmt = $pdo->prepare("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.quarantine_notification', :quarantine_notification) WHERE `username` = :username"); @@ -1360,7 +1360,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { continue; } $validity = round((int)time() + ($_data['validity'] * 3600)); - $stmt = $pdo->prepare("UPDATE `spamalias` SET `validity` = :validity WHERE + $stmt = $pdo->prepare("UPDATE `spamalias` SET `validity` = :validity WHERE `address` = :address"); $stmt->execute(array( ':address' => $address, @@ -1888,7 +1888,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ); continue; } - $stmt = $pdo->prepare("UPDATE `domain` SET + $stmt = $pdo->prepare("UPDATE `domain` SET `description` = :description, `gal` = :gal WHERE `domain` = :domain"); @@ -1928,7 +1928,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { continue; } // todo: should be using api here - $stmt = $pdo->prepare("SELECT + $stmt = $pdo->prepare("SELECT COUNT(*) AS count, MAX(COALESCE(ROUND(`quota`/1048576), 0)) AS `biggest_mailbox`, COALESCE(ROUND(SUM(`quota`)/1048576), 0) AS `quota_all` @@ -2009,7 +2009,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ); continue; } - $stmt = $pdo->prepare("UPDATE `domain` SET + $stmt = $pdo->prepare("UPDATE `domain` SET `relay_all_recipients` = :relay_all_recipients, `backupmx` = :backupmx, `gal` = :gal, @@ -2071,7 +2071,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $domain = $is_now['domain']; $quota_b = $quota_m * 1048576; $password = (!empty($_data['password'])) ? $_data['password'] : null; - $password2 = (!empty($_data['password2'])) ? $_data['password2'] : null; + $password2 = (!empty($_data['password2'])) ? $_data['password2'] : null; } else { $_SESSION['return'][] = array( @@ -2517,14 +2517,14 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { } $stmt = $pdo->prepare("SELECT `domain` FROM `domain` WHERE `domain` NOT IN ( - SELECT REPLACE(`send_as`, '@', '') FROM `sender_acl` + SELECT REPLACE(`send_as`, '@', '') FROM `sender_acl` WHERE `logged_in_as` = :logged_in_as1 AND `external` = '0' AND `send_as` LIKE '@%') UNION SELECT '*' FROM `domain` WHERE '*' NOT IN ( - SELECT `send_as` FROM `sender_acl` + SELECT `send_as` FROM `sender_acl` WHERE `logged_in_as` = :logged_in_as2 AND `external` = '0' )"); @@ -2546,7 +2546,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $stmt = $pdo->prepare("SELECT `address` FROM `alias` WHERE `goto` != :goto AND `address` NOT IN ( - SELECT `send_as` FROM `sender_acl` + SELECT `send_as` FROM `sender_acl` WHERE `logged_in_as` = :logged_in_as AND `external` = '0' AND `send_as` NOT LIKE '@%')"); @@ -3074,11 +3074,11 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { if (!empty($row)) { $_data = $row['target_domain']; } - $stmt = $pdo->prepare("SELECT + $stmt = $pdo->prepare("SELECT `domain`, `description`, `aliases`, - `mailboxes`, + `mailboxes`, `defquota`, `maxquota`, `quota`, @@ -3096,7 +3096,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ':domain' => $_data )); $row = $stmt->fetch(PDO::FETCH_ASSOC); - if (empty($row)) { + if (empty($row)) { return false; } $stmt = $pdo->prepare("SELECT COUNT(*) AS `count`, @@ -3254,6 +3254,19 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { } return $resourcedata; break; + case 'inactive_bytes': + if ($_SESSION['mailcow_cc_role'] != "admin") { + return false; + } + $stmt = $pdo->query("SELECT SUM(`quota2`.`bytes`) AS `bytes` + FROM `mailbox`, `quota2`, `domain` + WHERE `mailbox`.`kind` NOT REGEXP 'location|thing|group' + AND `mailbox`.`username` = `quota2`.`username` + AND `domain`.`domain` = `mailbox`.`domain` + AND `domain`.`active` = 0"); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + return $row['bytes']; + break; } break; case 'delete': @@ -3724,7 +3737,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { curl_setopt($curl, CURLOPT_HTTPHEADER,array('Content-Type: text/xml')); curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); curl_setopt($curl, CURLOPT_POST, 1); - curl_setopt($curl, CURLOPT_POSTFIELDS, 'user:' . $username . ''); + curl_setopt($curl, CURLOPT_POSTFIELDS, 'user:' . $username . ''); curl_setopt($curl, CURLOPT_TIMEOUT, 30); $response = curl_exec($curl); if ($response === false) { From 7c2a5ba37e51863a75039990c22d680871ad2791 Mon Sep 17 00:00:00 2001 From: heavygale Date: Sun, 29 Sep 2019 19:19:06 +0200 Subject: [PATCH 03/11] uniform spelling for quota --- data/web/lang/lang.de.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/web/lang/lang.de.php b/data/web/lang/lang.de.php index 9dd20b56..d0b6d119 100644 --- a/data/web/lang/lang.de.php +++ b/data/web/lang/lang.de.php @@ -149,7 +149,7 @@ $lang['success']['sogo_profile_reset'] = "ActiveSync Gerät des Benutzers %s wur $lang['success']['resource_removed'] = 'Ressource %s wurde entfernt'; $lang['warning']['cannot_delete_self'] = 'Kann derzeit eingeloggten Benutzer nicht entfernen'; $lang['warning']['no_active_admin'] = 'Kann letzten aktiven Administrator nicht deaktivieren'; -$lang['danger']['max_quota_in_use'] = 'Mailbox Speicherplatzlimit muss größer oder gleich %d MiB sein'; +$lang['danger']['max_quota_in_use'] = 'Mailbox-Speicherplatzlimit muss größer oder gleich %d MiB sein'; $lang['danger']['domain_quota_m_in_use'] = 'Domain-Speicherplatzlimit muss größer oder gleich %d MiB sein'; $lang['danger']['mailboxes_in_use'] = 'Maximale Anzahl an Mailboxen muss größer oder gleich %d sein'; $lang['danger']['aliases_in_use'] = 'Maximale Anzahl an Aliassen muss größer oder gleich %d sein'; From ecda554e5353b4b398165a05efdb8eea3ba5eaac Mon Sep 17 00:00:00 2001 From: heavygale Date: Tue, 1 Oct 2019 20:27:06 +0200 Subject: [PATCH 04/11] finalizing disk usage rework - changed dockerapi to not return human readable values for df - all caluclations now in KiB - moved code from domain_admin to mailbox functions - using actual occupied bytes as used quota - all outputs now in *iB using formatBytes() --- data/Dockerfiles/dockerapi/server.py | 22 ++++++++++----------- data/web/debug.php | 18 ++++++++--------- data/web/inc/functions.domain_admin.inc.php | 13 ------------ data/web/inc/functions.mailbox.inc.php | 11 ++++++++--- 4 files changed, 28 insertions(+), 36 deletions(-) diff --git a/data/Dockerfiles/dockerapi/server.py b/data/Dockerfiles/dockerapi/server.py index ede9433c..0c4672df 100644 --- a/data/Dockerfiles/dockerapi/server.py +++ b/data/Dockerfiles/dockerapi/server.py @@ -64,11 +64,11 @@ class container_post(Resource): print("api call: %s, container_id: %s" % (api_call_method_name, container_id)) return api_call_method(container_id) except Exception as e: - print("error - container_post: %s" % str(e)) + print("error - container_post: %s" % str(e)) return jsonify(type='danger', msg=str(e)) else: - return jsonify(type='danger', msg='invalid container id or missing action') + return jsonify(type='danger', msg='invalid container id or missing action') # api call: container_post - post_action: stop @@ -107,7 +107,7 @@ class container_post(Resource): # api call: container_post - post_action: exec - cmd: mailq - task: delete def container_post__exec__mailq__delete(self, container_id): - if 'items' in request.json: + if 'items' in request.json: r = re.compile("^[0-9a-fA-F]+$") filtered_qids = filter(r.match, request.json['items']) if filtered_qids: @@ -121,7 +121,7 @@ class container_post(Resource): # api call: container_post - post_action: exec - cmd: mailq - task: hold def container_post__exec__mailq__hold(self, container_id): - if 'items' in request.json: + if 'items' in request.json: r = re.compile("^[0-9a-fA-F]+$") filtered_qids = filter(r.match, request.json['items']) if filtered_qids: @@ -135,7 +135,7 @@ class container_post(Resource): # api call: container_post - post_action: exec - cmd: mailq - task: unhold def container_post__exec__mailq__unhold(self, container_id): - if 'items' in request.json: + if 'items' in request.json: r = re.compile("^[0-9a-fA-F]+$") filtered_qids = filter(r.match, request.json['items']) if filtered_qids: @@ -149,7 +149,7 @@ class container_post(Resource): # api call: container_post - post_action: exec - cmd: mailq - task: deliver def container_post__exec__mailq__deliver(self, container_id): - if 'items' in request.json: + if 'items' in request.json: r = re.compile("^[0-9a-fA-F]+$") filtered_qids = filter(r.match, request.json['items']) if filtered_qids: @@ -206,7 +206,7 @@ class container_post(Resource): def container_post__exec__system__df(self, container_id): if 'dir' in request.json: for container in docker_client.containers.list(filters={"id": container_id}): - df_return = container.exec_run(["/bin/bash", "-c", "/bin/df -H '" + request.json['dir'].replace("'", "'\\''") + "' | /usr/bin/tail -n1 | /usr/bin/tr -s [:blank:] | /usr/bin/tr ' ' ','"], user='nobody') + df_return = container.exec_run(["/bin/bash", "-c", "/bin/df '" + request.json['dir'].replace("'", "'\\''") + "' | /usr/bin/tail -n1 | /usr/bin/tr -s [:blank:] | /usr/bin/tr ' ' ','"], user='nobody') if df_return.exit_code == 0: return df_return.output.decode('utf-8').rstrip() else: @@ -264,7 +264,7 @@ class container_post(Resource): def container_post__exec__sieve__print(self, container_id): if 'username' in request.json and 'script_name' in request.json: for container in docker_client.containers.list(filters={"id": container_id}): - cmd = ["/bin/bash", "-c", "/usr/bin/doveadm sieve get -u '" + request.json['username'].replace("'", "'\\''") + "' '" + request.json['script_name'].replace("'", "'\\''") + "'"] + cmd = ["/bin/bash", "-c", "/usr/bin/doveadm sieve get -u '" + request.json['username'].replace("'", "'\\''") + "' '" + request.json['script_name'].replace("'", "'\\''") + "'"] sieve_return = container.exec_run(cmd) return exec_run_handler('utf8_text_only', sieve_return) @@ -286,7 +286,7 @@ class container_post(Resource): for container in docker_client.containers.list(filters={"id": container_id}): cmd = "/usr/bin/rspamadm pw -e -p '" + request.json['raw'].replace("'", "'\\''") + "' 2> /dev/null" cmd_response = exec_cmd_container(container, cmd, user="_rspamd") - + matched = False for line in cmd_response.split("\n"): if '$2$' in line: @@ -306,7 +306,7 @@ class container_post(Resource): return jsonify(type='success', msg='command completed successfully') else: return jsonify(type='danger', msg='command did not complete') - + def exec_cmd_container(container, cmd, user, timeout=2, shell_cmd="/bin/bash"): @@ -333,7 +333,7 @@ def exec_cmd_container(container, cmd, user, timeout=2, shell_cmd="/bin/bash"): except: pass return ''.join(total_data) - + try : socket = container.exec_run([shell_cmd], stdin=True, socket=True, user=user).output._sock if not cmd.endswith("\n"): diff --git a/data/web/debug.php b/data/web/debug.php index eaa4b9a9..03752d34 100644 --- a/data/web/debug.php +++ b/data/web/debug.php @@ -43,11 +43,11 @@ else { 'system', 'task' => 'df', 'dir' => '/var/vmail'); $vmail_df = explode(',', json_decode(docker('post', 'dovecot-mailcow', 'exec', $exec_fields), true)); - $domainQuota = round(domain_admin('total_quota')/1024); - $inactive_bytes = round(mailbox('get','inactive_bytes')/1024/1024/1024); - $quotaPercent1 = round(($domainQuota/substr($vmail_df[3], 0, -1))*100); - $quotaPercent2 = round((($domainQuota-substr($vmail_df[2], 0, -1)+$inactive_bytes)/substr($vmail_df[3], 0, -1))*100); - $quotaPercent2 = ($quotaPercent2+substr($vmail_df[4], 0, -1)>100) ? 100-substr($vmail_df[4], 0, -1) : $quotaPercent2; //handling overcommitment + $used_percent = substr($vmail_df[4], 0, -1); + $quota_stats = mailbox('get','quota_stats'); + $quotaPercent1 = round(($quota_stats['total_quota']/$vmail_df[3])*100); + $quotaPercent2 = round((($quota_stats['total_quota']-$quota_stats['used_bytes'])/$vmail_df[3])*100); + $quotaPercent2 = ($quotaPercent2+$used_percent>100) ? 100-$used_percent : $quotaPercent2; ?>
@@ -58,15 +58,15 @@ else {

/var/vmail on

-

B

+

-
+
-

  B ()

-

  GB (%)

+

  (%)

+

  (%)

diff --git a/data/web/inc/functions.domain_admin.inc.php b/data/web/inc/functions.domain_admin.inc.php index 7fe4b3fa..6c821aa1 100644 --- a/data/web/inc/functions.domain_admin.inc.php +++ b/data/web/inc/functions.domain_admin.inc.php @@ -444,18 +444,5 @@ function domain_admin($_action, $_data = null) { return $domainadmindata; break; - case 'total_quota': - if ($_SESSION['mailcow_cc_role'] != "admin") { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_action, $_data_log), - 'msg' => 'access_denied' - ); - return false; - } - $stmt = $pdo->query("SELECT SUM(`quota`) AS `quota` FROM `domain` WHERE `active`=1"); - $row = $stmt->fetch(PDO::FETCH_ASSOC); - return $row['quota']; - break; } } diff --git a/data/web/inc/functions.mailbox.inc.php b/data/web/inc/functions.mailbox.inc.php index 0b781a6c..7546c6c6 100644 --- a/data/web/inc/functions.mailbox.inc.php +++ b/data/web/inc/functions.mailbox.inc.php @@ -3254,7 +3254,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { } return $resourcedata; break; - case 'inactive_bytes': + case 'quota_stats': + $quotadata = array(); if ($_SESSION['mailcow_cc_role'] != "admin") { return false; } @@ -3263,9 +3264,13 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { WHERE `mailbox`.`kind` NOT REGEXP 'location|thing|group' AND `mailbox`.`username` = `quota2`.`username` AND `domain`.`domain` = `mailbox`.`domain` - AND `domain`.`active` = 0"); + AND `domain`.`active` = 1"); $row = $stmt->fetch(PDO::FETCH_ASSOC); - return $row['bytes']; + $quotadata['used_bytes'] = $row['bytes']/1024; + $stmt = $pdo->query("SELECT SUM(`quota`) AS `quota` FROM `domain` WHERE `active`=1"); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + $quotadata['total_quota'] = $row['quota']*1024; + return $quotadata; break; } break; From 8b5be0b56de4c00b29064360bde7f1bba17ca93d Mon Sep 17 00:00:00 2001 From: ntimo Date: Wed, 2 Oct 2019 13:05:12 +0200 Subject: [PATCH 05/11] [Web] Return 401 status code when API authentication fails --- data/web/inc/sessions.inc.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/data/web/inc/sessions.inc.php b/data/web/inc/sessions.inc.php index a94d438c..20232511 100644 --- a/data/web/inc/sessions.inc.php +++ b/data/web/inc/sessions.inc.php @@ -60,6 +60,7 @@ if (!empty($_SERVER['HTTP_X_API_KEY'])) { else { $redis->publish("F2B_CHANNEL", "mailcow UI: Invalid password for API_USER by " . $_SERVER['REMOTE_ADDR']); error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']); + http_response_code(401); echo json_encode(array( 'type' => 'error', 'msg' => 'api access denied for ip ' . $_SERVER['REMOTE_ADDR'] @@ -71,6 +72,7 @@ if (!empty($_SERVER['HTTP_X_API_KEY'])) { else { $redis->publish("F2B_CHANNEL", "mailcow UI: Invalid password for API_USER by " . $_SERVER['REMOTE_ADDR']); error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']); + http_response_code(401); echo json_encode(array( 'type' => 'error', 'msg' => 'authentication failed' From 1c35002505f4eeaa32329fbed42a123ede85cf4b Mon Sep 17 00:00:00 2001 From: andryyy Date: Wed, 2 Oct 2019 19:00:36 +0200 Subject: [PATCH 06/11] [Web] Do not allow to add domain admin for non existing domain [Web] oAuth2 implementation (wip) --- data/web/admin.php | 34 ++++- data/web/edit.php | 47 ++++++ data/web/inc/footer.inc.php | 4 +- data/web/inc/functions.customize.inc.php | 10 +- data/web/inc/functions.domain_admin.inc.php | 46 +++--- data/web/inc/functions.mailbox.inc.php | 12 ++ data/web/inc/functions.oauth2.inc.php | 152 ++++++++++++-------- data/web/inc/init_db.inc.php | 13 +- data/web/inc/prerequisites.inc.php | 8 +- data/web/inc/vars.inc.php | 4 + data/web/index.php | 8 +- data/web/js/site/admin.js | 38 +++++ data/web/json_api.php | 6 + data/web/lang/lang.de.php | 42 ++++-- data/web/lang/lang.en.php | 47 ++++-- data/web/lang/lang.nl.php | 2 +- data/web/modals/admin.php | 26 ++++ data/web/oauth/authorize.php | 66 +++++++++ data/web/oauth/profile.php | 28 ++++ data/web/oauth/token.php | 4 + 20 files changed, 478 insertions(+), 119 deletions(-) create mode 100644 data/web/oauth/authorize.php create mode 100644 data/web/oauth/profile.php create mode 100644 data/web/oauth/token.php diff --git a/data/web/admin.php b/data/web/admin.php index 155c6fc1..d78f3621 100644 --- a/data/web/admin.php +++ b/data/web/admin.php @@ -177,6 +177,30 @@ if (!isset($_SESSION['gal']) && $license_cache = $redis->Get('LICENSE_STATUS_CAC
+
+
OAuth2 Apps
+
+

+
+
+
+
+
+ + + + Add OAuth2 client +
+
+
+
+

Rspamd UI

@@ -950,10 +974,8 @@ if (!isset($_SESSION['gal']) && $license_cache = $redis->Get('LICENSE_STATUS_CAC

- - - -
+ +
@@ -975,8 +997,8 @@ if (!isset($_SESSION['gal']) && $license_cache = $redis->Get('LICENSE_STATUS_CAC
- - + +
diff --git a/data/web/edit.php b/data/web/edit.php index acbd8841..10896077 100644 --- a/data/web/edit.php +++ b/data/web/edit.php @@ -432,6 +432,53 @@ if (isset($_SESSION['mailcow_cc_role'])) { +

OAuth2

+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ +
+
+
+ + +

- +
set('TITLE_NAME', htmlspecialchars($title_name)); $redis->set('MAIN_NAME', htmlspecialchars($main_name)); $redis->set('APPS_NAME', htmlspecialchars($apps_name)); $redis->set('HELP_TEXT', $help_text); - $redis->set('UI_IMPRESS', $ui_impress); + $redis->set('UI_FOOTER', $ui_footer); } catch (RedisException $e) { $_SESSION['return'][] = array( @@ -203,7 +203,11 @@ function customize($_action, $_item, $_data = null) { $data['main_name'] = ($main_name = $redis->get('MAIN_NAME')) ? $main_name : 'mailcow UI'; $data['apps_name'] = ($apps_name = $redis->get('APPS_NAME')) ? $apps_name : 'mailcow Apps'; $data['help_text'] = ($help_text = $redis->get('HELP_TEXT')) ? $help_text : false; - $data['ui_impress'] = ($ui_impress = $redis->get('UI_IMPRESS')) ? $ui_impress : false; + if (!empty($redis->get('UI_IMPRESS'))) { + $redis->set('UI_FOOTER', $redis->get('UI_IMPRESS')); + $redis->del('UI_IMPRESS'); + } + $data['ui_footer'] = ($ui_footer = $redis->get('UI_FOOTER')) ? $ui_footer : false; return $data; } catch (RedisException $e) { diff --git a/data/web/inc/functions.domain_admin.inc.php b/data/web/inc/functions.domain_admin.inc.php index 064ba6c8..9c29b561 100644 --- a/data/web/inc/functions.domain_admin.inc.php +++ b/data/web/inc/functions.domain_admin.inc.php @@ -83,15 +83,17 @@ function domain_admin($_action, $_data = null) { return false; } $password_hashed = hash_password($password); + $valid_domains = 0; foreach ($domains as $domain) { - if (!is_valid_domain_name($domain)) { + if (!is_valid_domain_name($domain) || mailbox('get', 'domain_details', $domain) === false) { $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $_action, $_data_log), - 'msg' => 'domain_invalid' + 'msg' => array('domain_invalid', htmlspecialchars($domain)) ); - return false; + continue; } + $valid_domains++; $stmt = $pdo->prepare("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`) VALUES (:username, :domain, :created, :active)"); $stmt->execute(array( @@ -101,13 +103,15 @@ function domain_admin($_action, $_data = null) { ':active' => $active )); } - $stmt = $pdo->prepare("INSERT INTO `admin` (`username`, `password`, `superadmin`, `active`) - VALUES (:username, :password_hashed, '0', :active)"); - $stmt->execute(array( - ':username' => $username, - ':password_hashed' => $password_hashed, - ':active' => $active - )); + if ($valid_domains != 0) { + $stmt = $pdo->prepare("INSERT INTO `admin` (`username`, `password`, `superadmin`, `active`) + VALUES (:username, :password_hashed, '0', :active)"); + $stmt->execute(array( + ':username' => $username, + ':password_hashed' => $password_hashed, + ':active' => $active + )); + } } else { $_SESSION['return'][] = array( @@ -117,15 +121,17 @@ function domain_admin($_action, $_data = null) { ); return false; } - $stmt = $pdo->prepare("INSERT INTO `da_acl` (`username`) VALUES (:username)"); - $stmt->execute(array( - ':username' => $username - )); - $_SESSION['return'][] = array( - 'type' => 'success', - 'log' => array(__FUNCTION__, $_action, $_data_log), - 'msg' => array('domain_admin_added', htmlspecialchars($username)) - ); + if ($valid_domains != 0) { + $stmt = $pdo->prepare("INSERT INTO `da_acl` (`username`) VALUES (:username)"); + $stmt->execute(array( + ':username' => $username + )); + $_SESSION['return'][] = array( + 'type' => 'success', + 'log' => array(__FUNCTION__, $_action, $_data_log), + 'msg' => array('domain_admin_added', htmlspecialchars($username)) + ); + } break; case 'edit': if ($_SESSION['mailcow_cc_role'] != "admin" && $_SESSION['mailcow_cc_role'] != "domainadmin") { @@ -165,7 +171,7 @@ function domain_admin($_action, $_data = null) { $password2 = $_data['password2']; if (!empty($domains)) { foreach ($domains as $domain) { - if (!is_valid_domain_name($domain)) { + if (!is_valid_domain_name($domain) || mailbox('get', 'domain_details', $domain) === false) { $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $_action, $_data_log), diff --git a/data/web/inc/functions.mailbox.inc.php b/data/web/inc/functions.mailbox.inc.php index 99fda2d8..740fca3f 100644 --- a/data/web/inc/functions.mailbox.inc.php +++ b/data/web/inc/functions.mailbox.inc.php @@ -3806,6 +3806,18 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $stmt->execute(array( ':username' => $username )); + $stmt = $pdo->prepare("DELETE FROM `oauth_access_tokens` WHERE `user_id` = :username"); + $stmt->execute(array( + ':username' => $username + )); + $stmt = $pdo->prepare("DELETE FROM `oauth_refresh_tokens` WHERE `user_id` = :username"); + $stmt->execute(array( + ':username' => $username + )); + $stmt = $pdo->prepare("DELETE FROM `oauth_authorization_codes` WHERE `user_id` = :username"); + $stmt->execute(array( + ':username' => $username + )); $stmt = $pdo->prepare("SELECT `address`, `goto` FROM `alias` WHERE `goto` REGEXP :username"); $stmt->execute(array(':username' => '(^|,)'.$username.'($|,)')); diff --git a/data/web/inc/functions.oauth2.inc.php b/data/web/inc/functions.oauth2.inc.php index fdc908b3..7bc7dea6 100644 --- a/data/web/inc/functions.oauth2.inc.php +++ b/data/web/inc/functions.oauth2.inc.php @@ -4,9 +4,10 @@ function oauth2($_action, $_type, $_data = null) { global $redis; global $lang; if ($_SESSION['mailcow_cc_role'] != "admin") { - $_SESSION['return'] = array( + $_SESSION['return'][] = array( 'type' => 'danger', - 'msg' => sprintf($lang['danger']['access_denied']) + 'log' => array(__FUNCTION__, $_action, $_type, $_data), + 'msg' => 'access_denied' ); return false; } @@ -14,30 +15,26 @@ function oauth2($_action, $_type, $_data = null) { case 'add': switch ($_type) { case 'client': - $client_id = $_data['client_id']; - $client_secret = $_data['client_secret']; + $client_id = bin2hex(random_bytes(6)); + $client_secret = bin2hex(random_bytes(12)); $redirect_uri = $_data['redirect_uri']; + $scope = 'profile'; + // For future use // $grant_type = isset($_data['grant_type']) ? $_data['grant_type'] : 'authorization_code'; // $scope = isset($_data['scope']) ? $_data['scope'] : 'profile'; - if ($grant_type != "authorization_code" && $grant_type != "password") { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => sprintf($lang['danger']['access_denied']) - ); - return false; - } - // For future use + // if ($grant_type != "authorization_code" && $grant_type != "password") { + // $_SESSION['return'][] = array( + // 'type' => 'danger', + // 'log' => array(__FUNCTION__, $_action, $_type, $_data), + // 'msg' => 'access_denied' + // ); + // return false; + // } if ($scope != "profile") { - $_SESSION['return'] = array( + $_SESSION['return'][] = array( 'type' => 'danger', - 'msg' => sprintf($lang['danger']['access_denied']) - ); - return false; - } - if (!ctype_alnum($client_id) || !ctype_alnum($client_secret)) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => sprintf($lang['danger']['access_denied']) + 'log' => array(__FUNCTION__, $_action, $_type, $_data), + 'msg' => 'Invalid scope' ); return false; } @@ -46,21 +43,24 @@ function oauth2($_action, $_type, $_data = null) { $stmt->execute(array(':client_id' => $client_id)); $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); if ($num_results != 0) { - $_SESSION['return'] = array( + $_SESSION['return'][] = array( 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_type, $_data), 'msg' => 'Client ID exists' ); return false; } - $stmt = $pdo->prepare("INSERT INTO `oauth_clients` (`client_id`, `client_secret` ,`redirect_uri`) - VALUES (:client_id, :client_secret, :redirect_uri)"); + $stmt = $pdo->prepare("INSERT INTO `oauth_clients` (`client_id`, `client_secret`, `redirect_uri`, `scope`) + VALUES (:client_id, :client_secret, :redirect_uri, :scope)"); $stmt->execute(array( ':client_id' => $client_id, ':client_secret' => $client_secret, - ':redirect_uri' => $redirect_uri + ':redirect_uri' => $redirect_uri, + ':scope' => $scope )); - $_SESSION['return'] = array( + $_SESSION['return'][] = array( 'type' => 'success', + 'log' => array(__FUNCTION__, $_action, $_type, $_data), 'msg' => 'Added client access' ); break; @@ -73,47 +73,73 @@ function oauth2($_action, $_type, $_data = null) { foreach ($ids as $id) { $is_now = oauth2('details', 'client', $id); if (!empty($is_now)) { - $client_id = (!empty($_data['client_id'])) ? $_data['client_id'] : $is_now['client_id']; - $client_secret = (!empty($_data['client_secret'])) ? $_data['client_secret'] : $is_now['client_secret']; - $redirect_uri = (!empty($_data['redirect_uri'])) ? $_data['redirect_uri'] : $is_now['redirect_uri']; + $redirect_uri = (!empty($_data['redirect_uri'])) ? $_data['redirect_uri'] : $is_now['redirect_uri']; } else { - $_SESSION['return'] = array( + $_SESSION['return'][] = array( 'type' => 'danger', - 'msg' => sprintf($lang['danger']['access_denied']) + 'log' => array(__FUNCTION__, $_action, $_type, $_data), + 'msg' => 'access_denied' ); return false; } - if (!ctype_alnum($client_id) || !ctype_alnum($client_secret)) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => 'Client ID and secret must be alphanumeric' + if (isset($_data['revoke_tokens'])) { + $stmt = $pdo->prepare("DELETE FROM `oauth_access_tokens` + WHERE `client_id` IN ( + SELECT `client_id` FROM `oauth_clients` WHERE `id` = :id + )"); + $stmt->execute(array( + ':id' => $id + )); + $stmt = $pdo->prepare("DELETE FROM `oauth_refresh_tokens` + WHERE `client_id` IN ( + SELECT `client_id` FROM `oauth_clients` WHERE `id` = :id + )"); + $stmt->execute(array( + ':id' => $id + )); + $_SESSION['return'][] = array( + 'type' => 'success', + 'log' => array(__FUNCTION__, $_action, $_type, $_data), + 'msg' => array('object_modified', htmlspecialchars($id)) ); - return false; + continue; + } + if (isset($_data['renew_secret'])) { + $client_secret = bin2hex(random_bytes(12)); + $stmt = $pdo->prepare("UPDATE `oauth_clients` SET `client_secret` = :client_secret WHERE `id` = :id"); + $stmt->execute(array( + ':client_secret' => $client_secret, + ':id' => $id + )); + $_SESSION['return'][] = array( + 'type' => 'success', + 'log' => array(__FUNCTION__, $_action, $_type, $_data), + 'msg' => array('object_modified', htmlspecialchars($id)) + ); + continue; } if (empty($redirect_uri)) { - $_SESSION['return'] = array( + $_SESSION['return'][] = array( 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_type, $_data), 'msg' => 'Redirect/Callback URL cannot be empty' ); - return false; + continue; } $stmt = $pdo->prepare("UPDATE `oauth_clients` SET - `client_id` = :client_id, - `client_secret` = :client_secret, `redirect_uri` = :redirect_uri WHERE `id` = :id"); $stmt->execute(array( ':id' => $id, - ':client_id' => $client_id, - ':client_secret' => $client_secret, ':redirect_uri' => $redirect_uri )); + $_SESSION['return'][] = array( + 'type' => 'success', + 'log' => array(__FUNCTION__, $_action, $_type, $_data), + 'msg' => array('object_modified', htmlspecialchars($id)) + ); } - $_SESSION['return'] = array( - 'type' => 'success', - 'msg' => sprintf($lang['success']['object_modified'], htmlspecialchars(implode(', ', $ids))) - ); break; } break; @@ -123,39 +149,45 @@ function oauth2($_action, $_type, $_data = null) { (array)$ids = $_data['id']; foreach ($ids as $id) { if (!is_numeric($id)) { - $_SESSION['return'] = array( + $_SESSION['return'][] = array( 'type' => 'danger', - 'msg' => sprintf($lang['danger']['access_denied']) + 'log' => array(__FUNCTION__, $_action, $_type, $_data), + 'msg' => 'access_denied' ); - return false; + continue; } - $stmt = $pdo->prepare("DELETE FROM `oauth_clients` WHERE `id` = :id"); + $stmt = $pdo->prepare("DELETE FROM `oauth_clients` + WHERE `id` = :id"); $stmt->execute(array( ':id' => $id )); } - $_SESSION['return'] = array( + $_SESSION['return'][] = array( 'type' => 'success', - 'msg' => sprintf($lang['success']['items_deleted'], implode(', ', $ids)) + 'log' => array(__FUNCTION__, $_action, $_type, $_data), + 'msg' => array('items_deleted', htmlspecialchars($id)) ); break; case 'access_token': (array)$access_tokens = $_data['access_token']; foreach ($access_tokens as $access_token) { if (!ctype_alnum($access_token)) { - $_SESSION['return'] = array( + $_SESSION['return'][] = array( 'type' => 'danger', - 'msg' => sprintf($lang['danger']['access_denied']) + 'log' => array(__FUNCTION__, $_action, $_type, $_data), + 'msg' => 'access_denied' ); return false; } - $stmt = $pdo->prepare("DELETE FROM `oauth_access_tokens` WHERE `access_token` = :access_token"); + $stmt = $pdo->prepare("DELETE FROM `oauth_access_tokens` + WHERE `access_token` = :access_token"); $stmt->execute(array( ':access_token' => $access_token )); } - $_SESSION['return'] = array( + $_SESSION['return'][] = array( 'type' => 'success', + 'log' => array(__FUNCTION__, $_action, $_type, $_data), 'msg' => sprintf($lang['success']['items_deleted'], implode(', ', $access_tokens)) ); break; @@ -163,9 +195,10 @@ function oauth2($_action, $_type, $_data = null) { (array)$refresh_tokens = $_data['refresh_token']; foreach ($refresh_tokens as $refresh_token) { if (!ctype_alnum($refresh_token)) { - $_SESSION['return'] = array( + $_SESSION['return'][] = array( 'type' => 'danger', - 'msg' => sprintf($lang['danger']['access_denied']) + 'log' => array(__FUNCTION__, $_action, $_type, $_data), + 'msg' => 'access_denied' ); return false; } @@ -174,8 +207,9 @@ function oauth2($_action, $_type, $_data = null) { ':refresh_token' => $refresh_token )); } - $_SESSION['return'] = array( + $_SESSION['return'][] = array( 'type' => 'success', + 'log' => array(__FUNCTION__, $_action, $_type, $_data), 'msg' => sprintf($lang['success']['items_deleted'], implode(', ', $refresh_tokens)) ); break; diff --git a/data/web/inc/init_db.inc.php b/data/web/inc/init_db.inc.php index c97eeb56..e1daed63 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 = "27092019_1040"; + $db_version = "29092019_1040"; $stmt = $pdo->query("SHOW TABLES LIKE 'versions'"); $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); @@ -1055,6 +1055,17 @@ BEGIN DELETE FROM spamalias WHERE validity < UNIX_TIMESTAMP(); END; // +DELIMITER ;'; + $events[] = 'DROP EVENT IF EXISTS clean_oauth2; +DELIMITER // +CREATE EVENT clean_oauth2 +ON SCHEDULE EVERY 1 DAY DO +BEGIN + DELETE FROM oauth_refresh_tokens WHERE expires < NOW(); + DELETE FROM oauth_access_tokens WHERE expires < NOW(); + DELETE FROM oauth_authorization_codes WHERE expires < NOW(); +END; +// DELIMITER ;'; foreach ($events as $event) { $pdo->exec($event); diff --git a/data/web/inc/prerequisites.inc.php b/data/web/inc/prerequisites.inc.php index 35688315..e0cc5993 100644 --- a/data/web/inc/prerequisites.inc.php +++ b/data/web/inc/prerequisites.inc.php @@ -105,13 +105,15 @@ class mailcowPdo extends OAuth2\Storage\Pdo { $oauth2_scope_storage = new OAuth2\Storage\Memory(array('default_scope' => 'profile', 'supported_scopes' => array('profile'))); $oauth2_storage = new mailcowPdo(array('dsn' => $dsn, 'username' => $database_user, 'password' => $database_pass)); $oauth2_server = new OAuth2\Server($oauth2_storage, array( - 'always_issue_new_refresh_token' => true, - 'refresh_token_lifetime' => 2678400, + 'refresh_token_lifetime' => $REFRESH_TOKEN_LIFETIME, + 'access_lifetime' => $ACCESS_TOKEN_LIFETIME, )); $oauth2_server->setScopeUtil(new OAuth2\Scope($oauth2_scope_storage)); $oauth2_server->addGrantType(new OAuth2\GrantType\AuthorizationCode($oauth2_storage)); $oauth2_server->addGrantType(new OAuth2\GrantType\UserCredentials($oauth2_storage)); -$oauth2_server->addGrantType(new OAuth2\GrantType\RefreshToken($oauth2_storage)); +$oauth2_server->addGrantType(new OAuth2\GrantType\RefreshToken($oauth2_storage, array( + 'always_issue_new_refresh_token' => true +))); function exception_handler($e) { if ($e instanceof PDOException) { diff --git a/data/web/inc/vars.inc.php b/data/web/inc/vars.inc.php index 90b7e377..eceee8ae 100644 --- a/data/web/inc/vars.inc.php +++ b/data/web/inc/vars.inc.php @@ -129,6 +129,10 @@ $DOCKER_TIMEOUT = 60; // Anonymize IPs logged via UI $ANONYMIZE_IPS = true; +// OAuth2 settings +$REFRESH_TOKEN_LIFETIME = 2678400; +$ACCESS_TOKEN_LIFETIME = 86400; + // MAILBOX_DEFAULT_ATTRIBUTES define default attributes for new mailboxes // These settings will not change existing mailboxes diff --git a/data/web/index.php b/data/web/index.php index 19372351..ffaec3c1 100644 --- a/data/web/index.php +++ b/data/web/index.php @@ -1,7 +1,13 @@ '; }); + } else if (table == 'oauth2clientstable') { + $.each(data, function (i, item) { + item.action = ''; + item.scope = "profile"; + item.grant_types = 'refresh_token password authorization_code'; + item.chkbox = ''; + }); } else if (table == 'domainadminstable') { $.each(data, function (i, item) { item.selected_domains = escapeHtml(item.selected_domains); @@ -299,6 +336,7 @@ jQuery(function($){ draw_admins(); draw_fwd_hosts(); draw_relayhosts(); + draw_oauth2_clients(); draw_transport_maps(); draw_queue(); // Relayhost diff --git a/data/web/json_api.php b/data/web/json_api.php index fea4565b..ea4304af 100644 --- a/data/web/json_api.php +++ b/data/web/json_api.php @@ -142,6 +142,9 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u case "mailbox": process_add_return(mailbox('add', 'mailbox', $attr)); break; + case "oauth2-client": + process_add_return(oauth2('add', 'client', $attr)); + break; case "domain": process_add_return(mailbox('add', 'domain', $attr)); break; @@ -1056,6 +1059,9 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u case "alias": process_delete_return(mailbox('delete', 'alias', array('id' => $items))); break; + case "oauth2-client": + process_delete_return(oauth2('delete', 'client', array('id' => $items))); + break; case "relayhost": process_delete_return(relayhost('delete', array('id' => $items))); break; diff --git a/data/web/lang/lang.de.php b/data/web/lang/lang.de.php index 22c07caa..ef1b682e 100644 --- a/data/web/lang/lang.de.php +++ b/data/web/lang/lang.de.php @@ -711,7 +711,39 @@ $lang['admin']['help_text'] = "Hilfstext unter Login-Maske (HTML zulässig)"; $lang['admin']['title_name'] = '"mailcow UI" Webseiten Titel'; $lang['admin']['main_name'] = '"mailcow UI" Name'; $lang['admin']['apps_name'] = '"mailcow Apps" Name'; -$lang['admin']['ui_impress'] = 'Impressum, Footer (HTML zulässig)'; +$lang['admin']['ui_footer'] = 'Footer (HTML zulässig)'; + +$lang['admin']['oauth2_info'] = 'Die OAuth2 Implementierung untersützt den Grant Type "Authorization Code" mit Refresh Tokens.
+Der Server wird automatisch einen neuen Refresh Token ausstellen, sobald ein vorheriger Token gegen einen Access Token eingetauscht wurde.

+→ Der Standard Scope lautet profile. Nur Mailbox-Benutzer können sich gegen OAuth2 authentifizieren. Wird kein Scope angegeben, verwendet das System per Standard profile.
+→ Der state Parameter wird im Zuge des Autorisierungsprozesses benötigt.

+Die Pfade für die OAuth2 API lauten wie folgt:
+
    +
  • Authorization Endpoint: /oauth/authorize
  • +
  • Token Endpoint: /oauth/token
  • +
  • Resource Page: /oauth/profile
  • +
+Die Regenerierung des Client Secrets wird vorhandene Authorization Codes nicht invalidieren, dennoch wird der Renew des Access Tokens durch einen Refresh Token nicht mehr gelingen.

+Das Entfernen aller Client Tokens verursacht die umgehende Terminierung aller aktiven OAuth2 Sessions. Clients müssen sich erneut gegen die OAuth2 Anwendung authentifizieren.'; + +$lang['admin']['oauth2_client_id'] = "Client ID"; +$lang['admin']['oauth2_client_secret'] = "Client Secret"; +$lang['admin']['oauth2_redirect_uri'] = "Redirect URI"; +$lang['admin']['oauth2_revoke_tokens'] = 'Alle Client Tokens entfernen'; +$lang['admin']['oauth2_renew_secret'] = 'Neues Client Secret generieren'; +$lang['edit']['client_id'] = 'Client ID'; +$lang['edit']['client_secret'] = 'Client Secret'; +$lang['edit']['scope'] = 'Scope'; +$lang['edit']['grant_types'] = 'Grant types'; +$lang['edit']['redirect_uri'] = 'Redirect/Callback URL'; +$lang['oauth2']['scope_ask_permission'] = 'Eine Anwendung hat um die folgenden Berechtigungen gebeten'; +$lang['oauth2']['profile'] = 'Profil'; +$lang['oauth2']['profile_desc'] = 'Persönliche Informationen anzeigen: Benutzername, Name, Erstellzeitpunkt, Änderungszeitpunkt, Status'; +$lang['oauth2']['permit'] = 'Anwendung authorisieren'; +$lang['oauth2']['authorize_app'] = 'Anwendung authorisieren'; +$lang['oauth2']['deny'] = 'Ablehnen'; +$lang['oauth2']['access_denied'] = 'Bitte als Mailbox-Nutzer einloggen, um den Zugriff via OAuth2 zu erlauben.'; + $lang['admin']['customize'] = "UI Anpassung"; $lang['admin']['change_logo'] = "Logo ändern"; @@ -836,14 +868,6 @@ $lang['mailbox']['add_tls_policy_map'] = "TLS-Richtlinieneintrag hinzufügen"; $lang['danger']['tls_policy_map_parameter_invalid'] = "Parameter ist ungültig"; $lang['danger']['temp_error'] = "Temporärer Fehler"; -$lang['oauth2']['scope_ask_permission'] = 'Eine Anwendung hat um die folgenden Berechtigungen gebeten'; -$lang['oauth2']['profile'] = 'Profil'; -$lang['oauth2']['profile_desc'] = 'Persönliche Informationen anzeigen: Benutzername, Name, Erstellzeitpunkt, Änderungszeitpunkt, Status'; -$lang['oauth2']['permit'] = 'Anwendung authorisieren'; -$lang['oauth2']['authorize_app'] = 'Anwendung authorisieren'; -$lang['oauth2']['deny'] = 'Ablehnen'; -$lang['oauth2']['access_denied'] = 'Bitte als Mailbox-Nutzer einloggen, um den Zugriff via OAuth2 zu erlauben.'; - $lang['admin']['sys_mails'] = 'System-E-Mails'; $lang['admin']['subject'] = 'Betreff'; $lang['admin']['from'] = 'Absender'; diff --git a/data/web/lang/lang.en.php b/data/web/lang/lang.en.php index 795e531c..de5aaaef 100644 --- a/data/web/lang/lang.en.php +++ b/data/web/lang/lang.en.php @@ -346,11 +346,6 @@ $lang['mailbox']['sogo_visible'] = 'Alias is visible in SOGo'; $lang['mailbox']['sogo_visible_y'] = 'Show alias in SOGo'; $lang['mailbox']['sogo_visible_n'] = 'Hide alias in SOGo'; $lang['edit']['syncjob'] = 'Edit sync job'; -$lang['edit']['client_id'] = 'Client ID'; -$lang['edit']['client_secret'] = 'Client secret'; -$lang['edit']['scope'] = 'Scope'; -$lang['edit']['grant_types'] = 'Grant types'; -$lang['edit']['redirect_uri'] = 'Redirect/Callback URL'; $lang['edit']['hostname'] = 'Hostname'; $lang['edit']['encryption'] = 'Encryption'; $lang['edit']['maxage'] = 'Maximum age of messages in days that will be polled from remote
(0 = ignore age)'; @@ -679,6 +674,38 @@ $lang['admin']['credentials_transport_warning'] = 'Warning: Adding a new $lang['admin']['destination'] = 'Destination'; $lang['admin']['nexthop'] = 'Next hop'; +$lang['admin']['oauth2_info'] = 'The OAuth2 implementation supports the grant type "Authorization Code" and issues refresh tokens.
+The server also automatically issues new refresh tokens, after a refresh token has been used.

+→ The default scope is profile. Only mailbox users can be authenticated against OAuth2. If the scope parameter is omitted, it falls back to profile.
+→ The state parameter is required to be sent by the client as part of the authorize request.

+Pathes for requests to the OAuth2 API:
+
    +
  • Authorization endpoint: /oauth/authorize
  • +
  • Token endpoint: /oauth/token
  • +
  • Resource page: /oauth/profile
  • +
+Regenerating the client secret will not expire existing authorization codes, but they will fail to renew their token.

+Revoking client tokens will cause immediate termination of all active sessions. All clients need to re-authenticate.'; + +$lang['admin']['oauth2_client_id'] = "Client ID"; +$lang['admin']['oauth2_client_secret'] = "Client secret"; +$lang['admin']['oauth2_redirect_uri'] = "Redirect URI"; +$lang['admin']['oauth2_revoke_tokens'] = 'Revoke all client tokens'; +$lang['admin']['oauth2_renew_secret'] = 'Generate new client secret'; +$lang['edit']['client_id'] = 'Client ID'; +$lang['edit']['client_secret'] = 'Client secret'; +$lang['edit']['scope'] = 'Scope'; +$lang['edit']['grant_types'] = 'Grant types'; +$lang['edit']['redirect_uri'] = 'Redirect/Callback URL'; +$lang['oauth2']['scope_ask_permission'] = 'An application asked for the following permissions'; +$lang['oauth2']['profile'] = 'Profile'; +$lang['oauth2']['profile_desc'] = 'View personal information: username, full name, created, modified, active'; +$lang['oauth2']['permit'] = 'Authorize application'; +$lang['oauth2']['authorize_app'] = 'Authorize application'; +$lang['oauth2']['deny'] = 'Deny'; +$lang['oauth2']['access_denied'] = 'Please login as mailbox owner to grant access via OAuth2.'; + + $lang['success']['forwarding_host_removed'] = "Forwarding host %s has been removed"; $lang['success']['forwarding_host_added'] = "Forwarding host %s has been added"; $lang['success']['relayhost_removed'] = "Map entry %s has been removed"; @@ -727,7 +754,7 @@ $lang['admin']['help_text'] = "Override help text below login mask (HTML allowed $lang['admin']['title_name'] = '"mailcow UI" website title'; $lang['admin']['main_name'] = '"mailcow UI" name'; $lang['admin']['apps_name'] = '"mailcow Apps" name'; -$lang['admin']['ui_impress'] = 'Impress, Footer note (HTML allowed)'; +$lang['admin']['ui_footer'] = 'Footer (HTML allowed)'; $lang['admin']['customize'] = "Customize"; $lang['admin']['change_logo'] = "Change logo"; @@ -864,14 +891,6 @@ $lang['mailbox']['add_recipient_map_entry'] = 'Add recipient map'; $lang['danger']['tls_policy_map_parameter_invalid'] = "Policy parameter is invalid"; $lang['danger']['temp_error'] = "Temporary error"; -$lang['oauth2']['scope_ask_permission'] = 'An application asked for the following permissions'; -$lang['oauth2']['profile'] = 'Profile'; -$lang['oauth2']['profile_desc'] = 'View personal information: username, full name, created, modified, active'; -$lang['oauth2']['permit'] = 'Authorize application'; -$lang['oauth2']['authorize_app'] = 'Authorize application'; -$lang['oauth2']['deny'] = 'Deny'; -$lang['oauth2']['access_denied'] = 'Please login as mailbox owner to grant access via OAuth2.'; - $lang['admin']['sys_mails'] = 'System mails'; $lang['admin']['subject'] = 'Subject'; $lang['admin']['from'] = 'From'; diff --git a/data/web/lang/lang.nl.php b/data/web/lang/lang.nl.php index 85812380..b04e727a 100644 --- a/data/web/lang/lang.nl.php +++ b/data/web/lang/lang.nl.php @@ -710,7 +710,7 @@ $lang['admin']['help_text'] = "Hulpteksten onder inlogvenster (HTML toegestaan)" $lang['admin']['title_name'] = '"Mailcow" (website-titel)'; $lang['admin']['main_name'] = '"Mailcow"'; $lang['admin']['apps_name'] = '"Mailcow-apps"'; -$lang['admin']['ui_impress'] = 'Footer-vermelding (HTML toegestaan)'; +$lang['admin']['ui_footer'] = 'Footer-vermelding (HTML toegestaan)'; $lang['admin']['customize'] = "Uiterlijk"; $lang['admin']['change_logo'] = "Logo"; diff --git a/data/web/modals/admin.php b/data/web/modals/admin.php index 2303519e..ae23d63a 100644 --- a/data/web/modals/admin.php +++ b/data/web/modals/admin.php @@ -105,6 +105,32 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
+ +