diff --git a/lam/HISTORY b/lam/HISTORY
index 033852ef..84599e41 100644
--- a/lam/HISTORY
+++ b/lam/HISTORY
@@ -4,6 +4,8 @@ September 2020
- Show password prompt when a user with expired password logs into LAM admin interface (requires PHP 7.2)
- Better error messages on login when account is expired/deactivated/...
- Windows users: group display format can be configured (cn/dn)
+ - LAM Pro:
+ -> Windows: new cron job to send users a summary of their managed groups
01.05.2020 7.2
- Unix: allow to create group with same name during user creation
diff --git a/lam/docs/manual-sources/chapter-configuration.xml b/lam/docs/manual-sources/chapter-configuration.xml
index 6b89e4cf..d8a43229 100644
--- a/lam/docs/manual-sources/chapter-configuration.xml
+++ b/lam/docs/manual-sources/chapter-configuration.xml
@@ -1141,6 +1141,11 @@ mysql> GRANT ALL PRIVILEGES ON lam_cron.* TO 'lam_cron'@'localhost';
move expired accounts
+
+ Windows: Notify
+ users about their managed groups
+
+
FreeRadius:
Delete or move expired accounts
@@ -1829,6 +1834,95 @@ mysql> GRANT ALL PRIVILEGES ON lam_cron.* TO 'lam_cron'@'localhost';
+
+ Windows: Notify users about their managed groups
+
+ This will send your users an email with the groups they
+ manage. This also includes a list of users in these groups. The
+ users and groups are searched using the user+group account types
+ that are specified in server profile.
+
+ You need to activate the Windows module for users to be able
+ to add this job. The job can be added multiple times.
+
+
+
+
+
+
+ Options
+
+
+
+
+ Option
+
+ Description
+
+
+
+ From address
+
+ The email address to set as FROM.
+
+
+
+ Reply-to address
+
+ Optional Reply-to address for email.
+
+
+
+ CC address
+
+ Optional CC mail address.
+
+
+
+ BCC address
+
+ Optional BCC mail address.
+
+
+
+ Subject
+
+ The email subject line. Supports wildcards, see
+ below.
+
+
+
+ HTML format
+
+ Send email as HTML instead of plain text.
+
+
+
+ Text
+
+ The email body text. Supports wildcards, see
+ below.
+
+
+
+ Period
+
+ Defines how often the mail is sent (e.g.
+ quarterly).
+
+
+
+
Wildcards:
+
+ You can enter LDAP attributes as wildcards in the form
+ @@ATTRIBUTE_NAME@@. E.g. to add the user's common name use "@@cn@@".
+ For the common name it would be "@@cn@@".
+
+ Use the wildcard "@@LAM_MANAGED_GROUPS@@" to insert the group
+ listing. This wildcard is mandatory.
+
+
FreeRadius: Delete or move expired accounts
diff --git a/lam/docs/manual-sources/images/jobs_windowsNotifyGroups.png b/lam/docs/manual-sources/images/jobs_windowsNotifyGroups.png
new file mode 100644
index 00000000..9f635975
Binary files /dev/null and b/lam/docs/manual-sources/images/jobs_windowsNotifyGroups.png differ
diff --git a/lam/help/help.inc b/lam/help/help.inc
index 8c6162d5..955cad60 100644
--- a/lam/help/help.inc
+++ b/lam/help/help.inc
@@ -430,6 +430,16 @@ $helpArray = array (
"Headline" => _('Target DN'),
"Text" => _('The expired accounts will be moved to this DN.')
),
+ '810' => array(
+ "Headline" => _('Text'),
+ "Text" => _('The mail text of all mails.') .
+ _('You can use wildcards for LDAP attributes in the form @@attribute@@ (e.g. @@uid@@ for the user name).')
+ . ' ' . _('The managed groups need to be added with @@LAM_MANAGED_GROUPS@@.')
+ ),
+ '811' => array(
+ "Headline" => _('Period'),
+ "Text" => _('This defines how often the email is sent (e.g. each month).')
+ ),
);
/* This is a sample help entry. Just copy this line an modify the values between the [] brackets.
diff --git a/lam/lib/modules/windowsUser.inc b/lam/lib/modules/windowsUser.inc
index 353bc200..ade505f0 100644
--- a/lam/lib/modules/windowsUser.inc
+++ b/lam/lib/modules/windowsUser.inc
@@ -3915,7 +3915,8 @@ class windowsUser extends baseModule implements passwordService {
return array(
new WindowsPasswordNotifyJob(),
new WindowsAccountExpirationCleanupJob(),
- new WindowsAccountExpirationNotifyJob()
+ new WindowsAccountExpirationNotifyJob(),
+ new WindowsManagedGroupsNotifyJob()
);
}
@@ -4147,6 +4148,299 @@ if (interface_exists('\LAM\JOB\Job', false)) {
}
+ /**
+ * Job to notify users about their managed groups.
+ *
+ * @package jobs
+ */
+ class WindowsManagedGroupsNotifyJob extends \LAM\JOB\PasswordExpirationJob {
+
+ const MANAGED_GROUPS = 'LAM_MANAGED_GROUPS';
+ const PERIOD_MONTHLY = 'MONTHLY';
+ const PERIOD_QUARTERLY = 'QUARTERLY';
+ const PERIOD_HALF_YEARLY = 'HALF_YEARLY';
+ const PERIOD_YEARLY = 'YEARLY';
+
+ /**
+ * Returns the alias name of the job.
+ *
+ * @return String name
+ */
+ public function getAlias() {
+ return _('Windows') . ': ' . _('Notify users about their managed groups');
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getDescription() {
+ return _('This will send each user a summary of the managed groups and their members.');
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getConfigOptions($jobID) {
+ $prefix = $this->getConfigPrefix();
+ $container = new htmlResponsiveRow();
+ $container->add(new htmlResponsiveInputField(_('From address'), $prefix . '_mailFrom' . $jobID, null, '800', true), 12);
+ $container->add(new htmlResponsiveInputField(_('Reply-to address'), $prefix . '_mailReplyTo' . $jobID, null, '801'), 12);
+ $container->add(new htmlResponsiveInputField(_('CC address'), $prefix . '_mailCC' . $jobID, null, '805'), 12);
+ $container->add(new htmlResponsiveInputField(_('BCC address'), $prefix . '_mailBCC' . $jobID, null, '806'), 12);
+ $container->add(new htmlResponsiveInputField(_('Subject'), $prefix . '_mailSubject' . $jobID, null, '802'), 12);
+ $container->add(new htmlResponsiveInputCheckbox($prefix . '_mailIsHTML' . $jobID, false, _('HTML format'), '553'), 12);
+ $container->add(new htmlResponsiveInputTextarea($prefix . '_mailtext' . $jobID, '', 50, 4, _('Text'), '810'), 12);
+ $periodOptions = array(
+ _('Monthly') => self::PERIOD_MONTHLY,
+ _('Quarterly') => self::PERIOD_QUARTERLY,
+ _('Half-yearly') => self::PERIOD_HALF_YEARLY,
+ _('Yearly') => self::PERIOD_YEARLY,
+ );
+ $periodSelect = new htmlResponsiveSelect($prefix . '_period' . $jobID, $periodOptions, array(), _('Period'), '811');
+ $periodSelect->setHasDescriptiveElements(true);
+ $periodSelect->setSortElements(false);
+ $container->add($periodSelect, 12);
+ return $container;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function checkConfigOptions($jobID, $options) {
+ $prefix = $this->getConfigPrefix();
+ $errors = array();
+ // from address
+ if (empty($options[$prefix . '_mailFrom' . $jobID][0])
+ || !(get_preg($options[$prefix . '_mailFrom' . $jobID][0], 'email')
+ || get_preg($options[$prefix . '_mailFrom' . $jobID][0], 'emailWithName'))) {
+ $errors[] = array('ERROR', _('Please enter a valid email address!'), _('From address'));
+ }
+ // reply-to
+ if (!empty($options[$prefix . '_mailReplyTo' . $jobID][0])
+ && !get_preg($options[$prefix . '_mailReplyTo' . $jobID][0], 'email')
+ && !get_preg($options[$prefix . '_mailReplyTo' . $jobID][0], 'emailWithName')) {
+ $errors[] = array('ERROR', _('Please enter a valid email address!'), _('Reply-to address'));
+ }
+ // CC address
+ if (!empty($options[$prefix . '_mailCC' . $jobID][0])
+ && !get_preg($options[$prefix . '_mailCC' . $jobID][0], 'email')
+ && !get_preg($options[$prefix . '_mailCC' . $jobID][0], 'emailWithName')) {
+ $errors[] = array('ERROR', _('Please enter a valid email address!'), _('CC address'));
+ }
+ // BCC address
+ if (!empty($options[$prefix . '_mailBCC' . $jobID][0])
+ && !get_preg($options[$prefix . '_mailBCC' . $jobID][0], 'email')
+ && !get_preg($options[$prefix . '_mailBCC' . $jobID][0], 'emailWithName')) {
+ $errors[] = array('ERROR', _('Please enter a valid email address!'), _('BCC address'));
+ }
+ // text
+ $mailText = implode('', $options[$prefix . '_mailtext' . $jobID]);
+ if (empty($mailText)) {
+ $errors[] = array('ERROR', _('Please set a email text.'));
+ }
+ if (strpos($mailText, '@@' . self::MANAGED_GROUPS . '@@') === false) {
+ $errors[] = array('ERROR', _('Please add the wildcard for the list of managed groups.'), '@@' . self::MANAGED_GROUPS . '@@');
+ }
+ return $errors;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ protected function getPolicyOptions() {
+ return array();
+ }
+
+ /**
+ * Searches for users in LDAP.
+ *
+ * @param String $jobID unique job identifier
+ * @param array $options config options (name => value)
+ * @return array list of user attributes
+ */
+ protected function findUsers($jobID, $options) {
+ // read users
+ $sysAttrs = array('managedObjects', 'mail');
+ $attrs = $this->getAttrWildcards($jobID, $options);
+ $attrs = array_values(array_unique(array_merge($attrs, $sysAttrs)));
+ $users = searchLDAPByFilter('(&(mail=*)(managedObjects=*))', $attrs, array('user'));
+ $groups = searchLDAPByFilter('(managedBy=*)', array('cn', 'member'), array('group'));
+ $groupByDn = array();
+ foreach ($groups as $group) {
+ $groupByDn[$group['dn']] = $group;
+ }
+ $groups = null;
+ foreach ($users as $index => $user) {
+ $managedObjectDns = $user['managedobjects'];
+ $managedGroups = array();
+ foreach ($managedObjectDns as $managedObjectDn) {
+ if (array_key_exists($managedObjectDn, $groupByDn)) {
+ $managedGroups[] = $groupByDn[$managedObjectDn];
+ }
+ }
+ $users[$index][strtolower(self::MANAGED_GROUPS)] = $managedGroups;
+ }
+ return $users;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function execute($jobID, $options, &$pdo, $isDryRun, &$resultLog) {
+ $this->jobResultLog = &$resultLog;
+ $this->jobResultLog->logDebug("Configuration options:");
+ foreach ($options as $key => $value) {
+ if (strpos($key, $jobID) === false) {
+ continue;
+ }
+ $this->jobResultLog->logDebug($key . ': ' . implode(', ', $value));
+ }
+ $now = new DateTime(null, getTimeZone());
+ $baseDate = $this->getBaseDate($now);
+ $monthInterval = $this->getMonthInterval($options, $jobID);
+ if (!$this->shouldRun($pdo, $options, $jobID, $baseDate, $monthInterval)) {
+ $this->jobResultLog->logDebug('No run needed yet');
+ return;
+ }
+ $userResults = $this->findUsers($jobID, $options);
+ $this->jobResultLog->logDebug("Found " . sizeof($userResults) . " users to send an email.");
+ $isHTML = (!empty($options[$this->getConfigPrefix() . '_mailIsHTML' . $jobID][0]) && ($options[$this->getConfigPrefix() . '_mailIsHTML' . $jobID][0] == 'true'));
+ foreach ($userResults as $user) {
+ if (empty($user[strtolower(self::MANAGED_GROUPS)])) {
+ continue;
+ }
+ $user[strtolower(self::MANAGED_GROUPS)][0] = $this->formatGroups($user[strtolower(self::MANAGED_GROUPS)], $isHTML);
+ if ($isDryRun) {
+ // no action for dry run
+ $this->jobResultLog->logInfo("Managed groups text for " . $user['dn'] . ":\n" . $user[strtolower(self::MANAGED_GROUPS)][0]);
+ $this->jobResultLog->logInfo('Not sending email to ' . $user['dn'] . ' because of dry run.');
+ continue;
+ }
+ // send email
+ $this->sendMail($options, $jobID, $user, null);
+ }
+ if (!$isDryRun) {
+ $this->setDBLastPwdChangeTime($jobID, $pdo, $jobID, self::getLastEffectiveExecutionDate($baseDate, $monthInterval, $this->jobResultLog)->format('Y-m-d'));
+ }
+ }
+
+ /**
+ * Returns if the job should run.
+ *
+ * @param $pdo PDO
+ * @param $options job options
+ * @param $jobId job id
+ * @param DateTime $baseDate base date
+ * @param int $monthInterval month interval
+ * @return bool should run
+ */
+ private function shouldRun(&$pdo, $options, $jobId, $baseDate, $monthInterval) {
+ $dbLastChange = $this->getDBLastPwdChangeTime($jobId, $pdo, $jobId);
+ if (empty($dbLastChange)) {
+ return true;
+ }
+ $this->jobResultLog->logDebug('Base date: ' . $baseDate->format('Y-m-d'));
+ $effectiveDate = self::getLastEffectiveExecutionDate($baseDate, $monthInterval, $this->jobResultLog);
+ $dbLastChangeDate = DateTime::createFromFormat('Y-m-d', $dbLastChange, getTimeZone());
+ $this->jobResultLog->logDebug('Last run date: ' . $dbLastChangeDate->format('Y-m-d'));
+ return $effectiveDate > $dbLastChangeDate;
+ }
+
+ /**
+ * Returns the month interval.
+ *
+ * @param arry $options config options
+ * @param $jobId job id
+ * @return int interval
+ */
+ private function getMonthInterval($options, $jobId) {
+ $monthInterval = 12;
+ switch ($options[$this->getConfigPrefix() . '_period' . $jobId][0]) {
+ case self::PERIOD_HALF_YEARLY:
+ $monthInterval = 6;
+ break;
+ case self::PERIOD_QUARTERLY:
+ $monthInterval = 3;
+ break;
+ case self::PERIOD_MONTHLY:
+ $monthInterval = 1;
+ break;
+ }
+ return $monthInterval;
+ }
+
+ /**
+ * Returns the base date (first of month) for the current date.
+ *
+ * @param DateTime $currentDate current date
+ * @return DateTime base date
+ */
+ private function getBaseDate($currentDate) {
+ $baseDateText = $currentDate->format('Y-m-') . '1';
+ return DateTime::createFromFormat('Y-m-d', $baseDateText, getTimeZone());
+ }
+
+ /**
+ * Returns the last effective execution date.
+ *
+ * @param DateTime $baseDate base date
+ * @param int $monthInterval number of months in interval
+ * @param \LAM\JOB\JobResultLog $resultLog result log
+ */
+ public static function getLastEffectiveExecutionDate($baseDate, $monthInterval, $resultLog) {
+ $month = $baseDate->format('m');
+ $monthIndex = $month - 1;
+ while (($monthIndex % $monthInterval) !== 0) {
+ $monthIndex--;
+ }
+ $month = $monthIndex + 1;
+ $effectiveDateString = $baseDate->format('Y-') . $month . '-1';
+ $effectiveDate = DateTime::createFromFormat('Y-m-d', $effectiveDateString, getTimeZone());
+ $resultLog->logDebug("Effective date: " . $effectiveDate->format('Y-m-d'));
+ return $effectiveDate;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ protected function checkSingleUser($jobID, $options, &$pdo, $now, $policyOptions, $user, $isDryRun) {
+ // not used
+ }
+
+ /**
+ * Formats the managed groups.
+ *
+ * @param $managedGroups managed groups
+ * @param bool $isHTML HTML email
+ * @return string formatted text
+ */
+ private function formatGroups($managedGroups, bool $isHTML) {
+ $text = '';
+ foreach ($managedGroups as $managedGroup) {
+ if ($isHTML) {
+ $text .= ' ' . $managedGroup['cn'][0] . ' ';
+ }
+ else {
+ $text .= "\r\n" . $managedGroup['cn'][0] . "\r\n\r\n";
+ }
+ if (empty($managedGroup['member'])) {
+ continue;
+ }
+ foreach ($managedGroup['member'] as $member) {
+ $member = getAbstractDN($member);
+ if ($isHTML) {
+ $text .= ' ' . $member . ' ';
+ }
+ else {
+ $text .= " " . $member . "\r\n";
+ }
+ }
+ }
+ return $text;
+ }
+
+ }
+
/**
* Job to notify users about account expiration.
*
diff --git a/lam/tests/lib/modules/windowsUserTest.php b/lam/tests/lib/modules/windowsUserTest.php
index e5534204..4d109ae3 100644
--- a/lam/tests/lib/modules/windowsUserTest.php
+++ b/lam/tests/lib/modules/windowsUserTest.php
@@ -21,9 +21,9 @@ use PHPUnit\Framework\TestCase;
*/
- include_once 'lam/lib/baseModule.inc';
- include_once 'lam/lib/modules.inc';
- include_once 'lam/lib/modules/windowsUser.inc';
+ include_once __DIR__ . '/../../../lib/baseModule.inc';
+ include_once __DIR__ . '/../../../lib/modules.inc';
+ include_once __DIR__ . '/../../../lib/modules/windowsUser.inc';
/**
* Checks the windowsUser class.
@@ -78,6 +78,33 @@ use PHPUnit\Framework\TestCase;
return $seconds . '0000000';
}
+ public function testWindowsManagedGroupsNotifyJob_getLastEffectiveExecutionDate() {
+ if (!interface_exists('\LAM\JOB\Job', false)) {
+ return;
+ }
+ $resultLog = new \LAM\JOB\JobResultLog();
+ $baseDate = DateTime::createFromFormat('Y-m-d', '2020-08-21', getTimeZone());
+ $this->assertEquals('2020-01-01', WindowsManagedGroupsNotifyJob::getLastEffectiveExecutionDate($baseDate, 12, $resultLog)->format('Y-m-d'));
+ $this->assertEquals('2020-07-01', WindowsManagedGroupsNotifyJob::getLastEffectiveExecutionDate($baseDate, 6, $resultLog)->format('Y-m-d'));
+ $this->assertEquals('2020-07-01', WindowsManagedGroupsNotifyJob::getLastEffectiveExecutionDate($baseDate, 3, $resultLog)->format('Y-m-d'));
+ $this->assertEquals('2020-08-01', WindowsManagedGroupsNotifyJob::getLastEffectiveExecutionDate($baseDate, 1, $resultLog)->format('Y-m-d'));
+ $baseDate = DateTime::createFromFormat('Y-m-d', '2020-12-31', getTimeZone());
+ $this->assertEquals('2020-01-01', WindowsManagedGroupsNotifyJob::getLastEffectiveExecutionDate($baseDate, 12, $resultLog)->format('Y-m-d'));
+ $this->assertEquals('2020-07-01', WindowsManagedGroupsNotifyJob::getLastEffectiveExecutionDate($baseDate, 6, $resultLog)->format('Y-m-d'));
+ $this->assertEquals('2020-10-01', WindowsManagedGroupsNotifyJob::getLastEffectiveExecutionDate($baseDate, 3, $resultLog)->format('Y-m-d'));
+ $this->assertEquals('2020-12-01', WindowsManagedGroupsNotifyJob::getLastEffectiveExecutionDate($baseDate, 1, $resultLog)->format('Y-m-d'));
+ $baseDate = DateTime::createFromFormat('Y-m-d', '2020-01-01', getTimeZone());
+ $this->assertEquals('2020-01-01', WindowsManagedGroupsNotifyJob::getLastEffectiveExecutionDate($baseDate, 12, $resultLog)->format('Y-m-d'));
+ $this->assertEquals('2020-01-01', WindowsManagedGroupsNotifyJob::getLastEffectiveExecutionDate($baseDate, 6, $resultLog)->format('Y-m-d'));
+ $this->assertEquals('2020-01-01', WindowsManagedGroupsNotifyJob::getLastEffectiveExecutionDate($baseDate, 3, $resultLog)->format('Y-m-d'));
+ $this->assertEquals('2020-01-01', WindowsManagedGroupsNotifyJob::getLastEffectiveExecutionDate($baseDate, 1, $resultLog)->format('Y-m-d'));
+ $baseDate = DateTime::createFromFormat('Y-m-d', '2020-06-05', getTimeZone());
+ $this->assertEquals('2020-01-01', WindowsManagedGroupsNotifyJob::getLastEffectiveExecutionDate($baseDate, 12, $resultLog)->format('Y-m-d'));
+ $this->assertEquals('2020-01-01', WindowsManagedGroupsNotifyJob::getLastEffectiveExecutionDate($baseDate, 6, $resultLog)->format('Y-m-d'));
+ $this->assertEquals('2020-04-01', WindowsManagedGroupsNotifyJob::getLastEffectiveExecutionDate($baseDate, 3, $resultLog)->format('Y-m-d'));
+ $this->assertEquals('2020-06-01', WindowsManagedGroupsNotifyJob::getLastEffectiveExecutionDate($baseDate, 1, $resultLog)->format('Y-m-d'));
+ }
+
}
?>