<?php
namespace Platform\SecurityBundle\Listeners\OneRoster;
use Cms\CoreBundle\Entity\OneRoster\OneRosterUser;
use Cms\CoreBundle\Entity\OneRosterSync;
use Cms\CoreBundle\Events\OneRosterLinkEvent;
use Cms\CoreBundle\Model\Interfaces\OneRosterable\AbstractOneRosterableSubscriber;
use Doctrine\Common\Util\ClassUtils;
use Platform\SecurityBundle\Entity\Access\GroupAccount;
use Platform\SecurityBundle\Entity\Identity\Account;
use Platform\SecurityBundle\Entity\Identity\Group;
/**
* Class OneRosterLinkUserSubscriber
* @package Platform\SecurityBundle\Listeners\OneRoster
*/
final class OneRosterLinkUserSubscriber extends AbstractOneRosterableSubscriber
{
/**
* {@inheritdoc}
*/
static public function getSubscribedEvents(): array
{
return [
OneRosterLinkEvent::EVENT__USER => [
['groupSyncOld', 0],
['groupsSync', 0],
],
];
}
/**
* @param OneRosterLinkEvent $event
*/
public function groupSyncOld(OneRosterLinkEvent $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 \Exception(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;
}
// attempt to load an account for us
$account = $this->em->getRepository(Account::class)->findOneByOneRosterId(
$user->getSourcedId()
);
// if we don't have one, that may be a problem
if (empty($account)) {
// if the user doesn't have an email, this isn't weird as they would have been skipped over in the process phase
if ( ! $user->getEmail()) {
// DEBUGGING
$event->getOutput()->writeln(sprintf(
'User missing email for #%s, skipping...',
$user->getSourcedId()
));
// quit early, nothing else to do since not staff
return;
} else {
// NOTE: JEZ 2022-01-20
// there are many reasons why this would happen
// some are legit, and some could be bugs in the syncing code
// most are probably legit, so no longer throwing errors for this stuff
// if the email is set and nothing matched, then we have an issue...
/*
throw new \Exception(
'SSO: Could not find user account by OneRoster ID.'
);
*/
// DEBUGGING
$event->getOutput()->writeln(sprintf(
'User absent from database for #%s, skipping...',
$user->getSourcedId()
));
// quit early, nothing else to do since not staff
return;
}
}
// we need to get the groups tied to us for oneroster
$group = $this->em->getRepository(Group::class)->findOneBy([
'name' => $this->translator->trans(sprintf(
OneRosterPrepareSubscriber::NAME_FORMAT,
$user->getRole()
)),
]);
if (empty($group)) {
throw new \Exception(sprintf(
'SSO: Could not load security group for role "%s".',
$user->getRole()
));
}
// get our existing group association
$membership = $this->em->getRepository(GroupAccount::class)->findByAccountAndGroup(
$account,
$group
);
// if we don't have one, need to make one
if ( ! $membership) {
$membership = new GroupAccount();
}
// set things
$membership
->setAlias(sprintf(
'campussuite.platform.security.membership.%s.%s',
$group->getId(),
$account->getId()
))
->setFixed(true)
->setAccount($account)
->setGroup($group);
// oneroster stuff
$membership
->setOneRosterId($user->getSourcedId())
->setOneRosterArchived($user->isStatusToBeDeleted());
// cache the output
$output = sprintf(
' %s %s (%s >> %s | %s | %s)',
(empty($membership->getId())) ? 'Generating' : 'Updating',
ClassUtils::getClass($membership),
$membership->getAccount()->getUserIdentifier(),
$membership->getGroup()->getName(),
$membership->getOneRosterId(),
$membership->getId() ?: '-'
);
// do update
$this->em->save($membership);
// DEBUGGING
$event->getOutput()->writeln($output);
}
/**
* @param OneRosterLinkEvent $event
*/
public function groupsSync(OneRosterLinkEvent $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 \Exception(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;
}
// attempt to load an account for us
$account = $this->em->getRepository(Account::class)->findOneByOneRosterId(
$user->getSourcedId()
);
// if we don't have one, that may be a problem
if (empty($account)) {
// if the user doesn't have an email, this isn't weird as they would have been skipped over in the process phase
if ( ! $user->getEmail()) {
// DEBUGGING
$event->getOutput()->writeln(sprintf(
'User missing email for #%s, skipping...',
$user->getSourcedId()
));
// quit early, nothing else to do since not staff
return;
} else {
// NOTE: JEZ 2022-01-20
// there are many reasons why this would happen
// some are legit, and some could be bugs in the syncing code
// most are probably legit, so no longer throwing errors for this stuff
// if the email is set and nothing matched, then we have an issue...
/*
throw new \Exception(
'SSO: Could not find user account by OneRoster ID.'
);
*/
// DEBUGGING
$event->getOutput()->writeln(sprintf(
'User absent from database for #%s, skipping...',
$user->getSourcedId()
));
// quit early, nothing else to do since not staff
return;
}
}
// determine the aliases we need to look for
$aliases = array_values(array_filter(array_unique([
'oneroster.global.all',
$user->getRole() ? 'oneroster.global.'.$user->getRole() : null,
...array_merge(...array_map(
function (array $org) use ($user) {
return [
sprintf(
'oneroster.school.all.%s',
$org['sourcedId']
),
sprintf(
'oneroster.school.%s.%s',
$user->getRole(),
$org['sourcedId']
),
];
},
$user->getOrgs()
))
])));
// we need to get the groups tied to us for oneroster
$groups = $this->em->getRepository(Group::class)->findBy([
'alias' => $aliases,
]);
if (empty($groups)) {
throw new \Exception(sprintf(
'SSO: Could not load security groups for role "%s".',
$user->getRole()
));
}
// TODO: should we check that the number of groups found matches the number of aliases?
// find all of our existing memberships that are covered by this code
/** @var GroupAccount[] $memberships */
$memberships = $this->em->getRepository(GroupAccount::class)->createQueryBuilder('memberships')
->andWhere('memberships.account = :account')
->setParameter('account', $account)
->andWhere('memberships.alias LIKE :alias')
->setParameter('alias', 'oneroster.%')
->getQuery()
->getResult();
// loop over the groups
$managed = [];
foreach ($groups as $group) {
// try to find an existing relationship
$membership = null;
foreach ($memberships as $thing) {
if ($thing->getGroup() === $group) {
$membership = $thing;
break;
}
}
// if we don't have one, we may need to make one
if ( ! $membership) {
// first, see if the person has been added to this group manually
// if so, it will become managed by syncing code
$membership = $this->em->getRepository(GroupAccount::class)->findByAccountAndGroup(
$account,
$group
);
// if we still don't have one, that means we need to make a brand new one
if ( ! $membership) {
$membership = new GroupAccount();
}
}
// set things
$managed[] = $membership
->setAlias(sprintf(
'oneroster.%s.%s',
$group->getOneRosterId(),
$account->getOneRosterId()
))
->setFixed(true)
->setAccount($account)
->setGroup($group);
// oneroster stuff
$membership
->setOneRosterId($user->getSourcedId())
->setOneRosterArchived($user->isStatusToBeDeleted());
}
// DEBUGGING
array_walk($managed, function (GroupAccount $membership) use ($event) {
$event->getOutput()->writeln(sprintf(
' %s %s (%s >> %s | %s | %s)',
(empty($membership->getId())) ? 'Generating' : 'Updating',
ClassUtils::getClass($membership),
$membership->getAccount()->getEmail(),
$membership->getGroup()->getName(),
$membership->getOneRosterId(),
$membership->getId() ?: '-'
));
});
// go ahead and save the things we know want to keep
// this allows us to get ids that are needed in the following checks
$this->em->saveAll($managed);
// determine which ones we need to remove
$removals = array_udiff($memberships, $managed, function (GroupAccount $a, GroupAccount $b) {
return ($a === $b) ? 0 : $a->getId() <=> $b->getId();
});
// DEBUGGING
array_walk($removals, function (GroupAccount $membership) use ($event) {
$event->getOutput()->writeln(sprintf(
' Deleting %s (%s >> %s | %s | %s)',
ClassUtils::getClass($membership),
$membership->getAccount()->getEmail(),
$membership->getGroup()->getName(),
$membership->getOneRosterId(),
$membership->getId() ?: '-'
));
});
// remove things we no longer need
$this->em->deleteAll($removals);
}
}