Skip to content

Commit

Permalink
Handling selected member using custom header Member key for user ha…
Browse files Browse the repository at this point in the history
…ving multiple profile linked to them
  • Loading branch information
froozeify committed Dec 22, 2024
1 parent 4d4891a commit 7520717
Show file tree
Hide file tree
Showing 8 changed files with 104 additions and 15 deletions.
3 changes: 3 additions & 0 deletions config/packages/api_platform.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,6 @@ api_platform:
JWT:
name: Authorization
type: header
Member:
name: Member
type: header
2 changes: 1 addition & 1 deletion src/Doctrine/UserClubExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ private function getUser(): User {
private function getUserClubs(): array {
$user = $this->getUser();
$userClubs = [];
foreach ($user->getLinkedClubs() as $club) {
foreach ($user->getLinkedProfiles() as $club) {
$userClubs[] = $club['club'];
}

Expand Down
12 changes: 7 additions & 5 deletions src/Entity/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ class User extends UuidEntity implements UserInterface, PasswordAuthenticatedUse
private UserRole $role = UserRole::user;

#[Groups(['self-read'])]
private array $linkedClubs = [];
private array $linkedProfiles = [];

/**
* @var string The hashed password
Expand Down Expand Up @@ -197,7 +197,7 @@ public function __construct() {

// Custom calculated fields

public function getLinkedClubs(): array {
public function getLinkedProfiles(): array {
$userClubs = [];
$multipleClubs = $this->getMemberships()->count() > 1;

Expand All @@ -208,14 +208,16 @@ public function getLinkedClubs(): array {
'club' => $club,
'role' => $membership->getRole(),
];
$displayName = $club->getName();

if ($membership->getMember()) {
$fullName = $club->getName();
$userClub['member'] = $membership->getMember()->getUuid()->toString();
if ($multipleClubs) {
$fullName .= " - " . $membership->getMember()->getFullName();
$displayName .= " - " . $membership->getMember()->getFullName();
}
$userClub['displayName'] = $fullName;
}

$userClub['displayName'] = $displayName;
$userClubs[] = $userClub;
}
}
Expand Down
22 changes: 19 additions & 3 deletions src/Security/Voter/ClubVoter.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
use App\Service\RequestService;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;

Expand All @@ -35,7 +37,9 @@ protected function supports(string $attribute, mixed $subject): bool {
}

protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool {
$requestMemberUuid = null;
if ($subject instanceof Request) {
$requestMemberUuid = $this->requestService->getMemberUuidFromRequest($subject);
$subject = $this->requestService->getClubFromRequest($subject);
}

Expand Down Expand Up @@ -65,10 +69,22 @@ protected function voteOnAttribute(string $attribute, mixed $subject, TokenInter
return false;
}

foreach ($user->getLinkedClubs() as $club) {
if ($club['club']->getId() === $targetedClub->getId()) {
$linkedProfiles = $user->getLinkedProfiles();
// When user have multiple profiles linked, the member header must be specified
if (!$requestMemberUuid && count($linkedProfiles) > 1) {
throw new HttpException(Response::HTTP_BAD_REQUEST, "Missing required 'Member' header.");
}

foreach ($linkedProfiles as $linkedProfile) {
if ($requestMemberUuid) {
if (!$linkedProfile['member'] || $linkedProfile['member'] !== $requestMemberUuid) {
continue;
}
}

if ($linkedProfile['club']->getId() === $targetedClub->getId()) {
/** @var ClubRole $role */
$role = $club['role'];
$role = $linkedProfile['role'];
return $role->hasRole($targetedClubRole);
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/Service/ClubService.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,13 @@ public function getBadger(Club $club): ?User {
$this->entityManager->persist($user);
$this->entityManager->persist($userMember);
$this->entityManager->flush();
// We refresh so getLinkedClubs() contain the club
// We refresh so getLinkedProfiles() contain the club
$this->entityManager->refresh($user);
}

// We check the club are matching
$matched = false;
foreach ($user->getLinkedClubs() as $dbClub) {
foreach ($user->getLinkedProfiles() as $dbClub) {
if ($dbClub['club'] === $club) {
$matched = true;
break;
Expand Down
4 changes: 4 additions & 0 deletions src/Service/RequestService.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ public function __construct(
) {
}

public function getMemberUuidFromRequest(Request $request): ?string {
return $request->headers->get('Member');
}

public function getClubUuidFromRequest(Request $request): ?string {
$uuid = $request->attributes->get("clubUuid");
$resourceClass = $request->attributes->get('_api_resource_class');
Expand Down
27 changes: 23 additions & 4 deletions tests/AbstractTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ abstract class AbstractTestCase extends ApiTestCase {

private ?string $accessToken = null;
private ?string $refreshToken = null;
private ?string $selectedMember = null;

public function setUp(): void {
parent::setUp();
Expand All @@ -48,16 +49,22 @@ public function debugTestDatabase(): void {
protected function createClientWithCredentials(?string $token = null): Client {
$token = $token ?? $this->accessToken;

$headers = [
'Authorization' => 'Bearer ' . $token,
];
if ($this->selectedMember) {
$headers['Member'] = $this->selectedMember;
}

return static::createClient([], [
'headers' => [
'Authorization' => 'Bearer ' . $token,
],
'headers' => $headers,
]);
}

protected function logout(): void {
$this->accessToken = null;
$this->refreshToken = null;
$this->selectedMember = null;
}

protected function loggedAs(string $email, string $password): bool {
Expand Down Expand Up @@ -86,7 +93,7 @@ protected function loggedAsBadger(Club $club): bool {
'json' => [
'token' => $club->getBadgerToken(),
'club' => $club->getUuid()->toString(),
]
],
]);

if ($response->getStatusCode() !== Response::HTTP_OK) {
Expand All @@ -99,6 +106,18 @@ protected function loggedAsBadger(Club $club): bool {
return true;
}

/**
* Possibility to define in the query headers the selectedProfile.
* Required when the user have multiple profile
*
* @param string|null $uuid
*
* @return void
*/
public function selectedMember(?string $uuid): void {
$this->selectedMember = $uuid;
}

private function checkRequestResponse(ResponseCodeEnum $responseCode, ?array $payloadToValidate): void {
$this->assertResponseStatusCodeSame($responseCode->value);

Expand Down
45 changes: 45 additions & 0 deletions tests/Controller/LoginTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@

namespace App\Tests\Controller;

use App\Enum\ClubRole;
use App\Enum\UserRole;
use App\Tests\AbstractTestCase;
use App\Tests\Enum\ResponseCodeEnum;
use App\Tests\Factory\MemberFactory;
use App\Tests\Factory\UserFactory;
use App\Tests\Factory\UserMemberFactory;
use App\Tests\Story\_InitStory;
use Symfony\Component\HttpFoundation\Response;

class LoginTest extends AbstractTestCase {
Expand Down Expand Up @@ -60,4 +65,44 @@ public function testLoginAsUnknown(): void {
$this->loggedAs("notexisting@test.fr", "test");
$this->assertResponseStatusCodeSame(Response::HTTP_UNAUTHORIZED);
}

public function testLoginWithMultipleProfiles(): void {
$club1 = _InitStory::club_1();

$this->loggedAsAdminClub1();
$response = $this->makeGetRequest('/self');
$this->assertCount(1, $response->toArray()['linkedProfiles']);

// No error when getting members
$this->makeGetRequest($this->getIriFromResource($club1) . '/members');
$this->assertResponseIsSuccessful();

// We linked the admin club 1 with another account
$member = MemberFactory::createOne();
$userMember = UserMemberFactory::createOne([
"member" => $member,
"user" => _InitStory::USER_admin_club_1(),
"role" => ClubRole::member
]);

$response = $this->makeGetRequest('/self');
$this->assertCount(2, $response->toArray()['linkedProfiles']);


$this->makeGetRequest($this->getIriFromResource($club1) . '/members');
$this->assertResponseStatusCodeSame(ResponseCodeEnum::bad_request->value);
$this->assertJsonContains([
"detail" => "Missing required 'Member' header.",
]);

// We try getting as regular member, access should be denied
$this->selectedMember($member->getUuid()->toString());
$this->makeGetRequest($this->getIriFromResource($club1) . '/members');
$this->assertResponseStatusCodeSame(ResponseCodeEnum::forbidden->value);

// As admin club no problem
$this->selectedMember(_InitStory::MEMBER_admin_club_1()->getUuid()->toString());
$this->makeGetRequest($this->getIriFromResource($club1) . '/members');
$this->assertResponseIsSuccessful();
}
}

0 comments on commit 7520717

Please sign in to comment.