feat: admin-managed roles and hotel codes

This commit is contained in:
Björn Fromme committed 2026-08-10 10:07:24 +02:00
1 parent 124c0af0f5
commit 6c1073e41c
19 files changed
+718 -34

No files matched your search

+34 -8
View File
@@ -31,9 +31,13 @@ use Symfony\Component\Security\Http\Util\TargetPathTrait;
/**
* Authenticates users against the BPN API.
*
* Validates credentials via BPN's getPersonalData endpoint, creates or updates
* local User entities, and retrieves CRM attributes (roles, hotel codes) for
* authorization. Passwords are stored encrypted with RSA for subsequent API calls.
* Validates credentials via BPN's getPersonalData endpoint and creates or updates
* local User entities. Passwords are stored encrypted with RSA for subsequent API calls.
*
* CRM attributes (roles, hotel codes) seed a *new* account only: BusPro backend users can
* edit their own CRM selections, so taking roles over on every login would let anybody grant
* themselves Role::PRIVILEGED here. Privileged roles are never imported at all, and from the
* second login on both roles and hotel codes are managed by an administrator in /admin/user.
*/
class BpnAuthenticator extends AbstractLoginFormAuthenticator implements AuthenticationEntryPointInterface
{
@@ -96,15 +100,17 @@ class BpnAuthenticator extends AbstractLoginFormAuthenticator implements Authent
throw new CustomUserMessageAuthenticationException($message);
}
$roles = $crmAttributes->roles;
$hotelCodes = $crmAttributes->hotelCodes;
$encryptedPassword = $this->crypt->encrypt($password);
$userRepository = $this->entityManager->getRepository(User::class);
if (null === $user = $userRepository->findOneBy(['email' => $email])) {
$user = new User($email);
$user
->setRoles($this->importableRoles($email, $crmAttributes->roles))
->setHotelCodes(array_values(array_unique($crmAttributes->hotelCodes)))
;
$this->entityManager->persist($user);
}
@@ -112,8 +118,6 @@ class BpnAuthenticator extends AbstractLoginFormAuthenticator implements Authent
->setPassword($encryptedPassword)
->setPersonId($personalData->personId)
->setAddressId($personalData->addressId)
->setRoles($roles)
->setHotelCodes($hotelCodes)
->setLastLoginAt(new \DateTimeImmutable())
->setProfileComplete($this->completenessChecker->isComplete($personalData))
;
@@ -123,6 +127,28 @@ class BpnAuthenticator extends AbstractLoginFormAuthenticator implements Authent
return $user;
}
/**
* @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,
]);
}
return $roles;
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
$this->authLogger->info('Login', [
+84
View File
@@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
namespace App\Security;
/**
* The roles this application knows about.
*
* Roles are stored as plain strings in the User::$roles JSON column and consumed as strings
* by #[IsGranted], the voters and security.yaml's role_hierarchy, so they are constants
* rather than an enum.
*/
final class Role
{
public const ADMIN = 'ROLE_ADMIN';
public const MANAGER = 'ROLE_MANAGER';
public const TEAMER = 'ROLE_TEAMER';
public const CUSTOMER = 'ROLE_CUSTOMER';
public const HOUSE_MANAGER = 'ROLE_HOUSE_MANAGER';
public const GROUPS_ADMIN = 'ROLE_GROUPS_ADMIN';
public const GROUPS_MANAGER = 'ROLE_GROUPS_MANAGER';
/**
* The roles that are actually assigned to accounts. ROLE_USER is left out because every
* account has it implicitly (see User::getRoles()) and it is never stored.
*
* @var string[]
*/
public const ALL = [
self::ADMIN,
self::MANAGER,
self::TEAMER,
self::CUSTOMER,
self::HOUSE_MANAGER,
self::GROUPS_ADMIN,
self::GROUPS_MANAGER,
];
/**
* Roles that are never taken over from BusProNet. Many people can edit CRM selections in
* the BusPro backend, so these are granted by an administrator in /admin/user only.
*
* @var string[]
*/
public const PRIVILEGED = [
self::ADMIN,
self::GROUPS_ADMIN,
self::GROUPS_MANAGER,
];
/**
* Reduces the roles derived from BPN CRM attributes to the ones we accept from there.
*
* Falls back to ROLE_CUSTOMER the way CrmAttributesResponseParser does, so an account
* whose only selection was a privileged one does not end up without any role.
*
* @param string[] $roles
*
* @return string[]
*/
public static function filterImportable(array $roles): array
{
$importable = array_values(array_unique(array_diff($roles, self::PRIVILEGED)));
return [] === $importable ? [self::CUSTOMER] : $importable;
}
/**
* @return array<string, string> role => label
*/
public static function labels(): array
{
return [
self::ADMIN => 'Administration',
self::MANAGER => 'Manager:in',
self::TEAMER => 'Teamer:in',
self::CUSTOMER => 'Kund:in',
self::HOUSE_MANAGER => 'Hausleitung',
self::GROUPS_ADMIN => 'Preisrechner Admin',
self::GROUPS_MANAGER => 'Preisrechner',
];
}
}