diff --git a/src/API/TeamController.php b/src/API/TeamController.php index 22c5252b..b9380d90 100644 --- a/src/API/TeamController.php +++ b/src/API/TeamController.php @@ -71,7 +71,7 @@ final class TeamController extends BaseApiController * Fetch team */ #[IsGranted('view_team')] - #[OA\Response(response: 200, description: 'Returns one team entity', content: new OA\JsonContent(ref: '#/components/schemas/Team'))] + #[OA\Response(response: 200, description: 'Returns the team', content: new OA\JsonContent(ref: '#/components/schemas/Team'))] #[Route(methods: ['GET'], path: '/{id}', name: 'get_team', requirements: ['id' => '\d+'])] public function getAction(Team $team): Response { @@ -84,8 +84,8 @@ final class TeamController extends BaseApiController /** * Delete team */ - #[IsGranted('delete_team')] - #[OA\Delete(responses: [new OA\Response(response: 204, description: 'Delete one team')])] + #[IsGranted('delete', 'team')] + #[OA\Delete(responses: [new OA\Response(response: 204, description: 'Empty')])] #[OA\Parameter(name: 'id', in: 'path', description: 'Team ID to delete', required: true)] #[Route(methods: ['DELETE'], path: '/{id}', name: 'delete_team', requirements: ['id' => '\d+'])] public function deleteAction(Team $team): Response @@ -129,7 +129,7 @@ final class TeamController extends BaseApiController /** * Update team */ - #[IsGranted('edit_team')] + #[IsGranted('edit', 'team')] #[OA\Patch(description: 'Update an existing team, you can pass all or just a subset of all attributes (passing members will replace all existing ones)', responses: [new OA\Response(response: 200, description: 'Returns the updated team', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])] #[OA\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/TeamEditForm'))] #[OA\Parameter(name: 'id', in: 'path', description: 'Team ID to update', required: true)] @@ -169,7 +169,7 @@ final class TeamController extends BaseApiController /** * Add team member */ - #[IsGranted('edit_team')] + #[IsGranted('edit', 'team')] #[OA\Post(responses: [new OA\Response(response: 200, description: 'Adds a new user to a team.', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])] #[OA\Parameter(name: 'id', in: 'path', description: 'The team which will receive the new member', required: true)] #[OA\Parameter(name: 'userId', in: 'path', description: 'The team member to add (User ID)', required: true)] @@ -193,7 +193,7 @@ final class TeamController extends BaseApiController /** * Remove team member */ - #[IsGranted('edit_team')] + #[IsGranted('edit', 'team')] #[OA\Delete(responses: [new OA\Response(response: 200, description: 'Removes a user from the team. The teamlead cannot be removed.', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])] #[OA\Parameter(name: 'id', in: 'path', description: 'The team from which the member will be removed', required: true)] #[OA\Parameter(name: 'userId', in: 'path', description: 'The team member to remove (User ID)', required: true)] @@ -219,10 +219,12 @@ final class TeamController extends BaseApiController } /** - * Grant team access to customer + * Grant customer access + * + * The team is granted access to the customer. */ - #[IsGranted('edit_team')] - #[OA\Post(responses: [new OA\Response(response: 200, description: 'Adds a new customer to a team.', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])] + #[IsGranted('edit', 'team')] + #[OA\Post(responses: [new OA\Response(response: 200, description: 'Returns the team including the customer', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])] #[OA\Parameter(name: 'id', in: 'path', description: 'The team that is granted access', required: true)] #[OA\Parameter(name: 'customerId', in: 'path', description: 'The customer to grant acecess to (Customer ID)', required: true)] #[Route(methods: ['POST'], path: '/{id}/customers/{customerId}', name: 'post_team_customer', requirements: ['id' => '\d+', 'customerId' => '\d+'])] @@ -242,10 +244,12 @@ final class TeamController extends BaseApiController } /** - * Revoke customer access from team + * Revoke customer access + * + * This removes access to the customer from the team. */ - #[IsGranted('edit_team')] - #[OA\Delete(responses: [new OA\Response(response: 200, description: 'Removes a customer from the team.', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])] + #[IsGranted('edit', 'team')] + #[OA\Delete(responses: [new OA\Response(response: 200, description: 'Returns the team without the customer', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])] #[OA\Parameter(name: 'id', in: 'path', description: 'The team whose permission will be revoked', required: true)] #[OA\Parameter(name: 'customerId', in: 'path', description: 'The customer to remove (Customer ID)', required: true)] #[Route(methods: ['DELETE'], path: '/{id}/customers/{customerId}', name: 'delete_team_customer', requirements: ['id' => '\d+', 'customerId' => '\d+'])] @@ -265,10 +269,12 @@ final class TeamController extends BaseApiController } /** - * Grant team access to project + * Grant project access + * + * The team is granted access to the project. */ - #[IsGranted('edit_team')] - #[OA\Post(responses: [new OA\Response(response: 200, description: 'Adds a new project to a team.', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])] + #[IsGranted('edit', 'team')] + #[OA\Post(responses: [new OA\Response(response: 200, description: 'Returns the team including the project', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])] #[OA\Parameter(name: 'id', in: 'path', description: 'The team that is granted access', required: true)] #[OA\Parameter(name: 'projectId', in: 'path', description: 'The project to grant acecess to (Project ID)', required: true)] #[Route(methods: ['POST'], path: '/{id}/projects/{projectId}', name: 'post_team_project', requirements: ['id' => '\d+', 'projectId' => '\d+'])] @@ -288,10 +294,12 @@ final class TeamController extends BaseApiController } /** - * Revoke project access from team + * Revoke project access + * + * This removes access to the project from the team. */ - #[IsGranted('edit_team')] - #[OA\Delete(responses: [new OA\Response(response: 200, description: 'Removes a project from the team.', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])] + #[IsGranted('edit', 'team')] + #[OA\Delete(responses: [new OA\Response(response: 200, description: 'Returns the team without the project', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])] #[OA\Parameter(name: 'id', in: 'path', description: 'The team whose permission will be revoked', required: true)] #[OA\Parameter(name: 'projectId', in: 'path', description: 'The project to remove (Project ID)', required: true)] #[Route(methods: ['DELETE'], path: '/{id}/projects/{projectId}', name: 'delete_team_project', requirements: ['id' => '\d+', 'projectId' => '\d+'])] @@ -311,10 +319,12 @@ final class TeamController extends BaseApiController } /** - * Grant team access to activity + * Grant activity access + * + * The team is granted access to the activity. */ - #[IsGranted('edit_team')] - #[OA\Post(responses: [new OA\Response(response: 200, description: 'Adds a new activity to a team.', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])] + #[IsGranted('edit', 'team')] + #[OA\Post(responses: [new OA\Response(response: 200, description: 'Returns the team including the activity', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])] #[OA\Parameter(name: 'id', in: 'path', description: 'The team that is granted access', required: true)] #[OA\Parameter(name: 'activityId', in: 'path', description: 'The activity to grant acecess to (Activity ID)', required: true)] #[Route(methods: ['POST'], path: '/{id}/activities/{activityId}', name: 'post_team_activity', requirements: ['id' => '\d+', 'activityId' => '\d+'])] @@ -334,10 +344,12 @@ final class TeamController extends BaseApiController } /** - * Revoke activity access from team + * Revoke activity access + * + * This removes access to the activity from the team. */ - #[IsGranted('edit_team')] - #[OA\Delete(responses: [new OA\Response(response: 200, description: 'Removes a activity from the team.', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])] + #[IsGranted('edit', 'team')] + #[OA\Delete(responses: [new OA\Response(response: 200, description: 'Returns the team without the activity', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])] #[OA\Parameter(name: 'id', in: 'path', description: 'The team whose permission will be revoked', required: true)] #[OA\Parameter(name: 'activityId', in: 'path', description: 'The activity to remove (Activity ID)', required: true)] #[Route(methods: ['DELETE'], path: '/{id}/activities/{activityId}', name: 'delete_team_activity', requirements: ['id' => '\d+', 'activityId' => '\d+'])] diff --git a/src/Controller/TeamController.php b/src/Controller/TeamController.php index bb4935bb..7a094abe 100644 --- a/src/Controller/TeamController.php +++ b/src/Controller/TeamController.php @@ -38,15 +38,9 @@ final class TeamController extends AbstractController { } - /** - * @param TeamRepository $repository - * @param Request $request - * @param int $page - * @return Response - */ #[Route(path: '/', defaults: ['page' => 1], name: 'admin_team', methods: ['GET'])] #[Route(path: '/page/{page}', requirements: ['page' => '[1-9]\d*'], name: 'admin_team_paginated', methods: ['GET'])] - public function listTeams(TeamRepository $repository, Request $request, $page): Response + public function listTeams(int $page, TeamRepository $repository, Request $request): Response { $query = new TeamQuery(); $query->setPage($page); @@ -81,10 +75,6 @@ final class TeamController extends AbstractController ]); } - /** - * @param Request $request - * @return Response - */ #[Route(path: '/create', name: 'admin_team_create', methods: ['GET', 'POST'])] #[IsGranted('create_team')] public function createTeam(Request $request): Response diff --git a/src/Voter/TeamVoter.php b/src/Voter/TeamVoter.php index ee1f1366..046701bb 100644 --- a/src/Voter/TeamVoter.php +++ b/src/Voter/TeamVoter.php @@ -24,7 +24,6 @@ final class TeamVoter extends Voter * support rules based on the given $subject (here: Team) */ private const ALLOWED_ATTRIBUTES = [ - 'view', 'edit', 'delete', ]; @@ -52,17 +51,13 @@ final class TeamVoter extends Voter { $user = $token->getUser(); - if (!$user instanceof User) { + if (!$user instanceof User || !($subject instanceof Team)) { return false; } - switch ($attribute) { - case 'edit': - case 'delete': - // changing existing teams should be limited to admins and teamleads - if (!$user->isAdmin() && !$user->isSuperAdmin() && !$user->isTeamleadOf($subject)) { - return false; - } + // changing existing teams should be limited to admins and teamleads + if (!$user->isAdmin() && !$user->isSuperAdmin() && !$user->isTeamleadOf($subject)) { + return false; } return $this->permissionManager->hasRolePermission($user, $attribute . '_team'); diff --git a/tests/Voter/TeamVoterTest.php b/tests/Voter/TeamVoterTest.php index a48cd281..622fce78 100644 --- a/tests/Voter/TeamVoterTest.php +++ b/tests/Voter/TeamVoterTest.php @@ -21,7 +21,7 @@ use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface; class TeamVoterTest extends AbstractVoterTestCase { #[DataProvider('getTestData')] - public function testVote(User $user, $subject, $attribute, $result): void + public function testVote(User $user, mixed $subject, string $attribute, int $result): void { $token = new UsernamePasswordToken($user, 'bar', $user->getRoles()); $sut = $this->getVoter(TeamVoter::class); @@ -29,7 +29,7 @@ class TeamVoterTest extends AbstractVoterTestCase self::assertEquals($result, $sut->vote($token, $subject, [$attribute])); } - public static function getTestData() + public static function getTestData(): iterable { $user0 = self::getUser(0, null); $user1 = self::getUser(1, User::ROLE_USER); @@ -39,42 +39,42 @@ class TeamVoterTest extends AbstractVoterTestCase $team = new Team('foo'); - $result = VoterInterface::ACCESS_ABSTAIN; + $abstain = VoterInterface::ACCESS_ABSTAIN; $allTeamPerms = ['view_team', 'create_team', 'edit_team', 'delete_team']; foreach ($allTeamPerms as $fullPerm) { - yield [$user0, [], $fullPerm, $result]; - yield [$user0, new \stdClass(), $fullPerm, $result]; - yield [$user0, $team, $fullPerm, $result]; - yield [$user1, $team, $fullPerm, $result]; - yield [$user2, $team, $fullPerm, $result]; - yield [$user3, $team, $fullPerm, $result]; - yield [$user4, $team, $fullPerm, $result]; + yield [$user0, [], $fullPerm, $abstain]; + yield [$user0, new \stdClass(), $fullPerm, $abstain]; + yield [$user0, $team, $fullPerm, $abstain]; + yield [$user1, $team, $fullPerm, $abstain]; + yield [$user2, $team, $fullPerm, $abstain]; + yield [$user3, $team, $fullPerm, $abstain]; + yield [$user4, $team, $fullPerm, $abstain]; } - $result = VoterInterface::ACCESS_DENIED; + $denied = VoterInterface::ACCESS_DENIED; - yield [$user0, $team, 'view', $result]; - yield [$user0, $team, 'edit', $result]; - yield [$user0, $team, 'delete', $result]; + yield [$user0, $team, 'view', $abstain]; + yield [$user0, $team, 'edit', $denied]; + yield [$user0, $team, 'delete', $denied]; - yield [$user1, $team, 'view', $result]; - yield [$user1, $team, 'edit', $result]; - yield [$user1, $team, 'delete', $result]; + yield [$user1, $team, 'view', $abstain]; + yield [$user1, $team, 'edit', $denied]; + yield [$user1, $team, 'delete', $denied]; - yield [$user2, $team, 'view', $result]; - yield [$user2, $team, 'edit', $result]; - yield [$user2, $team, 'delete', $result]; + yield [$user2, $team, 'view', $abstain]; + yield [$user2, $team, 'edit', $denied]; + yield [$user2, $team, 'delete', $denied]; - $result = VoterInterface::ACCESS_GRANTED; + $granted = VoterInterface::ACCESS_GRANTED; - yield [$user3, $team, 'view', $result]; - yield [$user3, $team, 'edit', $result]; - yield [$user3, $team, 'delete', $result]; + yield [$user3, $team, 'view', $abstain]; + yield [$user3, $team, 'edit', $granted]; + yield [$user3, $team, 'delete', $granted]; - yield [$user4, $team, 'view', $result]; - yield [$user4, $team, 'edit', $result]; - yield [$user4, $team, 'delete', $result]; + yield [$user4, $team, 'view', $abstain]; + yield [$user4, $team, 'edit', $granted]; + yield [$user4, $team, 'delete', $granted]; } } diff --git a/tests/phpstan.neon b/tests/phpstan.neon index f94d9b0d..c5411e6d 100644 --- a/tests/phpstan.neon +++ b/tests/phpstan.neon @@ -2381,26 +2381,6 @@ parameters: count: 1 path: Voter/RolePermissionVoterTest.php - - - message: "#^Method App\\\\Tests\\\\Voter\\\\TeamVoterTest\\:\\:getTestData\\(\\) has no return type specified\\.$#" - count: 1 - path: Voter/TeamVoterTest.php - - - - message: "#^Method App\\\\Tests\\\\Voter\\\\TeamVoterTest\\:\\:testVote\\(\\) has parameter \\$attribute with no type specified\\.$#" - count: 1 - path: Voter/TeamVoterTest.php - - - - message: "#^Method App\\\\Tests\\\\Voter\\\\TeamVoterTest\\:\\:testVote\\(\\) has parameter \\$result with no type specified\\.$#" - count: 1 - path: Voter/TeamVoterTest.php - - - - message: "#^Method App\\\\Tests\\\\Voter\\\\TeamVoterTest\\:\\:testVote\\(\\) has parameter \\$subject with no type specified\\.$#" - count: 1 - path: Voter/TeamVoterTest.php - - message: "#^Method App\\\\Tests\\\\Voter\\\\TimesheetVoterTest\\:\\:assertVote\\(\\) has parameter \\$attribute with no type specified\\.$#" count: 1