feat: align role assignment logic with myep-team
This commit is contained in:
@@ -8,6 +8,7 @@ use App\BusProNet\ApiClient;
|
||||
use App\BusProNet\Exception\ApiClientException;
|
||||
use App\BusProNet\Exception\ImmediateConnectionCloseException;
|
||||
use App\BusProNet\Exception\TimeoutException;
|
||||
use App\BusProNet\Model\CrmAttributes;
|
||||
use App\BusProNet\Model\PersonalData;
|
||||
use App\Entity\User;
|
||||
use App\Htmx\HxRedirectResponse;
|
||||
@@ -34,13 +35,11 @@ use Symfony\Component\Security\Http\Util\TargetPathTrait;
|
||||
* Validates credentials via BPN's getPersonalData endpoint and creates or updates
|
||||
* local User entities. Passwords are stored encrypted with RSA for subsequent API calls.
|
||||
*
|
||||
* Roles have two owners. The non-privileged ones mirror the CRM selections on every login, in
|
||||
* both directions, so somebody who becomes (or stops being) a Teamer in BusPro is granted (or
|
||||
* loses) ROLE_TEAMER here — the sibling app myep-team gates on it. Role::PRIVILEGED is never
|
||||
* imported: BusPro backend users can edit their own CRM selections, so honouring those would
|
||||
* let anybody make themselves an administrator; they are granted in /admin/user only.
|
||||
*
|
||||
* Hotel codes still seed a *new* account only and are managed in /admin/user afterwards.
|
||||
* BusPro owns the whole role set and the hotel codes: both are synced on every login, in both
|
||||
* directions, so anything the CRM no longer reports is withdrawn here. What the CRM claims is
|
||||
* not automatically granted, though — Role::sync() turns an administrative claim into a
|
||||
* nomination that an administrator has to approve in /admin/user, because BusPro backend users
|
||||
* can edit their own CRM selections and would otherwise make themselves administrators.
|
||||
*/
|
||||
class BpnAuthenticator extends AbstractLoginFormAuthenticator implements AuthenticationEntryPointInterface
|
||||
{
|
||||
@@ -109,13 +108,13 @@ class BpnAuthenticator extends AbstractLoginFormAuthenticator implements Authent
|
||||
|
||||
if (null === $user = $userRepository->findOneBy(['email' => $email])) {
|
||||
$user = new User($email);
|
||||
$user->setHotelCodes(array_values(array_unique($crmAttributes->hotelCodes)));
|
||||
|
||||
$this->entityManager->persist($user);
|
||||
}
|
||||
|
||||
$this->syncFromCrm($user, $crmAttributes);
|
||||
|
||||
$user
|
||||
->setRoles($this->syncedRoles($email, $user->getRoles(), $crmAttributes->roles))
|
||||
->setPassword($encryptedPassword)
|
||||
->setPersonId($personalData->personId)
|
||||
->setAddressId($personalData->addressId)
|
||||
@@ -131,40 +130,43 @@ class BpnAuthenticator extends AbstractLoginFormAuthenticator implements Authent
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges the two halves of the role set: the non-privileged roles the BusPro CRM currently
|
||||
* reports, and the privileged ones an administrator granted here. Anything the CRM no
|
||||
* longer reports is dropped, so revoking a selection there revokes it here too.
|
||||
*
|
||||
* @param string[] $storedRoles
|
||||
* @param string[] $crmRoles
|
||||
*
|
||||
* @return string[]
|
||||
* Writes back what the CRM currently claims: the roles per Role::sync() and the hotel codes
|
||||
* verbatim. Both replace what is stored, which is what makes BusPro the source of truth.
|
||||
*/
|
||||
private function syncedRoles(string $email, array $storedRoles, array $crmRoles): array
|
||||
private function syncFromCrm(User $user, CrmAttributes $crmAttributes): void
|
||||
{
|
||||
return Role::combine($this->importableRoles($email, $crmRoles), $storedRoles);
|
||||
}
|
||||
$previousRoles = $user->getRoles();
|
||||
|
||||
/**
|
||||
* @param string[] $crmRoles
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
private function importableRoles(string $email, array $crmRoles): array
|
||||
{
|
||||
$roles = Role::filterImportable($crmRoles);
|
||||
$dropped = array_values(array_intersect($crmRoles, Role::PRIVILEGED));
|
||||
|
||||
if ([] !== $dropped) {
|
||||
// Somebody holds a privileged CRM selection in BusPro. We do not honour it, but it
|
||||
// should stay visible: it either needs to be revoked there or granted in /admin/user.
|
||||
$this->authLogger->warning('Ignored privileged roles from BPN CRM attributes', [
|
||||
'email' => $email,
|
||||
'roles' => $dropped,
|
||||
if ([] === $crmAttributes->selectionGroups) {
|
||||
// BusPro always answers with the full attribute tree and expresses membership through
|
||||
// the `auswahl` flag, so an empty one is a degraded payload rather than a revocation.
|
||||
// Syncing it would strip the roles of every user who logs in.
|
||||
$this->authLogger->warning('Skipped the role sync: the BPN CRM response carries no selection groups', [
|
||||
'email' => $user->getEmail(),
|
||||
]);
|
||||
|
||||
// An existing account keeps everything it has. A brand new one still needs a role,
|
||||
// and an empty claim set is exactly what Role::sync() answers with the fallback.
|
||||
if ([] !== Role::assignedOnly($previousRoles)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return $roles;
|
||||
$user
|
||||
->setRoles(Role::sync($previousRoles, $crmAttributes->roles))
|
||||
->setHotelCodes(array_values(array_unique($crmAttributes->hotelCodes)))
|
||||
;
|
||||
|
||||
$nominated = array_diff(Role::pendingOnly($user->getRoles()), Role::pendingOnly($previousRoles));
|
||||
|
||||
if ([] !== $nominated) {
|
||||
// The CRM claims an administrative role for somebody who does not hold it. It grants
|
||||
// nothing until an administrator approves it in /admin/user.
|
||||
$this->authLogger->info('Nominated for administrative roles by the BPN CRM', [
|
||||
'email' => $user->getEmail(),
|
||||
'roles' => array_values($nominated),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
|
||||
|
||||
Reference in New Issue
Block a user