src/Platform/SecurityBundle/Listeners/OneRoster/OneRosterProcessUserSubscriber.php line 30

Open in your IDE?
  1. <?php
  2. namespace Platform\SecurityBundle\Listeners\OneRoster;
  3. use Cms\CoreBundle\Entity\AbstractOneRosterEntity;
  4. use Cms\CoreBundle\Entity\OneRoster\OneRosterUser;
  5. use Cms\CoreBundle\Entity\OneRosterSync;
  6. use Cms\CoreBundle\Events\OneRosterProcessEvent;
  7. use Cms\CoreBundle\Model\Interfaces\OneRosterable\AbstractOneRosterableSubscriber;
  8. use Doctrine\Common\Util\ClassUtils;
  9. use Platform\SecurityBundle\Entity\Identity\Account;
  10. final class OneRosterProcessUserSubscriber extends AbstractOneRosterableSubscriber
  11. {
  12.     /**
  13.      * {@inheritdoc}
  14.      */
  15.     public static function getSubscribedEvents(): array
  16.     {
  17.         return [
  18.             OneRosterProcessEvent::EVENT__USER => [
  19.                 ['accountSync'1],
  20.             ],
  21.         ];
  22.     }
  23.     /**
  24.      * @param OneRosterProcessEvent $event
  25.      */
  26.     public function accountSync(OneRosterProcessEvent $event): void
  27.     {
  28.         // ensure we are meant to process this
  29.         if ( ! $this->checkTypes($event->getSync(), [
  30.             OneRosterSync::STRATEGIES__SSO,
  31.         ])) {
  32.             return;
  33.         }
  34.         // get the user
  35.         $user $event->getEntity();
  36.         if ( ! $user instanceof OneRosterUser) {
  37.             throw new \RuntimeException(sprintf(
  38.                 'SSO: User is not of proper type, got "%s".',
  39.                 ClassUtils::getClass($user)
  40.             ));
  41.         }
  42.         // check and make sure that we are of a proper role
  43.         if ( ! $user->isRoleStaff()) {
  44.             // DEBUGGING
  45.             $event->getOutput()->writeln(sprintf(
  46.                 'User role for #%s is "%s", skipping...',
  47.                 $user->getSourcedId(),
  48.                 $user->getRole()
  49.             ));
  50.             // quit early, nothing else to do since not staff
  51.             return;
  52.         }
  53.         // need to see if the email is unique in the dataset
  54.         // gg4l appears to be sending over duplicate accounts
  55.         // if there are duplicates, we are going to prioritize the last one updated
  56.         if ($user->getEmail()) {
  57.             // attempt to find the duplicates
  58.             $duplicates $this->em->getRepository(OneRosterUser::class)->findBy(
  59.                 [
  60.                     'role' => OneRosterUser::TYPES_MAPPING[OneRosterUser::TYPES__STAFF],
  61.                     'email' => $user->getEmail(),
  62.                     'status' => AbstractOneRosterEntity::ENUMS__STATUS_TYPE__ACTIVE,
  63.                 ],
  64.                 [
  65.                     'dateLastModified' => 'DESC',
  66.                     'sourcedId' => 'ASC',
  67.                 ]
  68.             );
  69.             // if there are duplicates, need to check more things
  70.             // the set should be ordered by what we want to prioritize
  71.             // if we are not the one to be prioritized, we should skip
  72.             if ($duplicates && $duplicates[0] !== $user) {
  73.                 // DEBUGGING
  74.                 $event->getOutput()->writeln(sprintf(
  75.                     'Duplicate email for "%s" record #%s found; skipping to prioritize #%s instead.',
  76.                     $user->getRole(),
  77.                     $user->getSourcedId(),
  78.                     $duplicates[0]->getSourcedId()
  79.                 ));
  80.                 // quit early
  81.                 return;
  82.             }
  83.         }
  84.         // attempt to load an account for us
  85.         $account $this->em->getRepository(Account::class)->findOneByOneRosterId(
  86.             $user->getSourcedId()
  87.         );
  88.         // if null we need to check for email
  89.         if ( ! $account) {
  90.             // check fields
  91.             if (empty($user->getEmail())) {
  92.                 // DEBUGGING
  93.                 $event->getOutput()->writeln(sprintf(
  94.                     'Email for "%s" record #%s missing; skipping.',
  95.                     $user->getRole(),
  96.                     $user->getSourcedId()
  97.                 ));
  98.                 // quit early, nothing else to do since no email
  99.                 return;
  100.             }
  101.             // try to find by email
  102.             $account $this->em->getRepository(Account::class)->findOneByEmail(
  103.                 strtolower($user->getEmail())
  104.             );
  105.             // now if null, we need to make a new one
  106.             if ( ! $account) {
  107.                 // make the account
  108.                 $account = new Account();
  109.                 // ensure that the new user is not set to any special access permissions
  110.                 $account->getSpecialPermissions()
  111.                     ->setSuperUser(false);
  112.             }
  113.         }
  114.         // check fields
  115.         if (empty($user->getEmail())) {
  116.             throw new \RuntimeException(
  117.                 'SSO: User is missing email address; cannot generate account.'
  118.             );
  119.         }
  120.         // see if there is a duplicate user
  121.         $collision $this->em->getRepository(Account::class)->findOneByEmail($user->getEmail());
  122.         if ($collision && $collision !== $account) {
  123.             throw new \RuntimeException(sprintf(
  124.                 'SSO: Account collision (#%s %s) found in system for email "%s" (#%s).',
  125.                 $collision->getId(),
  126.                 $collision->getDisplayName(),
  127.                 $user->getEmail(),
  128.                 $user->getSourcedId()
  129.             ));
  130.         }
  131.         // update fields on the account
  132.         $account
  133.             ->setEmail(strtolower($user->getEmail()));
  134.         // update stuff on the system profile
  135.         $account->getSystemProfile()
  136.             ->setFirstName($user->getGivenName())
  137.             ->setLastName($user->getFamilyName())
  138.             ->setDisplayName(null)
  139.         ;
  140.         // determine the usability of the user
  141.         // if we are to ignore the enabledUser property (different schools have different takes on this), then we just use the active status
  142.         // otherwise, we need to incorporate that enabledUser property into our calculations
  143.         $usable $event->getSync()->hasFlag(OneRosterSync::FLAGS__IGNORE_ENABLED_USER_PROPERTY)
  144.             ? $user->isStatusActive()
  145.             : $user->isUsable();
  146.         // if we happen to be an internal account, we should always be usable, no matter what is in the sis data
  147.         if ($account->isInternal()) {
  148.             $usable true;
  149.         }
  150.         // set active status based on whether the oneroster entity is available
  151.         // do only if the record is already active; if manually switched to inactive status that status should stay
  152.         if ($account->isActive()) {
  153.             $account->setActive($usable);
  154.         }
  155.         // oneroster stuff
  156.         $account
  157.             ->setOneRosterId($user->getSourcedId())
  158.             ->setOneRosterArchived($user->isStatusToBeDeleted());
  159.         // handle all orgs
  160.         $orgs array_values(array_unique(array_merge(
  161.             $user->hasMetadataEntry('gg4l.primary_school')
  162.                 ? [$user->getMetadataEntry('gg4l.primary_school')]
  163.                 : [],
  164.             $user->getOrgsSourcedIds(),
  165.         )));
  166.         if ($event->getSync()->hasFlag(OneRosterSync::FLAGS__SINGLE_SCHOOL)) {
  167.             $orgs = [$event->getSync()->getDistrictId()];
  168.         }
  169.         // handle core metadata
  170.         $metadata = [
  171.             '_role' => $user->getRole(),
  172.             '_role_type' => OneRosterUser::TYPES_LOOKUP[
  173.                 OneRosterUser::ROLES_MAPPING[$user->getRole()]
  174.             ],
  175.             '_orgs' => $orgs,
  176.             '_schools' => array_values(array_diff(
  177.                 $orgs,
  178.                 [$event->getSync()->getDistrictId()],
  179.             )),
  180.             '_district' => $event->getSync()->getDistrictId(),
  181.         ];
  182.         // handle metadata
  183.         $account->setMetadata(
  184.             array_merge(
  185.                 $user->getMetadata() ?: [],
  186.                 $metadata
  187.             )
  188.         );
  189.         // cache the output
  190.         $output sprintf(
  191.             '    %s    %s    (%s | %s | %s)',
  192.             (empty($account->getId())) ? 'Generating' 'Updating',
  193.             ClassUtils::getClass($account),
  194.             $account->getUserIdentifier(),
  195.             $account->getOneRosterId(),
  196.             $account->getId() ?: '-'
  197.         );
  198.         // let's save the account
  199.         $this->em->save($account);
  200.         // DEBUGGING
  201.         $event->getOutput()->writeln($output);
  202.     }
  203. }