From e786609aa97a5d02764aab5112ca8352b80719de Mon Sep 17 00:00:00 2001 From: Damien Martins Date: Fri, 17 Aug 2018 16:07:14 +0200 Subject: [PATCH] Adding support for password expiration. Please read README.password_expiration for more details --- README.password_expiration | 38 ++++++++++++++++++++++++++++++ check_mailpass_expiration.sh | 34 ++++++++++++++++++++++++++ config.inc.php | 6 +++++ functions.inc.php | 25 +++++++++++++++++++- languages/en.lang | 1 + languages/fr.lang | 1 + model/MailboxHandler.php | 1 + password_expiration.sql | 5 ++++ public/list-virtual.php | 5 ++++ templates/list-virtual_mailbox.tpl | 6 +++++ 10 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 README.password_expiration create mode 100644 check_mailpass_expiration.sh create mode 100644 password_expiration.sql diff --git a/README.password_expiration b/README.password_expiration new file mode 100644 index 00000000..0a350d1d --- /dev/null +++ b/README.password_expiration @@ -0,0 +1,38 @@ +*Description +This extension adds support for password expiration. +It is designed to have expiration on users passwords. An email is sent when the password is expiring in 30 days, then 14 days, then 7 days. +It is strongly inspired by https://abridge2devnull.com/posts/2014/09/29/dovecot-user-password-expiration-notifications-updated-4122015/, and adapted to fit with Postfix Admin & Roundcube's password plugin + +*Installation +Perform the following changes: + +**Changes in MySQL/MariaDB mailbox table (as defined in $CONF['database_tables'] from config.inc.php): +You are invited to backup your DB first, and ensure the table name is correct. + +Execute the attached SQL script (password_expiration.sql) that will add the required columns. The expiration value for existing users will be set to 90 days. If you want a different value, edit the last line in the script and replace 90 by the required value. + +**Changes in Postfix Admin : +To enable password expiration, add the following to your config.inc.php file: +$CONF['password_expiration_enabled'] = 'YES'; + +Do not forget to set the expiration value (in days) +$CONF['password_expiration_value'] = '90'; + +All my tests are performed using $CONF['encrypt'] = 'md5crypt'; + +**If you are using Roundcube's password plugin, you should also adapt the $config['password_query'] value. +I recommend to use: +$config['password_query'] = 'UPDATE mailbox SET password=%c, modified=now(),pw_expires_on=now() + interval 90 day, thirty=0,fourteen=0,seven=0 where username=%u'; +of cource you may adapt to the expected expiration value + +All my tests are performed using $config['password_algorithm'] = 'md5-crypt'; + +**Changes in Dovecot (adapt if you use another LDA) +Edit dovecot-mysql.conf file, and replace the user_query (and only this one) by this query: +user_query = SELECT concat('/var/vmail/', maildir) as home, concat('maildir:/var/vmail/', maildir) as mail, 20001 AS uid, 20001 AS gid, concat('dirsize:storage=', quota) AS quota FROM mailbox WHERE username = '%u' AND active = '1' AND pw_expires_on > now() +if course you may require to adapt the uid, gid, maildir and table to your setup + +**Changes in system +You need to have a script running on a daily basis to check password expiration and send emails 30, 14 and 7 days before password expiration (script attached: check_mailpass_expiration.sh). +Edit the script to adapt the variables to your setup. +Ensure the user running check_mailpass_expiration.sh is allowed to access (read-write) your database. diff --git a/check_mailpass_expiration.sh b/check_mailpass_expiration.sh new file mode 100644 index 00000000..af200abc --- /dev/null +++ b/check_mailpass_expiration.sh @@ -0,0 +1,34 @@ +#!/bin/bash +#Adapt to your setup + +POSTFIX_DB="postfix_test" +POSTFIX_USER="postfixadmin" +POSTFIX_PASSWORD="my_password_is_strong" + +#All the rest should be OK +QUERY30DAYS="SELECT username,pw_expires_on FROM mailbox WHERE pw_expires_on > now() + interval 29 DAY AND pw_expires_on < now() + interval 30 day AND thirty = false;" +QUERY14DAYS="SELECT username,pw_expires_on FROM mailbox WHERE pw_expires_on > now() + interval 13 DAY AND pw_expires_on < now() + interval 14 day AND fourteen = false;" +QUERY7DAYS="SELECT username,pw_expires_on FROM mailbox WHERE pw_expires_on > now() + interval 6 DAY AND pw_expires_on < now() + interval 7 day AND seven = false;" + +function notifyThirtyDays() { + mysql -B -u "$POSTFIX_USER" -p"$POSTFIX_PASSWORD" "$POSTFIX_DB" -e "$QUERY30DAYS" | while read -a RESULT; do + echo -e "Dear User, \n Your password will expire on ${RESULT[1]}" | mail -s "Password 30 days before expiration notication" -r noreply@eyetech.fr ${RESULT[0]} + echo "UPDATE mailbox SET thirty = true WHERE username = '${RESULT[0]}';" | mysql -u postfix postfix_test;done +} + +function notifyFourteenDays() { + mysql -B -u "$POSTFIX_USER" -p"$POSTFIX_PASSWORD" "$POSTFIX_DB" -e "$QUERY14DAYS" | while read -a RESULT; do + echo -e "Dear User, \n Your password will expire on ${RESULT[1]}" | mail -s "Password 14 days before expiration notication" -r noreply@eyetech.fr ${RESULT[0]} + echo "UPDATE mailbox SET fourteen = true WHERE username = '${RESULT[0]}';" | mysql -u postfix postfix_test;done +} + +function notifySevenDays() { + mysql -B -u "$POSTFIX_USER" -p"$POSTFIX_PASSWORD" "$POSTFIX_DB" -e "$QUERY7DAYS" | while read -a RESULT; do + echo -e "Dear User, \n Your password will expire on ${RESULT[1]}" | mail -s "Password 7 days before expiraiton notication" -r noreply@eyetech.fr ${RESULT[0]} + echo "UPDATE mailbox SET seven = true WHERE username = '${RESULT[0]}';" | mysql -u postfix postfix_test;done +} + +notifyThirtyDays # Execute the function for 30 day notices +notifyFourteenDays # Execute the function for 14 day notices +notifySevenDays # Execute the function for 7 day notices + diff --git a/config.inc.php b/config.inc.php index e5b40d2a..625b0315 100644 --- a/config.inc.php +++ b/config.inc.php @@ -661,6 +661,12 @@ $CONF['theme_custom_css'] = ''; // change to boolean true to enable xmlrpc $CONF['xmlrpc_enabled'] = false; +//Account expiration info +//If you want to display the password expiracy status of the accounts (read-only) +//More details in README.password_expiration +$CONF['password_expiration_enable'] = 'YES'; +$CONF['password_expiration_value'] = '90'; + // If you want to keep most settings at default values and/or want to ensure // that future updates work without problems, you can use a separate config // file (config.local.php) instead of editing this file and override some diff --git a/functions.inc.php b/functions.inc.php index 2298551d..b57c87af 100644 --- a/functions.inc.php +++ b/functions.inc.php @@ -1865,7 +1865,7 @@ function db_delete($table, $where, $delete, $additionalwhere='') { * @param array (optional) - array of fields to set to now() - default: array('created', 'modified') * @return int - number of inserted rows */ -function db_insert($table, $values, $timestamp = array('created', 'modified')) { +function db_insert ($table, $values, $timestamp = array('created', 'modified'), $timestamp_expiration = array('pw_expires_on') ) { $table = table_by_key($table); foreach (array_keys($values) as $key) { @@ -1879,6 +1879,18 @@ function db_insert($table, $values, $timestamp = array('created', 'modified')) { $values[$key] = "now()"; } } + if ($table == 'mailbox') { + global $CONF; + if ($CONF['password_expiration_enabled'] == 'YES') { + $expires_warning_values = array('thirty', 'fourteen', 'seven'); + foreach($expires_warning_values as $key) { + $values[$key] = escape_string($key) . "=0"; + } + foreach($timestamp_expiration as $key) { + $values[$key] = "now() + interval " . $CONF['password_expiration_value'] . " day"; + } + } + } $sql_values = "(" . implode(",", escape_string(array_keys($values))).") VALUES (".implode(",", $values).")"; @@ -1927,6 +1939,17 @@ function db_update_q($table, $where, $values, $timestamp = array('modified')) { $sql_values[$key] = escape_string($key) . "=now()"; } } + if ($table == 'mailbox') { + global $CONF; + if ($CONF['password_expiration_enabled'] == 'YES') { + $key = 'pw_expires_on'; + $sql_values[$key] = escape_string($key) . "=now() + interval " . $CONF['password_expiration_value'] . " day"; + $expires_warning_values = array('thirty', 'fourteen', 'seven'); + foreach($expires_warning_values as $key) { + $sql_values[$key] = escape_string($key) . "=0"; + } + } + } $sql="UPDATE $table SET " . implode(",", $sql_values) . " WHERE $where"; diff --git a/languages/en.lang b/languages/en.lang index ec7137ca..619f9aa5 100644 --- a/languages/en.lang +++ b/languages/en.lang @@ -407,6 +407,7 @@ $PALANG['pFetchmail_desc_returned_text'] = 'Text message from last polling'; $PALANG['dateformat_pgsql'] = 'YYYY-mm-dd'; # translators: rearrange to your local date format, but make sure it's a valid PostgreSQL date format $PALANG['dateformat_mysql'] = '%Y-%m-%d'; # translators: rearrange to your local date format, but make sure it's a valid MySQL date format +$PALANG['password_expiration'] = 'Pass expires'; $PALANG['please_keep_this_as_last_entry'] = ''; # needed for language-check.sh /* vim: set expandtab ft=php softtabstop=3 tabstop=3 shiftwidth=3: */ diff --git a/languages/fr.lang b/languages/fr.lang index 56d56d39..7ae7aa85 100644 --- a/languages/fr.lang +++ b/languages/fr.lang @@ -398,6 +398,7 @@ $PALANG['pFetchmail_desc_date'] = 'Date de la dernière vérification/changement $PALANG['pFetchmail_desc_returned_text'] = 'Message de la dernière vérification'; $PALANG['dateformat_pgsql'] = 'dd-mm-YYYY'; $PALANG['dateformat_mysql'] = '%d-%m-%Y'; +$PALANG['password_expiration'] = 'Expiration du mot de passe'; $PALANG['please_keep_this_as_last_entry'] = ''; # needed for language-check.sh /* vim: set expandtab ft=php softtabstop=3 tabstop=3 shiftwidth=3: */ diff --git a/model/MailboxHandler.php b/model/MailboxHandler.php index 57c4ab39..32d9dde8 100644 --- a/model/MailboxHandler.php +++ b/model/MailboxHandler.php @@ -49,6 +49,7 @@ class MailboxHandler extends PFAHandler { 'token_validity' => pacol(1, 0, 0, 'ts', '' , '', date("Y-m-d H:i:s",time())), 'created' => pacol(0, 0, 1, 'ts', 'created' , '' ), 'modified' => pacol(0, 0, 1, 'ts', 'last_modified' , '' ), + 'pw_expires_on' => pacol( 0, 0, 1, 'ts', 'password_expiration' , '' ), # TODO: add virtual 'notified' column and allow to display who received a vacation response? ); diff --git a/password_expiration.sql b/password_expiration.sql new file mode 100644 index 00000000..c6f53216 --- /dev/null +++ b/password_expiration.sql @@ -0,0 +1,5 @@ +ALTER TABLE mailbox ADD COLUMN pw_expires_on TIMESTAMP DEFAULT now() not null; +ALTER TABLE mailbox ADD COLUMN thirty boolean not null DEFAULT false; +ALTER TABLE mailbox ADD COLUMN fourteen boolean not null DEFAULT false; +ALTER TABLE mailbox ADD COLUMN seven boolean not null DEFAULT false; +UPDATE mailbox set pw_expires_on = now() + interval 90 day; diff --git a/public/list-virtual.php b/public/list-virtual.php index 7f7d489f..9860580f 100644 --- a/public/list-virtual.php +++ b/public/list-virtual.php @@ -165,6 +165,7 @@ $tAlias = $handler->result(); # $display_mailbox_aliases = Config::bool('alias_control_admin'); +$password_expiration = Config::bool('password_expiration'); # build the sql query $sql_select = "SELECT $table_mailbox.* "; @@ -190,6 +191,10 @@ if ($display_mailbox_aliases) { $sql_join .= " LEFT JOIN $table_alias ON $table_mailbox.username=$table_alias.address "; } +if ($password_expiration) { + $sql_select .= ", $table_mailbox.pw_expires_on as password_expiration "; +} + if (Config::bool('vacation_control_admin')) { $table_vacation = table_by_key('vacation'); $sql_select .= ", $table_vacation.active AS v_active "; diff --git a/templates/list-virtual_mailbox.tpl b/templates/list-virtual_mailbox.tpl index 0b19628b..40eecc58 100644 --- a/templates/list-virtual_mailbox.tpl +++ b/templates/list-virtual_mailbox.tpl @@ -13,6 +13,9 @@ {$PALANG.name} {if $CONF.quota===YES}{$PALANG.pOverview_mailbox_quota}{/if} {$PALANG.last_modified} + {if $CONF.password_expiration===YES} + {$PALANG.password_expiration} + {/if} {$PALANG.active} {assign var="colspan" value="`$colspan-6`"}   @@ -74,6 +77,9 @@ {/if} {$item.modified} + {if $CONF.password_expiration===YES} + {$item.password_expiration} + {/if} {if $item.active==1}{$PALANG.YES}{else}{$PALANG.NO}{/if} {if $CONF.vacation_control_admin===YES && $CONF.vacation===YES}