Skip to content

Commit

Permalink
Merge pull request #375 from silinternational/feature/IDP-1221-sync-e…
Browse files Browse the repository at this point in the history
…rrors-notification-email

[IDP-1221] Optionally send external-groups sync-errors notification email
  • Loading branch information
forevermatt authored Oct 2, 2024
2 parents 692e2dd + aee1705 commit 3391eae
Show file tree
Hide file tree
Showing 12 changed files with 366 additions and 109 deletions.
58 changes: 52 additions & 6 deletions application/common/components/Emailer.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ class Emailer extends Component

public const SUBJ_ABANDONED_USER_ACCOUNTS = 'Unused {idpDisplayName} Identity Accounts';

public const SUBJ_EXT_GROUP_SYNC_ERRORS = "Errors while syncing '{appPrefix}' external-groups to the {idpDisplayName} IDP";

public const PROP_SUBJECT = 'subject';
public const PROP_TO_ADDRESS = 'to_address';
public const PROP_CC_ADDRESS = 'cc_address';
Expand Down Expand Up @@ -141,6 +143,8 @@ class Emailer extends Component

public $subjectForAbandonedUsers;

public $subjectForExtGroupSyncErrors;

/* The email to contact for HR notifications */
public $hrNotificationsEmail;

Expand Down Expand Up @@ -302,11 +306,14 @@ public function init()

$this->subjectForAbandonedUsers = $this->subjectForAbandonedUsers ?? self::SUBJ_ABANDONED_USER_ACCOUNTS;

$this->subjectForExtGroupSyncErrors = $this->subjectForExtGroupSyncErrors ?? self::SUBJ_EXT_GROUP_SYNC_ERRORS;

$this->subjects = [
EmailLog::MESSAGE_TYPE_INVITE => $this->subjectForInvite,
EmailLog::MESSAGE_TYPE_MFA_RATE_LIMIT => $this->subjectForMfaRateLimit,
EmailLog::MESSAGE_TYPE_PASSWORD_CHANGED => $this->subjectForPasswordChanged,
EmailLog::MESSAGE_TYPE_WELCOME => $this->subjectForWelcome,
EmailLog::MESSAGE_TYPE_EXT_GROUP_SYNC_ERRORS => $this->subjectForExtGroupSyncErrors,
EmailLog::MESSAGE_TYPE_GET_BACKUP_CODES => $this->subjectForGetBackupCodes,
EmailLog::MESSAGE_TYPE_REFRESH_BACKUP_CODES => $this->subjectForRefreshBackupCodes,
EmailLog::MESSAGE_TYPE_LOST_SECURITY_KEY => $this->subjectForLostSecurityKey,
Expand Down Expand Up @@ -338,15 +345,20 @@ public function init()
*
* @param string $messageType The message type. Must be one of the
* EmailLog::MESSAGE_TYPE_* values.
* @param User $user The intended recipient.
* @param ?User $user The intended recipient, if applicable. If not provided, a 'toAddress'
* must be in the $data parameter.
* @param string[] $data Data fields for email template. Include key 'toAddress' to override
* sending to primary address in User object.
* @param int $delaySeconds Number of seconds to delay sending the email. Default = no delay.
* @throws \Sil\EmailService\Client\EmailServiceClientException
*/
public function sendMessageTo(string $messageType, User $user, array $data = [], int $delaySeconds = 0)
{
if ($user->active === 'no') {
public function sendMessageTo(
string $messageType,
?User $user = null,
array $data = [],
int $delaySeconds = 0
) {
if ($user && $user->active === 'no') {
\Yii::warning([
'action' => 'send message',
'status' => 'canceled',
Expand All @@ -357,7 +369,7 @@ public function sendMessageTo(string $messageType, User $user, array $data = [],
}

$dataForEmail = ArrayHelper::merge(
$user->getAttributesForEmail(),
$user ? $user->getAttributesForEmail() : [],
$this->otherDataForEmails,
$data
);
Expand All @@ -372,7 +384,9 @@ public function sendMessageTo(string $messageType, User $user, array $data = [],

$this->email($toAddress, $subject, $htmlBody, strip_tags($htmlBody), $ccAddress, $bccAddress, $delaySeconds);

EmailLog::logMessage($messageType, $user->id);
if ($user !== null) {
EmailLog::logMessage($messageType, $user->id);
}
}

/**
Expand Down Expand Up @@ -740,6 +754,38 @@ public function sendPasswordExpiredEmails()
]));
}

public function sendExternalGroupSyncErrorsEmail(
string $appPrefix,
array $errors,
string $recipient,
string $googleSheetUrl
) {
$logData = [
'action' => 'send external-groups sync errors email',
'status' => 'starting',
];

$this->logger->info(array_merge($logData, [
'errors' => count($errors)
]));

$this->sendMessageTo(
EmailLog::MESSAGE_TYPE_EXT_GROUP_SYNC_ERRORS,
null,
[
'toAddress' => $recipient,
'appPrefix' => $appPrefix,
'errors' => $errors,
'googleSheetUrl' => $googleSheetUrl,
'idpDisplayName' => \Yii::$app->params['idpDisplayName'],
]
);

$this->logger->info(array_merge($logData, [
'status' => 'finished',
]));
}

/**
* Sends email alert to HR with all abandoned users, if any
*/
Expand Down
83 changes: 79 additions & 4 deletions application/common/components/ExternalGroupsSync.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public static function syncAllSets(array $syncSetsParams)
$appPrefixKey = sprintf('set%uAppPrefix', $i);
$googleSheetIdKey = sprintf('set%uGoogleSheetId', $i);
$jsonAuthStringKey = sprintf('set%uJsonAuthString', $i);
$errorsEmailRecipientKey = sprintf('set%uErrorsEmailRecipient', $i);

if (! array_key_exists($appPrefixKey, $syncSetsParams)) {
Yii::warning(sprintf(
Expand All @@ -29,6 +30,7 @@ public static function syncAllSets(array $syncSetsParams)
$appPrefix = $syncSetsParams[$appPrefixKey] ?? '';
$googleSheetId = $syncSetsParams[$googleSheetIdKey] ?? '';
$jsonAuthString = $syncSetsParams[$jsonAuthStringKey] ?? '';
$errorsEmailRecipient = $syncSetsParams[$errorsEmailRecipientKey] ?? '';

if (empty($appPrefix) || empty($googleSheetId) || empty($jsonAuthString)) {
Yii::error(sprintf(
Expand All @@ -45,23 +47,63 @@ public static function syncAllSets(array $syncSetsParams)
$appPrefix,
$googleSheetId
));
self::syncSet($appPrefix, $googleSheetId, $jsonAuthString);
self::syncSet(
$appPrefix,
$googleSheetId,
$jsonAuthString,
$errorsEmailRecipient
);
}
}
}

private static function syncSet(
/**
* Sync the specified external-groups data with the specified Google Sheet.
*
* @param string $appPrefix
* @param string $googleSheetId
* @param string $jsonAuthString
* @param string $errorsEmailRecipient
* @throws \Google\Service\Exception
*/
public static function syncSet(
string $appPrefix,
string $googleSheetId,
string $jsonAuthString
string $jsonAuthString,
string $errorsEmailRecipient = ''
) {
$desiredExternalGroups = self::getExternalGroupsFromGoogleSheet(
$googleSheetId,
$jsonAuthString
);
self::processUpdates(
$appPrefix,
$desiredExternalGroups,
$errorsEmailRecipient,
$googleSheetId
);
}

/**
* Update users' external-groups using the given data, and handle (and
* return) any errors.
*
* @param string $appPrefix
* @param array $desiredExternalGroups
* @param string $errorsEmailRecipient
* @param string $googleSheetIdForEmail -- The Google Sheet's ID, for use in
* the sync-error notification email.
* @return string[] -- The resulting error messages.
*/
public static function processUpdates(
string $appPrefix,
array $desiredExternalGroups,
string $errorsEmailRecipient = '',
string $googleSheetIdForEmail = ''
): array {
$errors = User::updateUsersExternalGroups($appPrefix, $desiredExternalGroups);
Yii::warning(sprintf(
"Ran sync for '%s' external groups.",
"Updated '%s' external groups.",
$appPrefix
));

Expand All @@ -76,7 +118,17 @@ private static function syncSet(
$errorSummary = substr($errorSummary, 0, 997) . '...';
}
Yii::error($errorSummary);

if (!empty($errorsEmailRecipient)) {
self::sendSyncErrorsEmail(
$appPrefix,
$errors,
$errorsEmailRecipient,
'https://docs.google.com/spreadsheets/d/' . $googleSheetIdForEmail
);
}
}
return $errors;
}

/**
Expand Down Expand Up @@ -122,4 +174,27 @@ private static function getExternalGroupsFromGoogleSheet(
}
return $data;
}

/**
* @param string $appPrefix
* @param string[] $errors
* @param string $recipient
* @param string $googleSheetUrl
* @return void
*/
private static function sendSyncErrorsEmail(
string $appPrefix,
array $errors,
string $recipient,
string $googleSheetUrl
) {
/* @var $emailer Emailer */
$emailer = \Yii::$app->emailer;
$emailer->sendExternalGroupSyncErrorsEmail(
$appPrefix,
$errors,
$recipient,
$googleSheetUrl
);
}
}
2 changes: 2 additions & 0 deletions application/common/config/main.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
'otherDataForEmails' => [
'emailSignature' => Env::get('EMAIL_SIGNATURE', ''),
'helpCenterUrl' => Env::get('HELP_CENTER_URL'),
'idpName' => $idpName,
'idpDisplayName' => $idpDisplayName,
'passwordProfileUrl' => $passwordProfileUrl . '/#',
'supportEmail' => Env::get('SUPPORT_EMAIL'),
Expand Down Expand Up @@ -125,6 +126,7 @@
'subjectForPasswordExpiring' => Env::get('SUBJECT_FOR_PASSWORD_EXPIRING'),
'subjectForPasswordExpired' => Env::get('SUBJECT_FOR_PASSWORD_EXPIRED'),
'subjectForAbandonedUsers' => Env::get('SUBJECT_FOR_ABANDONED_USERS'),
'subjectForExtGroupSyncErrors' => Env::get('SUBJECT_FOR_EXT_GROUP_SYNC_ERRORS'),

'lostSecurityKeyEmailDays' => Env::get('LOST_SECURITY_KEY_EMAIL_DAYS', 62),
'minimumBackupCodesBeforeNag' => Env::get('MINIMUM_BACKUP_CODES_BEFORE_NAG', 4),
Expand Down
44 changes: 44 additions & 0 deletions application/common/mail/ext-group-sync-errors.html.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php
use yii\helpers\Html as yHtml;

/**
* @var string $appPrefix
* @var string[] $errors
* @var string $googleSheetUrl
* @var string $emailSignature
* @var string $idpDisplayName
* @var string $idpName
*/
?>
<p>
The following errors occurred when syncing the <?= yHtml::encode($appPrefix) ?>
external groups to the <?= yHtml::encode($idpDisplayName) ?> IDP:
</p>
<?= yHtml::ul($errors) ?>

<?php
if (empty($googleSheetUrl)) {
?>
<p>
If any of these seem like simple data problems, you can potentially fix them
by updating the information in the Google Sheet used for this
external-groups sync.
</p>
<?php
} else {
?>
<p>
If any of these seem like simple data problems, you can potentially fix them
by updating the information in the "<?= yHtml::encode($idpName) ?>" tab of
this Google Sheet: <br />
<?= yHtml::a($googleSheetUrl, $googleSheetUrl) ?>
</p>
<?php
}
?>

<p>
Other users' external groups may have been synced successfully.
</p>

<p><i><?= nl2br(yHtml::encode($emailSignature), false) ?></i></p>
1 change: 1 addition & 0 deletions application/common/models/EmailLog.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class EmailLog extends EmailLogBase
public const MESSAGE_TYPE_MFA_RATE_LIMIT = 'mfa-rate-limit';
public const MESSAGE_TYPE_PASSWORD_CHANGED = 'password-changed';
public const MESSAGE_TYPE_WELCOME = 'welcome';
public const MESSAGE_TYPE_EXT_GROUP_SYNC_ERRORS = 'ext-group-sync-errors';
public const MESSAGE_TYPE_GET_BACKUP_CODES = 'get-backup-codes';
public const MESSAGE_TYPE_REFRESH_BACKUP_CODES = 'refresh-backup-codes';
public const MESSAGE_TYPE_LOST_SECURITY_KEY = 'lost-security-key';
Expand Down
3 changes: 2 additions & 1 deletion application/common/models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -1031,7 +1031,8 @@ public function isPromptForMfa(): bool
}

/**
* Update users' external-groups data using the given external-groups data.
* Update users' external-groups data using the given external-groups data
* and return a list of any errors that occurred.
*
* @param string $appPrefix -- Example: "wiki"
* @param array $desiredExternalGroupsByUserEmail -- The authoritative list
Expand Down
Loading

0 comments on commit 3391eae

Please sign in to comment.