200 lines
7.1 KiB
PHP
200 lines
7.1 KiB
PHP
<?php
|
|
|
|
namespace App\Security;
|
|
|
|
use App\BusProNet\ApiClient;
|
|
use App\BusProNet\ApiClientException;
|
|
use App\BusProNet\Model\CrmAttributesResponse;
|
|
use App\BusProNet\Model\ProfileResponse;
|
|
use App\BusProNet\UserDataHandler;
|
|
use App\Entity\User;
|
|
use App\RequiredTeamerCheck\RequiredTeamerCheckRegistry;
|
|
use Doctrine\ORM\EntityManagerInterface;
|
|
use Psr\Log\LoggerInterface;
|
|
use Symfony\Component\HttpFoundation\RedirectResponse;
|
|
use Symfony\Component\HttpFoundation\Request;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
|
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
|
use Symfony\Component\Security\Http\Authenticator\AbstractLoginFormAuthenticator;
|
|
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge;
|
|
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge;
|
|
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
|
|
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
|
|
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
|
|
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
|
|
|
|
class BpnAuthenticator extends AbstractLoginFormAuthenticator implements AuthenticationEntryPointInterface
|
|
{
|
|
public function __construct(
|
|
private readonly UrlGeneratorInterface $urlGenerator,
|
|
private readonly EntityManagerInterface $entityManager,
|
|
private readonly ApiClient $apiClient,
|
|
private readonly LoggerInterface $logger,
|
|
private readonly UserDataHandler $userDataHandler,
|
|
private readonly RequiredTeamerCheckRegistry $requiredTeamerCheckRegistry,
|
|
) {
|
|
}
|
|
|
|
protected function getLoginUrl(Request $request): string
|
|
{
|
|
return $this->urlGenerator->generate('app_security_login');
|
|
}
|
|
|
|
public function authenticate(Request $request): Passport
|
|
{
|
|
$email = trim($request->request->getString('_username'));
|
|
$passwordPlain = trim($request->request->getString('_password'));
|
|
|
|
// Very lame hashing applied here as required by BPN
|
|
$password = md5($passwordPlain);
|
|
|
|
$csrfToken = $request->request->getString('_csrf_token');
|
|
|
|
return new SelfValidatingPassport(
|
|
new UserBadge($email, function () use ($email, $password, $request) {
|
|
try {
|
|
$response = $this->apiClient->getProfile($email, $password);
|
|
} catch (ApiClientException $e) {
|
|
return null;
|
|
}
|
|
|
|
if (false === $response instanceof ProfileResponse) {
|
|
return null;
|
|
}
|
|
|
|
// Final checks and local user loading/creation
|
|
$user = $this->getOrCreateLocalUser($response, $email, $password);
|
|
|
|
if (null === $user) {
|
|
return null;
|
|
}
|
|
|
|
// Store BPN password in session for later use
|
|
$request->getSession()->set('bpn_password', $password);
|
|
|
|
return $user;
|
|
}),
|
|
[
|
|
new CsrfTokenBadge('authenticate', $csrfToken),
|
|
new RememberMeBadge(),
|
|
]
|
|
);
|
|
}
|
|
|
|
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
|
|
{
|
|
/** @var User $user */
|
|
$user = $token->getUser();
|
|
|
|
$user->setLastLoginAt(new \DateTimeImmutable());
|
|
$this->logger->info('Login');
|
|
|
|
$this->entityManager->flush();
|
|
|
|
$route = $user->getDefaultRoute();
|
|
if ('app_teamer_index' === $route) {
|
|
$check = $this->requiredTeamerCheckRegistry->getFirstUnresolvedCheck($user);
|
|
if (null !== $check) {
|
|
$route = $check->getRouteName();
|
|
}
|
|
}
|
|
|
|
$url = $this->urlGenerator->generate($route);
|
|
|
|
return new RedirectResponse($url);
|
|
}
|
|
|
|
private function getOrCreateLocalUser(
|
|
ProfileResponse $profileResponse,
|
|
string $email,
|
|
string $password,
|
|
): ?User {
|
|
// Fetch CRM attributes, early return in case of an API error
|
|
try {
|
|
$crmAttributes = $this->apiClient->getCrmAttributes($email, $password);
|
|
} catch (ApiClientException $e) {
|
|
return null;
|
|
}
|
|
|
|
// BusPro answers with a notification record instead of the data on its own errors
|
|
if (false === $crmAttributes instanceof CrmAttributesResponse) {
|
|
return null;
|
|
}
|
|
|
|
// Flatten selected CRM attributes
|
|
$crmSelections = $crmAttributes->toArray();
|
|
|
|
// Determine teamer status from CRM attributes
|
|
$isTeamer = $crmAttributes->isTeamer();
|
|
|
|
// Everything the CRM grants this person here: pending markers plus ROLE_TEAMER
|
|
$claimedRoles = $this
|
|
->userDataHandler
|
|
->collectRoles($crmAttributes)
|
|
;
|
|
|
|
// Check if user is already present in local database
|
|
$user = $this
|
|
->userDataHandler
|
|
->findLocalUser($profileResponse)
|
|
;
|
|
|
|
// BusPro knows this person but grants them nothing in this application, so they
|
|
// are no user of it: never create an account, block an existing one. Returning
|
|
// the blocked user lets the UserChecker explain why the login was refused.
|
|
if ([] === $claimedRoles) {
|
|
// A response without any attribute group carries no roles either, so it looks
|
|
// exactly like a revocation while it really means the CRM told us nothing:
|
|
// an empty payload, a changed schema, a misconfigured attribute id. Blocking
|
|
// on that would lock out every user logging in, so refuse this single login
|
|
// instead and leave the account alone.
|
|
if ([] === ($crmAttributes->getAttributeGroups() ?? [])) {
|
|
$this->logger->warning('Skip demotion: CRM attributes response carries no attribute groups', [
|
|
'user_id' => $user?->getId(),
|
|
'user_email' => $email,
|
|
]);
|
|
|
|
return null;
|
|
}
|
|
|
|
if (null === $user) {
|
|
return null;
|
|
}
|
|
|
|
$this->userDataHandler->disableForRevokedCrmRoles($user);
|
|
|
|
return $user;
|
|
}
|
|
|
|
// Update existing user's teamer data and return it, leaving roles and hotel
|
|
// codes alone: they are imported once on creation and managed manually after
|
|
if (null !== $user) {
|
|
$this
|
|
->userDataHandler
|
|
->updateLocalUser(
|
|
$user,
|
|
$profileResponse,
|
|
$isTeamer,
|
|
$crmSelections,
|
|
$this->userDataHandler->collectPendingRoles($crmAttributes),
|
|
)
|
|
;
|
|
|
|
return $user;
|
|
}
|
|
|
|
// Initial import of roles and hotel codes on user creation
|
|
return $this
|
|
->userDataHandler
|
|
->createLocalUser(
|
|
$profileResponse,
|
|
$claimedRoles,
|
|
$isTeamer,
|
|
$crmSelections,
|
|
$crmAttributes->getHotelCodes(),
|
|
)
|
|
;
|
|
}
|
|
}
|