feat: share the role policy across identity sources
This commit is contained in:
@@ -11,6 +11,13 @@ use App\Entity\User;
|
|||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Owns the role policy for local user accounts, whatever identity source reports them.
|
||||||
|
* The importing of BusPro profile data is specific to the CRM, but the role rules -
|
||||||
|
* toPendingRoles(), refreshPendingRoles() and grantTeamerRole() - are deliberately
|
||||||
|
* shared with the MyE&P SSO login (see App\Security\MyEpAuthenticator), so that no
|
||||||
|
* identity source can grant an administrative role this application would not.
|
||||||
|
*/
|
||||||
class UserDataHandler
|
class UserDataHandler
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
@@ -45,15 +52,45 @@ class UserDataHandler
|
|||||||
*/
|
*/
|
||||||
public function collectPendingRoles(CrmAttributesResponse $crmAttributes): array
|
public function collectPendingRoles(CrmAttributesResponse $crmAttributes): array
|
||||||
{
|
{
|
||||||
$roles = [];
|
$claimedRoles = [];
|
||||||
|
|
||||||
if ($crmAttributes->isAdmin()) {
|
if ($crmAttributes->isAdmin()) {
|
||||||
$roles[] = User::PENDING_ROLES['ROLE_ADMIN'];
|
$claimedRoles[] = 'ROLE_ADMIN';
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($crmAttributes->isManager()) {
|
if ($crmAttributes->isManager()) {
|
||||||
|
$claimedRoles[] = 'ROLE_MANAGER';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($crmAttributes->isHouseManager()) {
|
||||||
|
$claimedRoles[] = 'ROLE_HOUSE_MANAGER';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->toPendingRoles($claimedRoles);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps a list of claimed roles to the pending markers for the administrative ones
|
||||||
|
* among them. This is the same policy collectPendingRoles() applies to the CRM
|
||||||
|
* attributes, expressed over plain role names so that any identity source can use
|
||||||
|
* it. ROLE_TEAMER has no marker and is dropped here: it needs no approval.
|
||||||
|
*
|
||||||
|
* @param string[] $claimedRoles
|
||||||
|
*
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
public function toPendingRoles(array $claimedRoles): array
|
||||||
|
{
|
||||||
|
$roles = [];
|
||||||
|
|
||||||
|
if (true === in_array('ROLE_ADMIN', $claimedRoles, true)) {
|
||||||
|
$roles[] = User::PENDING_ROLES['ROLE_ADMIN'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// a manager outranks a house manager, so only the higher marker is kept
|
||||||
|
if (true === in_array('ROLE_MANAGER', $claimedRoles, true)) {
|
||||||
$roles[] = User::PENDING_ROLES['ROLE_MANAGER'];
|
$roles[] = User::PENDING_ROLES['ROLE_MANAGER'];
|
||||||
} elseif ($crmAttributes->isHouseManager()) {
|
} elseif (true === in_array('ROLE_HOUSE_MANAGER', $claimedRoles, true)) {
|
||||||
$roles[] = User::PENDING_ROLES['ROLE_HOUSE_MANAGER'];
|
$roles[] = User::PENDING_ROLES['ROLE_HOUSE_MANAGER'];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -279,8 +316,10 @@ class UserDataHandler
|
|||||||
*
|
*
|
||||||
* It is never withdrawn here. Losing the CRM attribute while holding no other role
|
* It is never withdrawn here. Losing the CRM attribute while holding no other role
|
||||||
* blocks the account anyway, and a role handed out manually must survive a login.
|
* blocks the account anyway, and a role handed out manually must survive a login.
|
||||||
|
*
|
||||||
|
* Does not flush; the caller decides when to.
|
||||||
*/
|
*/
|
||||||
private function grantTeamerRole(User $user): void
|
public function grantTeamerRole(User $user): void
|
||||||
{
|
{
|
||||||
$grantedRoles = $user->getAssignedRoles();
|
$grantedRoles = $user->getAssignedRoles();
|
||||||
|
|
||||||
@@ -302,9 +341,11 @@ class UserDataHandler
|
|||||||
* super admin can turn one into an actual role, and an already granted role is never
|
* super admin can turn one into an actual role, and an already granted role is never
|
||||||
* marked as pending again.
|
* marked as pending again.
|
||||||
*
|
*
|
||||||
* @param string[] $claimedRoles
|
* Does not flush; the caller decides when to.
|
||||||
|
*
|
||||||
|
* @param string[] $claimedRoles pending markers as returned by toPendingRoles()
|
||||||
*/
|
*/
|
||||||
private function refreshPendingRoles(User $user, array $claimedRoles): void
|
public function refreshPendingRoles(User $user, array $claimedRoles): void
|
||||||
{
|
{
|
||||||
// an approved role needs no marker anymore
|
// an approved role needs no marker anymore
|
||||||
$grantedRoles = $user->getAssignedRoles();
|
$grantedRoles = $user->getAssignedRoles();
|
||||||
|
|||||||
@@ -83,6 +83,59 @@ class UserDataHandlerTest extends TestCase
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The role policy has to be identical whichever identity source claims the role, so
|
||||||
|
* toPendingRoles() must agree with collectPendingRoles() on equivalent input.
|
||||||
|
*
|
||||||
|
* @dataProvider toPendingRolesProvider
|
||||||
|
*/
|
||||||
|
public function testToPendingRolesMatchesTheCrmPolicy(array $claimedRoles, CrmAttributesResponse $crmAttributes, array $expectedRoles): void
|
||||||
|
{
|
||||||
|
$handler = new UserDataHandler($this->entityManager, $this->logger);
|
||||||
|
|
||||||
|
$this->assertSame($expectedRoles, $handler->toPendingRoles($claimedRoles));
|
||||||
|
$this->assertSame($expectedRoles, $handler->collectPendingRoles($crmAttributes));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function toPendingRolesProvider(): iterable
|
||||||
|
{
|
||||||
|
yield 'admin' => [
|
||||||
|
['ROLE_ADMIN'],
|
||||||
|
(new CrmAttributesResponse())->setAdmin(true),
|
||||||
|
[User::PENDING_ROLES['ROLE_ADMIN']],
|
||||||
|
];
|
||||||
|
|
||||||
|
yield 'manager takes precedence over house manager' => [
|
||||||
|
['ROLE_HOUSE_MANAGER', 'ROLE_MANAGER'],
|
||||||
|
(new CrmAttributesResponse())->setManager(true)->setHouseManager(true),
|
||||||
|
[User::PENDING_ROLES['ROLE_MANAGER']],
|
||||||
|
];
|
||||||
|
|
||||||
|
yield 'house manager' => [
|
||||||
|
['ROLE_HOUSE_MANAGER'],
|
||||||
|
(new CrmAttributesResponse())->setHouseManager(true),
|
||||||
|
[User::PENDING_ROLES['ROLE_HOUSE_MANAGER']],
|
||||||
|
];
|
||||||
|
|
||||||
|
yield 'admin and house manager' => [
|
||||||
|
['ROLE_ADMIN', 'ROLE_HOUSE_MANAGER'],
|
||||||
|
(new CrmAttributesResponse())->setAdmin(true)->setHouseManager(true),
|
||||||
|
[User::PENDING_ROLES['ROLE_ADMIN'], User::PENDING_ROLES['ROLE_HOUSE_MANAGER']],
|
||||||
|
];
|
||||||
|
|
||||||
|
yield 'teamer has no marker' => [
|
||||||
|
['ROLE_TEAMER'],
|
||||||
|
(new CrmAttributesResponse())->setTeamer(true),
|
||||||
|
[],
|
||||||
|
];
|
||||||
|
|
||||||
|
yield 'nothing claimed' => [
|
||||||
|
[],
|
||||||
|
new CrmAttributesResponse(),
|
||||||
|
[],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
public function testUpdateLocalUserSyncsUserAndTeamerDataFromBusPro(): void
|
public function testUpdateLocalUserSyncsUserAndTeamerDataFromBusPro(): void
|
||||||
{
|
{
|
||||||
$user = (new User())
|
$user = (new User())
|
||||||
|
|||||||
Reference in New Issue
Block a user