<?php
namespace Platform\SecurityBundle\Listeners\OneRoster;
use Cms\CoreBundle\Entity\AbstractOneRosterEntity;
use Cms\CoreBundle\Entity\OneRoster\OneRosterUser;
use Cms\CoreBundle\Entity\OneRosterSync;
use Cms\CoreBundle\Events\OneRosterProcessEvent;
use Cms\CoreBundle\Model\Interfaces\OneRosterable\AbstractOneRosterableSubscriber;
use Doctrine\Common\Util\ClassUtils;
use Platform\SecurityBundle\Entity\Identity\Account;
final class OneRosterProcessUserSubscriber extends AbstractOneRosterableSubscriber
{
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array
{
return [
OneRosterProcessEvent::EVENT__USER => [
['accountSync', 1],
],
];
}
/**
* @param OneRosterProcessEvent $event
*/
public function accountSync(OneRosterProcessEvent $event): void
{
// ensure we are meant to process this
if ( ! $this->checkTypes($event->getSync(), [
OneRosterSync::STRATEGIES__SSO,
])) {
return;
}
// get the user
$user = $event->getEntity();
if ( ! $user instanceof OneRosterUser) {
throw new \RuntimeException(sprintf(
'SSO: User is not of proper type, got "%s".',
ClassUtils::getClass($user)
));
}
// check and make sure that we are of a proper role
if ( ! $user->isRoleStaff()) {
// DEBUGGING
$event->getOutput()->writeln(sprintf(
'User role for #%s is "%s", skipping...',
$user->getSourcedId(),
$user->getRole()
));
// quit early, nothing else to do since not staff
return;
}
// need to see if the email is unique in the dataset
// gg4l appears to be sending over duplicate accounts
// if there are duplicates, we are going to prioritize the last one updated
if ($user->getEmail()) {
// attempt to find the duplicates
$duplicates = $this->em->getRepository(OneRosterUser::class)->findBy(
[
'role' => OneRosterUser::TYPES_MAPPING[OneRosterUser::TYPES__STAFF],
'email' => $user->getEmail(),
'status' => AbstractOneRosterEntity::ENUMS__STATUS_TYPE__ACTIVE,
],
[
'dateLastModified' => 'DESC',
'sourcedId' => 'ASC',
]
);
// if there are duplicates, need to check more things
// the set should be ordered by what we want to prioritize
// if we are not the one to be prioritized, we should skip
if ($duplicates && $duplicates[0] !== $user) {
// DEBUGGING
$event->getOutput()->writeln(sprintf(
'Duplicate email for "%s" record #%s found; skipping to prioritize #%s instead.',
$user->getRole(),
$user->getSourcedId(),
$duplicates[0]->getSourcedId()
));
// quit early
return;
}
}
// attempt to load an account for us
$account = $this->em->getRepository(Account::class)->findOneByOneRosterId(
$user->getSourcedId()
);
// if null we need to check for email
if ( ! $account) {
// check fields
if (empty($user->getEmail())) {
// DEBUGGING
$event->getOutput()->writeln(sprintf(
'Email for "%s" record #%s missing; skipping.',
$user->getRole(),
$user->getSourcedId()
));
// quit early, nothing else to do since no email
return;
}
// try to find by email
$account = $this->em->getRepository(Account::class)->findOneByEmail(
strtolower($user->getEmail())
);
// now if null, we need to make a new one
if ( ! $account) {
// make the account
$account = new Account();
// ensure that the new user is not set to any special access permissions
$account->getSpecialPermissions()
->setSuperUser(false);
}
}
// check fields
if (empty($user->getEmail())) {
throw new \RuntimeException(
'SSO: User is missing email address; cannot generate account.'
);
}
// see if there is a duplicate user
$collision = $this->em->getRepository(Account::class)->findOneByEmail($user->getEmail());
if ($collision && $collision !== $account) {
throw new \RuntimeException(sprintf(
'SSO: Account collision (#%s %s) found in system for email "%s" (#%s).',
$collision->getId(),
$collision->getDisplayName(),
$user->getEmail(),
$user->getSourcedId()
));
}
// update fields on the account
$account
->setEmail(strtolower($user->getEmail()));
// update stuff on the system profile
$account->getSystemProfile()
->setFirstName($user->getGivenName())
->setLastName($user->getFamilyName())
->setDisplayName(null)
;
// determine the usability of the user
// if we are to ignore the enabledUser property (different schools have different takes on this), then we just use the active status
// otherwise, we need to incorporate that enabledUser property into our calculations
$usable = $event->getSync()->hasFlag(OneRosterSync::FLAGS__IGNORE_ENABLED_USER_PROPERTY)
? $user->isStatusActive()
: $user->isUsable();
// if we happen to be an internal account, we should always be usable, no matter what is in the sis data
if ($account->isInternal()) {
$usable = true;
}
// set active status based on whether the oneroster entity is available
// do only if the record is already active; if manually switched to inactive status that status should stay
if ($account->isActive()) {
$account->setActive($usable);
}
// oneroster stuff
$account
->setOneRosterId($user->getSourcedId())
->setOneRosterArchived($user->isStatusToBeDeleted());
// handle all orgs
$orgs = array_values(array_unique(array_merge(
$user->hasMetadataEntry('gg4l.primary_school')
? [$user->getMetadataEntry('gg4l.primary_school')]
: [],
$user->getOrgsSourcedIds(),
)));
if ($event->getSync()->hasFlag(OneRosterSync::FLAGS__SINGLE_SCHOOL)) {
$orgs = [$event->getSync()->getDistrictId()];
}
// handle core metadata
$metadata = [
'_role' => $user->getRole(),
'_role_type' => OneRosterUser::TYPES_LOOKUP[
OneRosterUser::ROLES_MAPPING[$user->getRole()]
],
'_orgs' => $orgs,
'_schools' => array_values(array_diff(
$orgs,
[$event->getSync()->getDistrictId()],
)),
'_district' => $event->getSync()->getDistrictId(),
];
// handle metadata
$account->setMetadata(
array_merge(
$user->getMetadata() ?: [],
$metadata
)
);
// cache the output
$output = sprintf(
' %s %s (%s | %s | %s)',
(empty($account->getId())) ? 'Generating' : 'Updating',
ClassUtils::getClass($account),
$account->getUserIdentifier(),
$account->getOneRosterId(),
$account->getId() ?: '-'
);
// let's save the account
$this->em->save($account);
// DEBUGGING
$event->getOutput()->writeln($output);
}
}