Browse Source

added job to send users group summary

pull/102/head
Roland Gruber 3 years ago
parent
commit
2698995cc2
  1. 2
      lam/HISTORY
  2. 94
      lam/docs/manual-sources/chapter-configuration.xml
  3. BIN
      lam/docs/manual-sources/images/jobs_windowsNotifyGroups.png
  4. 10
      lam/help/help.inc
  5. 296
      lam/lib/modules/windowsUser.inc
  6. 33
      lam/tests/lib/modules/windowsUserTest.php

2
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

94
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</link></para>
</listitem>
<listitem>
<para><link linkend="job_windows_notify_groups">Windows: Notify
users about their managed groups</link></para>
</listitem>
<listitem>
<para><link linkend="job_freeradius_move_expired">FreeRadius:
Delete or move expired accounts</link></para>
@ -1829,6 +1834,95 @@ mysql&gt; GRANT ALL PRIVILEGES ON lam_cron.* TO 'lam_cron'@'localhost';
</table>
</section>
<section id="job_windows_notify_groups">
<title>Windows: Notify users about their managed groups</title>
<para>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.</para>
<para>You need to activate the Windows module for users to be able
to add this job. The job can be added multiple times.</para>
<screenshot>
<graphic fileref="images/jobs_windowsNotifyGroups.png"/>
</screenshot>
<para><table>
<title>Options</title>
<tgroup cols="2">
<tbody>
<row>
<entry><emphasis role="bold">Option</emphasis></entry>
<entry><emphasis
role="bold">Description</emphasis></entry>
</row>
<row>
<entry>From address</entry>
<entry>The email address to set as FROM.</entry>
</row>
<row>
<entry>Reply-to address</entry>
<entry>Optional Reply-to address for email.</entry>
</row>
<row>
<entry>CC address</entry>
<entry>Optional CC mail address.</entry>
</row>
<row>
<entry>BCC address</entry>
<entry>Optional BCC mail address.</entry>
</row>
<row>
<entry>Subject</entry>
<entry>The email subject line. Supports wildcards, see
below.</entry>
</row>
<row>
<entry>HTML format</entry>
<entry>Send email as HTML instead of plain text.</entry>
</row>
<row>
<entry>Text</entry>
<entry>The email body text. Supports wildcards, see
below.</entry>
</row>
<row>
<entry>Period</entry>
<entry>Defines how often the mail is sent (e.g.
quarterly).</entry>
</row>
</tbody>
</tgroup>
</table>Wildcards:</para>
<para>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@@".</para>
<para>Use the wildcard "@@LAM_MANAGED_GROUPS@@" to insert the group
listing. This wildcard is mandatory.</para>
</section>
<section id="job_freeradius_move_expired">
<title>FreeRadius: Delete or move expired accounts</title>

BIN
lam/docs/manual-sources/images/jobs_windowsNotifyGroups.png

After

Width: 1332  |  Height: 688  |  Size: 70 KiB

10
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.

296
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 .= '<br><b>' . $managedGroup['cn'][0] . '</b><br>';
}
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 .= '&nbsp;&nbsp;' . $member . '<br>';
}
else {
$text .= " " . $member . "\r\n";
}
}
}
return $text;
}
}
/**
* Job to notify users about account expiration.
*

33
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'));
}
}
?>
Loading…
Cancel
Save