Skip to content
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

[IDP-1221] Optionally send external-groups sync-errors notification email #375

Merged
merged 17 commits into from
Oct 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
8dda42a
Add test for sync-error notification emails
forevermatt Sep 11, 2024
41cbac5
Add subject for sync-errors email to Emailer class
forevermatt Sep 11, 2024
ced1394
Merge branch 'develop' into feature/IDP-1221-sync-errors-notification…
forevermatt Sep 16, 2024
7a39a94
Merge branch 'develop' into feature/IDP-1221-sync-errors-notification…
forevermatt Sep 23, 2024
efb4c85
Enable test and live code to use shared code for processing sync errors
forevermatt Sep 25, 2024
f5d9f04
Rename external-group sync error variables/constants, for clarity
forevermatt Sep 30, 2024
1059591
Tweak the subject used for external-group sync error emails
forevermatt Sep 30, 2024
3c533d4
Add `EmailLog::MESSAGE_TYPE_EXT_GROUP_SYNC_ERRORS` for sync-error emails
forevermatt Sep 30, 2024
19ff3a7
Enable the `Emailer` to send to an arbitrary email address
forevermatt Sep 30, 2024
dc801b5
Optionally send an external-groups sync-errors email notification
forevermatt Sep 30, 2024
0a142ee
Raise minimum PHP version to 8.0 (and update dependencies)
forevermatt Sep 30, 2024
4703e72
Clarify purpose of Google Sheet ID parameter used for sync-error email
forevermatt Sep 30, 2024
14e31fa
Add line to sync-error email about partial-successes
forevermatt Sep 30, 2024
27a7b68
Document the new errors-email-recipient env. vars (in `local.env.dist`)
forevermatt Sep 30, 2024
8a5af26
Fix PSR-2 formatting issue
forevermatt Sep 30, 2024
8976497
Fix test-safety-net to work for GitHub Actions' tests, too
forevermatt Sep 30, 2024
aee1705
Re-add PHP 7.4 compatibility
forevermatt Oct 1, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't it impossible for googleSheetUrl to be empty? It would at least have this much: 'https://docs.google.com/spreadsheets/d/'

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh, good catch. I'll try to fix that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Submitted follow-up PR to fix that:
#376

?>
<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