-
Notifications
You must be signed in to change notification settings - Fork 6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
send notification when passwords are about to expire #27
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
/* | ||
* Copyright (c) 2018 Vincent Petry <[email protected]> | ||
* | ||
* This file is licensed under the Affero General Public License version 3 | ||
* or later. | ||
* | ||
* See the COPYING-README file. | ||
* | ||
*/ | ||
(function () { | ||
|
||
$(document).ready(function() { | ||
// convert action URL to redirect | ||
$('body').on('OCA.Notification.Action', function(e) { | ||
if (e.notification.app === 'password_policy' | ||
&& (e.notification.object_type === 'about_to_expire' || e.notification.object_type === 'expired') | ||
&& e.action.type === 'GET' | ||
) { | ||
OC.redirect(e.notification.link); | ||
return false; | ||
} | ||
}); | ||
}); | ||
})(); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -26,6 +26,7 @@ | |
use OCP\IRequest; | ||
use OCP\Settings\ISettings; | ||
use OCP\Template; | ||
use OCA\PasswordPolicy\UserNotificationConfigHandler; | ||
|
||
class SettingsController extends Controller implements ISettings { | ||
|
||
|
@@ -49,13 +50,26 @@ class SettingsController extends Controller implements ISettings { | |
'spv_password_history_value' => 3, | ||
'spv_user_password_expiration_checked' => false, | ||
'spv_user_password_expiration_value' => 90, | ||
'spv_user_password_expiration_notification_checked' => false, | ||
'spv_user_password_expiration_notification_value' => UserNotificationConfigHandler::DEFAULT_EXPIRATION_FOR_NORMAL_NOTIFICATION, | ||
'spv_user_password_force_change_on_first_login_checked' => false, | ||
'spv_expiration_password_checked' => false, | ||
'spv_expiration_password_value' => 7, | ||
'spv_expiration_nopassword_checked' => false, | ||
'spv_expiration_nopassword_value' => 7, | ||
]; | ||
|
||
/** | ||
* functions to convert values between what is shown and what is stored | ||
* these functions must be defined in this class, they're per config key | ||
*/ | ||
const CONVERSIONS = [ | ||
'spv_user_password_expiration_notification_value' => [ | ||
'in' => 'daysToSeconds', | ||
'out' => 'secondsToDays', | ||
], | ||
]; | ||
|
||
public function __construct($appName, | ||
IRequest $request, | ||
IConfig $config) { | ||
|
@@ -71,6 +85,10 @@ public function updatePolicy() { | |
if ($this->request->getParam($key) !== null) { | ||
if ($key !== 'spv_def_special_chars_value' && \substr($key, -6) === '_value') { | ||
$value = \min(\max(0, (int)$this->request->getParam($key)), 255); | ||
if (isset(self::CONVERSIONS[$key]['in'])) { | ||
$convertFuncName = self::CONVERSIONS[$key]['in']; | ||
$value = $this->$convertFuncName($value); | ||
} | ||
$this->config->setAppValue('password_policy', $key, $value); | ||
} else { | ||
$this->config->setAppValue('password_policy', $key, \strip_tags($this->request->getParam($key))); | ||
|
@@ -92,9 +110,33 @@ public function getPriority() { | |
public function getPanel() { | ||
$template = new Template('password_policy', 'admin'); | ||
foreach(self::DEFAULTS as $key => $default) { | ||
$template->assign($key, $this->config->getAppValue('password_policy', $key, $default)); | ||
$value = $this->config->getAppValue('password_policy', $key, $default); | ||
if (isset(self::CONVERSIONS[$key]['out'])) { | ||
$convertFuncName = self::CONVERSIONS[$key]['out']; | ||
$value = $this->$convertFuncName($value); | ||
} | ||
$template->assign($key, $value); | ||
} | ||
return $template; | ||
} | ||
|
||
/** | ||
* Convert the days to seconds | ||
* @param int $days | ||
* @return int the number of seconds | ||
*/ | ||
private function daysToSeconds($days) { | ||
return $days * 24 * 60 * 60; | ||
} | ||
|
||
/** | ||
* Convert seconds to days. The value will always be rounded up, | ||
* so 1 second will be converted to 1 day | ||
* @param int $seconds the number of seconds to be converted | ||
* @return int the number of days in those seconds, rounded up | ||
*/ | ||
private function secondsToDays($seconds) { | ||
$floatDays = $seconds / (24 * 60 * 60); | ||
return \intval(\ceil($floatDays)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. not sure if using so this means if someone uses occ to set a seconds value in the DB, the UI would show it as the higher value for days. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The UI is only displaying integer number of days. So we have to do something here. When testing, I will understand that it will show 1 day, but underneath I actually set something like 300 seconds only. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure either. Maybe we should take the restrictive approach and show 0 days. This will send notifications before the expected time. The good thing is that it's just a visualization problem in the UI if you set the value via occ. The notifications will be sent accordingly to the value stored in the DB (in seconds) |
||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -63,4 +63,39 @@ public function getLatestPassword($uid) { | |
} | ||
return $passwords[0]; | ||
} | ||
} | ||
|
||
/** | ||
* Get the passwords that are about to expire or already expired. | ||
* Last passwords which have been changed before the timestamp are the ones | ||
* selectable. Previous stored passwords won't be included | ||
* In addition, passwords from multiple users are expected | ||
* @param int $maxTimestamp timestamp marker, last passwords changed before | ||
* the timestamp will be selected | ||
* @return OldPassword[] the selected passwords | ||
*/ | ||
public function getPasswordsAboutToExpire($maxTimestamp) { | ||
$oldPasswords = []; | ||
|
||
$query = "SELECT `f`.`id`, `f`.`uid`, `f`.`password`, `f`.`change_time` FROM ("; | ||
$query .= "SELECT `uid`, max(`change_time`) AS `maxtime` FROM `*PREFIX*user_password_history` GROUP BY `uid`"; | ||
$query .= ") AS `x` INNER JOIN `*PREFIX*user_password_history` AS `f` ON `f`.`uid` = `x`.`uid` AND `f`.`change_time` = `x`.`maxtime`"; | ||
$query .= " WHERE `f`.`change_time` < ?"; | ||
|
||
$stmt = $this->db->prepare($query); | ||
$stmt->bindValue(1, $maxTimestamp); | ||
$result = $stmt->execute(); | ||
|
||
if ($result === false) { | ||
$info = \json_encode($stmt->erroInfo()); | ||
$message = "Cannot get the passwords that are about to expire. Error: {$info}"; | ||
\OCP\Util::writeLog('password_policy', $message, \OCP\Util::ERROR); | ||
return false; | ||
} | ||
|
||
while ($row = $stmt->fetch()) { | ||
$oldPasswords[] = OldPassword::fromRow($row); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @butonic is it ok scalability-wise ? or should we LIMIT and rely on rerunning the job more often, or use yield ? I imagine this could happen if an admin runs the occ command and decides that ALL users of the system need to change their password, so this would return all user entries here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jvillafanez the subselect is what you actually need, right? In the result set you want the columns, necessary to build the entitiy, which is the outer select. You do a join on uid AND the change time to the subselect. Here you could use the id to match the exact password. However, the subselect already selects all the information you want. No need to join that to the table to get the other columns: mysql> SELECT * FROM oc_user_password_history;
+-----+-------+----------------------------------------------------------------+-------------+
| id | uid | password | change_time |
+-----+-------+----------------------------------------------------------------+-------------+
| 1 | admin | 1|$2y$10$iJcpqCvfrm7RN/EBd4NPIOQrZvVr76RBrBI4DZm7pHSMVzs51/jXC | 1528802661 |
| 2 | admin | 1|$2y$10$K3v18hQGc2QppEAUwfIlVeT3v5I0WpY.Q5GG2GtiGg20r0pyd0fmS | 1529010840 |
| 3 | admin | 1|$2y$10$G5JjxOGGXT1PdcmFNf.p.evOIu.64dTeDOPEdTdRSTSSeuXI3ntkS | 1529010886 |
| 4 | admin | 1|$2y$10$SuAmaBeyA0LBXji54ekG.eMHywoqP1h573VbsYgwTndCMqN6aTYcS | 1529012884 |
| 5 | admin | 1|$2y$10$gKn4dZ60ALZU8DKFLzxCgeMIop5I.G6/.4PSzYZcY2Pb3vA57y4NS | 1529012941 |
| 6 | admin | 1|$2y$10$.5LFRjes2A0O061MRWxSGuzqbpOfqWS7EnlP45jKC8.LNYg1tymBq | 1529013064 |
| 7 | admin | 1|$2y$10$NVaealeCQTdfv5aU68RQ.OVbEQKUfJwlln0Mfp8Ac03p7iHqQtY06 | 1529013338 |
| 8 | admin | 1|$2y$10$SaFlqZSrolZs4wqGd7j/AOWvIBBVM8SfLfVCzpqg3ZfRdx55/Jqh2 | 1529013408 |
| 9 | admin | 1|$2y$10$cx/UbEsFFj0YQ3ICajrcPeCXum8AGwkiz08pf.ZSB1/wnQSH19WZS | 1529408997 |
| 10 | admin | 1|$2y$10$YdLFzYh36M12OQ6VllkwCeLePRQrXn/PJNl77OTHACz4BAIazXnNO | 1529409291 |
| 11 | admin | 1|$2y$10$n64oJTgbkDDuQtEKVw4/yOREctvjP1qreD5zPwR7bU.M.HV37I0gG | 1529409359 |
| 12 | admin | 1|$2y$10$rXQ5h9nSPUg9UVPhiLmPaOhkYxvX.YEU9/UIw5CDA.vgEcUeAfuYW | 1529409618 |
| 13 | admin | 1|$2y$10$H7q2D2JxgZ8CuuPqyBMqIOCxbwKl.8gSiE2vl.B5lFBDQSJ8Mcbva | 1529409667 |
| 14 | admin | 1|$2y$10$6bQRZnS0ezfJWsLKmbPX2.eyGiL6PM6d147kjMYnhSMvY6FG5oqj6 | 1529409986 |
| 15 | admin | 1|$2y$10$WaIcpl4Fm2BjxzEZx3.nvuG.vIvKkrUg0Vbu4M34ZpVTYc/AKAuNq | 1529410455 |
| 16 | admin | 1|$2y$10$awvxbG5wL54Zg6sYGgFpveskJ/bsjlPJVluKOtMe5r6/IgDHH3P2e | 1529410477 |
| 17 | admin | 1|$2y$10$UBEo7SutLVoQUQGysRk5seYvt8YUcl0M8Z3lP10m1KvuTtTDMT8i2 | 1529437059 |
| 18 | admin | 1|$2y$10$TWYrQpBkWMVJN45MmUi1KOaD.9gsRTUlQoYeaBpXdB/Xkuzb9aEli | 1529672819 |
| 19 | admin | 1|$2y$10$8QkkU1ZDwak0Ih2dIyTXW.1RmgwdYf9Es9bTD2UQuODAUpDc9xUTe | 1529672848 |
| 20 | admin | 1|$2y$10$Co8qKhNqf.ww9.BRzHbX0uh7E4p8XkPrctWZNxdMg3OSX4afImIQm | 1529673004 |
| 21 | admin | 1|$2y$10$l/bvtHoAzfgHX9O7VIsJiu1nUhQAd7y1WhAWZgpUm42fcDzYKtlne | 1529673041 |
| 22 | admin | 1|$2y$10$l1ToXrY7MgD2KkiZVDjYce.Q14m2vkWx7q1KlvloE4W3oZGlGN.t2 | 1529674775 |
| 23 | admin | 1|$2y$10$03WKH1TKscnOiqCoOTJoqOujnfJFlFSeWgObtSQj2nCh9DO99gfKK | 1529677007 |
| 24 | admin | 1|$2y$10$L/MDFrjGMnPm4nOiZ7mvuephhVUL1pvn1KsnMCPo3XwmCukB40R.K | 1529919562 |
| 25 | admin | 1|$2y$10$WRVgklX06FPQ9.gNGUTiLuMyTlinMw73DJZLqx3LJ2LlSBXB4WvvG | 1529919654 |
| 26 | admin | 1|$2y$10$VHt.R.lZX0PdYR86kJre.OgJCyaLXOgNjHAex6xXW/wKkOgRjnffK | 1529922722 |
| 27 | admin | 1|$2y$10$fXYr17rsqyztzu6HokqLpua/nm9fNf5mPZotAYKPLpvB2uU5C8gcq | 1529922783 |
| 28 | admin | 1|$2y$10$qR7w7vzae4XnZmWXOEhvEeGFqr.e7S6pKP0eEH4NCNHJHKiNwal4q | 1529928109 |
| 29 | admin | 1|$2y$10$x/be52NbGUdyqk2RtKihxOwr7QszeWVGHjRHcteiFwtNSRvXLH27K | 1529928317 |
| 30 | admin | 1|$2y$10$0/1NgwaFYkz0byBDP6FRauSesDuaa39KR.QlbbjXmDHX/LTDaHJU2 | 1529929051 |
| 31 | admin | 1|$2y$10$ncy2nAfKqopDD7xfL2xzQutajq1ULz7U7llKoYJA7f5kpJiyfISF6 | 1529929158 |
| 32 | admin | 1|$2y$10$Kojhu9ogf71pDODVoZAcwufBVAYYfZ0rskzTKGt.7u.0TcLKItn1K | 1529929495 |
| 33 | admin | 1|$2y$10$mQGjUuW52Si1SFpj8FaXfO4xFL6LJCLjrQm2WSahG2db0qw7vpjm. | 1529934553 |
| 34 | admin | 1|$2y$10$OFiETck8EABckSgbyKl6r.eOeWdd8WvHRR8OwEyVePN0ww9Gmer2m | 1530000377 |
| 35 | admin | 1|$2y$10$e8wPr0e.5qgv.rQ5BzKhRe8p5ErCe2kCIxWLhjsPcU3CzsBVVqs9i | 1530000452 |
| 36 | admin | 1|$2y$10$83uDOakjvWOhcBtv1QqnZu19AmMVSAd8Z7vHQ8yB326qBGdSS/Kty | 1530016008 |
| 37 | admin | 1|$2y$10$Igv7hYqcnNeBLASfr4UAxeYc1.5N/DJYqkHye/WBqLKVTcBIJhN6m | 1530019248 |
| 38 | admin | 1|$2y$10$ivCQvEIH2NS7KT.3nStpR.maBNcCMdOrPFWHvZqlzzUUr6Dk16of. | 1530087458 |
| 39 | admin | 1|$2y$10$pPNexxPypESD5MUobKKx..q4pbNzkhDSrb04fKvu29pJTb5cYaWtu | 1530087496 |
| 40 | admin | 1|$2y$10$wMAebL07zo3jfhY1g9f5KumVygs5HVIwnnYLoxTOilarc0oAO8GGS | 1530088064 |
| 215 | admin | 1|$2y$10$9DvIrgMhp16vS8PIIAiEL.9FgV03auKPWYzdnkLJ7ti0yyvo22SWG | 1530267611 |
| 216 | test | 1|$2y$10$zfc50VR1AvRoAagYA/v94egSY9S1tmDzmK.stUWjP86ENem3KtO3G | 1530611134 |
| 217 | admin | 1|$2y$10$lCNQKQGGrfyag2Nc53w92eH/kuQEVxYX66RphyvoByE.nll.B0j6q | 1531385660 |
+-----+-------+----------------------------------------------------------------+-------------+
43 rows in set (0.00 sec)
mysql> SELECT `f`.`id`, `f`.`uid`, `f`.`password`, `f`.`change_time` FROM ( SELECT `uid`, max(`change_time`) AS `maxtime` FROM `oc_user_password_history` GROUP BY `uid` ) AS `x` INNER JOIN `oc_user_password_history` AS `f` ON `f`.`uid` = `x`.`uid` AND `f`.`change_time` = `x`.`maxtime` WHERE `f`.`change_time` < 1540019248;
+-----+-------+----------------------------------------------------------------+-------------+
| id | uid | password | change_time |
+-----+-------+----------------------------------------------------------------+-------------+
| 217 | admin | 1|$2y$10$lCNQKQGGrfyag2Nc53w92eH/kuQEVxYX66RphyvoByE.nll.B0j6q | 1531385660 |
| 216 | test | 1|$2y$10$zfc50VR1AvRoAagYA/v94egSY9S1tmDzmK.stUWjP86ENem3KtO3G | 1530611134 |
+-----+-------+----------------------------------------------------------------+-------------+
2 rows in set (0.00 sec)
mysql> SELECT `id`, `uid`, `password`, max(`change_time`) AS `change_time` FROM `oc_user_password_history` WHERE `change_time` < 1540019248 GROUP BY `uid`;
+-----+-------+----------------------------------------------------------------+-------------+
| id | uid | password | change_time |
+-----+-------+----------------------------------------------------------------+-------------+
| 1 | admin | 1|$2y$10$iJcpqCvfrm7RN/EBd4NPIOQrZvVr76RBrBI4DZm7pHSMVzs51/jXC | 1531385660 |
| 216 | test | 1|$2y$10$zfc50VR1AvRoAagYA/v94egSY9S1tmDzmK.stUWjP86ENem3KtO3G | 1530611134 |
+-----+-------+----------------------------------------------------------------+-------------+
2 rows in set (0.00 sec) Can you double check if I misunderstand the query you built? You should be able to get the same results with this. The indexes that need to be used will vary, depending on the number of passwords per user in the table.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I expect something similar with postgresql. Note that I also expect to get valid data, specially when we are going to get objects from there. The last query doesn't return a valid existing object, although the information we need from it is correct. I'd prefer to be strict with this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see. Then move the mtime comparison into the subselect. Hm or we do a left join to get rid of the derived table: mysql> SET SESSION sql_mode='ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION';
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT p.* FROM `oc_user_password_history` p LEFT OUTER JOIN `oc_user_password_history` p2 ON p.uid = p2.uid AND p.change_time < p2.change_time WHERE p2.id IS NULL;
+-----+-------+----------------------------------------------------------------+-------------+
| id | uid | password | change_time |
+-----+-------+----------------------------------------------------------------+-------------+
| 216 | test | 1|$2y$10$zfc50VR1AvRoAagYA/v94egSY9S1tmDzmK.stUWjP86ENem3KtO3G | 1530611134 |
| 217 | admin | 1|$2y$10$lCNQKQGGrfyag2Nc53w92eH/kuQEVxYX66RphyvoByE.nll.B0j6q | 1531385660 |
+-----+-------+----------------------------------------------------------------+-------------+
2 rows in set (0.00 sec)
mysql> EXPLAIN SELECT p.* FROM `oc_user_password_history` p LEFT JOIN `oc_user_password_history` p2 ON p.uid = p2.uid AND p.change_time < p2.change_time WHERE p2.id IS NULL;
+----+-------------+-------+------+-------------------------------------------------------------------+--------------------+---------+-------------+------+--------------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+-------------------------------------------------------------------+--------------------+---------+-------------+------+--------------------------------------+
| 1 | SIMPLE | p | ALL | NULL | NULL | NULL | NULL | 43 | |
| 1 | SIMPLE | p2 | ref | pp_uid_index,pp_mtime_index,pp_uid_mtime_index,pp_mtime_uid_index | pp_uid_mtime_index | 258 | core1.p.uid | 1 | Using where; Using index; Not exists |
+----+-------------+-------+------+-------------------------------------------------------------------+--------------------+---------+-------------+------+--------------------------------------+
2 rows in set (0.00 sec) As you can see I added another index There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hm ... it really depends on the dbms which query is more efficient. go with the current code. In any case this wont be a performance issue. |
||
} | ||
$stmt->closeCursor(); | ||
return $oldPasswords; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this array is empty ? |
||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the reason for this
if
condition obvious? I could use a comment, or some way to understand what is the script+path pattern aimed for and why.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
copy-pasted from notifications app: https://github.com/owncloud/notifications/blob/master/appinfo/app.php#L29
I'll add a comment