feat: Attribution de rôles multiples par utilisateur
Les utilisateurs Classeo étaient limités à un seul rôle, alors que dans la réalité scolaire un directeur peut aussi être enseignant, ou un parent peut avoir un rôle vie scolaire. Cette limitation obligeait à créer des comptes distincts par fonction. Le modèle User supporte désormais plusieurs rôles simultanés avec basculement via le header. L'admin peut attribuer/retirer des rôles depuis l'interface de gestion, avec des garde-fous : pas d'auto- destitution, pas d'escalade de privilèges (seul SUPER_ADMIN peut attribuer SUPER_ADMIN), vérification du statut actif pour le switch de rôle, et TTL explicite sur le cache de rôle actif.
This commit is contained in:
@@ -16,6 +16,7 @@ use App\Shared\Domain\Clock;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use App\Shared\Infrastructure\RateLimit\LoginRateLimiterInterface;
|
||||
use Lexik\Bundle\JWTAuthenticationBundle\Event\AuthenticationSuccessEvent;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Symfony\Component\HttpFoundation\Cookie;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
@@ -101,6 +102,16 @@ final readonly class LoginSuccessHandler
|
||||
|
||||
$response->headers->setCookie($cookie);
|
||||
|
||||
// Session ID cookie for active role scoping (per-device isolation)
|
||||
$sessionIdCookie = Cookie::create('classeo_sid')
|
||||
->withValue(Uuid::uuid4()->toString())
|
||||
->withExpires($refreshToken->expiresAt)
|
||||
->withPath('/api')
|
||||
->withSecure($isSecure)
|
||||
->withHttpOnly(true)
|
||||
->withSameSite($isSecure ? 'strict' : 'lax');
|
||||
$response->headers->setCookie($sessionIdCookie);
|
||||
|
||||
// Reset the rate limiter for this email
|
||||
$this->rateLimiter->reset($email);
|
||||
|
||||
|
||||
@@ -23,12 +23,10 @@ final readonly class SecurityUserFactory
|
||||
email: (string) $domainUser->email,
|
||||
hashedPassword: $domainUser->hashedPassword ?? '',
|
||||
tenantId: $domainUser->tenantId,
|
||||
roles: [$this->mapRoleToSymfony($domainUser->role)],
|
||||
roles: array_values(array_map(
|
||||
static fn (Role $role) => $role->value,
|
||||
$domainUser->roles,
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
private function mapRoleToSymfony(Role $role): string
|
||||
{
|
||||
return $role->value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ final class UserVoter extends Voter
|
||||
public const string BLOCK = 'USER_BLOCK';
|
||||
public const string UNBLOCK = 'USER_UNBLOCK';
|
||||
public const string RESEND_INVITATION = 'USER_RESEND_INVITATION';
|
||||
public const string MANAGE_ROLES = 'USER_MANAGE_ROLES';
|
||||
|
||||
private const array SUPPORTED_ATTRIBUTES = [
|
||||
self::VIEW,
|
||||
@@ -37,6 +38,7 @@ final class UserVoter extends Voter
|
||||
self::BLOCK,
|
||||
self::UNBLOCK,
|
||||
self::RESEND_INVITATION,
|
||||
self::MANAGE_ROLES,
|
||||
];
|
||||
|
||||
#[Override]
|
||||
@@ -66,7 +68,7 @@ final class UserVoter extends Voter
|
||||
|
||||
return match ($attribute) {
|
||||
self::VIEW => $this->canView($roles),
|
||||
self::CREATE, self::BLOCK, self::UNBLOCK, self::RESEND_INVITATION => $this->canManage($roles),
|
||||
self::CREATE, self::BLOCK, self::UNBLOCK, self::RESEND_INVITATION, self::MANAGE_ROLES => $this->canManage($roles),
|
||||
default => false,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user