From e0dfcbdbf810ab73555d1d3864e327a427c177af Mon Sep 17 00:00:00 2001 From: Matthieu Date: Tue, 17 Mar 2026 15:23:06 +0100 Subject: [PATCH] fix(security) : add role checks on Gitea API resources and all MCP tools - GiteaBranch, GiteaBranchName, GiteaPullRequest: require ROLE_USER - All 22 MCP tools: require ROLE_USER (ROLE_ADMIN for users/clients listing) Tickets: T-002, T-007 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/ApiResource/GiteaBranch.php | 2 ++ src/ApiResource/GiteaBranchName.php | 1 + src/ApiResource/GiteaPullRequest.php | 1 + src/Mcp/Tool/Project/CreateProjectTool.php | 7 +++++++ src/Mcp/Tool/Project/GetProjectTool.php | 7 +++++++ src/Mcp/Tool/Project/ListProjectsTool.php | 7 +++++++ src/Mcp/Tool/Project/UpdateProjectTool.php | 7 +++++++ src/Mcp/Tool/Reference/ListClientsTool.php | 7 +++++++ src/Mcp/Tool/Reference/ListUsersTool.php | 7 +++++++ src/Mcp/Tool/Task/CreateTaskTool.php | 15 ++++++++++++--- src/Mcp/Tool/Task/DeleteTaskTool.php | 7 +++++++ src/Mcp/Tool/Task/GetTaskTool.php | 7 +++++++ src/Mcp/Tool/Task/ListTasksTool.php | 7 +++++++ src/Mcp/Tool/Task/UpdateTaskTool.php | 7 +++++++ src/Mcp/Tool/TaskMeta/CreateGroupTool.php | 7 +++++++ src/Mcp/Tool/TaskMeta/ListEffortsTool.php | 7 +++++++ src/Mcp/Tool/TaskMeta/ListGroupsTool.php | 7 +++++++ src/Mcp/Tool/TaskMeta/ListPrioritiesTool.php | 7 +++++++ src/Mcp/Tool/TaskMeta/ListStatusesTool.php | 7 +++++++ src/Mcp/Tool/TaskMeta/ListTagsTool.php | 7 +++++++ src/Mcp/Tool/TaskMeta/UpdateGroupTool.php | 7 +++++++ src/Mcp/Tool/TimeEntry/CreateTimeEntryTool.php | 7 +++++++ src/Mcp/Tool/TimeEntry/DeleteTimeEntryTool.php | 7 +++++++ src/Mcp/Tool/TimeEntry/ListTimeEntriesTool.php | 7 +++++++ src/Mcp/Tool/TimeEntry/UpdateTimeEntryTool.php | 7 +++++++ 25 files changed, 163 insertions(+), 3 deletions(-) diff --git a/src/ApiResource/GiteaBranch.php b/src/ApiResource/GiteaBranch.php index 8a5e5ed..03de7df 100644 --- a/src/ApiResource/GiteaBranch.php +++ b/src/ApiResource/GiteaBranch.php @@ -17,6 +17,7 @@ use Symfony\Component\Serializer\Attribute\Groups; uriTemplate: '/tasks/{taskId}/gitea/branches', normalizationContext: ['groups' => ['gitea_branch:read']], provider: GiteaBranchProvider::class, + security: "is_granted('ROLE_USER')", ), new Post( uriTemplate: '/tasks/{taskId}/gitea/branches', @@ -24,6 +25,7 @@ use Symfony\Component\Serializer\Attribute\Groups; normalizationContext: ['groups' => ['gitea_branch:read']], provider: GiteaBranchProvider::class, processor: GiteaBranchProcessor::class, + security: "is_granted('ROLE_USER')", ), ], )] diff --git a/src/ApiResource/GiteaBranchName.php b/src/ApiResource/GiteaBranchName.php index f54913d..1310b69 100644 --- a/src/ApiResource/GiteaBranchName.php +++ b/src/ApiResource/GiteaBranchName.php @@ -15,6 +15,7 @@ use Symfony\Component\Serializer\Attribute\Groups; uriTemplate: '/tasks/{taskId}/gitea/branch-name/{type}', normalizationContext: ['groups' => ['gitea_branch_name:read']], provider: GiteaBranchNameProvider::class, + security: "is_granted('ROLE_USER')", ), ], )] diff --git a/src/ApiResource/GiteaPullRequest.php b/src/ApiResource/GiteaPullRequest.php index b8a607d..9992883 100644 --- a/src/ApiResource/GiteaPullRequest.php +++ b/src/ApiResource/GiteaPullRequest.php @@ -15,6 +15,7 @@ use Symfony\Component\Serializer\Attribute\Groups; uriTemplate: '/tasks/{taskId}/gitea/pull-requests', normalizationContext: ['groups' => ['gitea_pr:read']], provider: GiteaPullRequestProvider::class, + security: "is_granted('ROLE_USER')", ), ], )] diff --git a/src/Mcp/Tool/Project/CreateProjectTool.php b/src/Mcp/Tool/Project/CreateProjectTool.php index 8daeb7e..b901ab1 100644 --- a/src/Mcp/Tool/Project/CreateProjectTool.php +++ b/src/Mcp/Tool/Project/CreateProjectTool.php @@ -10,6 +10,8 @@ use App\Repository\ClientRepository; use Doctrine\ORM\EntityManagerInterface; use InvalidArgumentException; use Mcp\Capability\Attribute\McpTool; +use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\Security\Core\Exception\AccessDeniedException; use function sprintf; @@ -19,6 +21,7 @@ class CreateProjectTool public function __construct( private readonly EntityManagerInterface $entityManager, private readonly ClientRepository $clientRepository, + private readonly Security $security, ) {} public function __invoke( @@ -28,6 +31,10 @@ class CreateProjectTool ?string $color = null, ?int $clientId = null, ): string { + if (!$this->security->isGranted('ROLE_USER')) { + throw new AccessDeniedException('Access denied: ROLE_USER required.'); + } + $project = new Project(); $project->setName($name); $project->setCode($code); diff --git a/src/Mcp/Tool/Project/GetProjectTool.php b/src/Mcp/Tool/Project/GetProjectTool.php index a45796a..8674fd4 100644 --- a/src/Mcp/Tool/Project/GetProjectTool.php +++ b/src/Mcp/Tool/Project/GetProjectTool.php @@ -9,6 +9,8 @@ use App\Repository\ProjectRepository; use App\Repository\TaskRepository; use InvalidArgumentException; use Mcp\Capability\Attribute\McpTool; +use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\Security\Core\Exception\AccessDeniedException; use function sprintf; @@ -18,10 +20,15 @@ class GetProjectTool public function __construct( private readonly ProjectRepository $projectRepository, private readonly TaskRepository $taskRepository, + private readonly Security $security, ) {} public function __invoke(int $id): string { + if (!$this->security->isGranted('ROLE_USER')) { + throw new AccessDeniedException('Access denied: ROLE_USER required.'); + } + $project = $this->projectRepository->find($id); if (null === $project) { diff --git a/src/Mcp/Tool/Project/ListProjectsTool.php b/src/Mcp/Tool/Project/ListProjectsTool.php index ef0f737..1d3e227 100644 --- a/src/Mcp/Tool/Project/ListProjectsTool.php +++ b/src/Mcp/Tool/Project/ListProjectsTool.php @@ -7,16 +7,23 @@ namespace App\Mcp\Tool\Project; use App\Mcp\Tool\Serializer; use App\Repository\ProjectRepository; use Mcp\Capability\Attribute\McpTool; +use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\Security\Core\Exception\AccessDeniedException; #[McpTool(name: 'list-projects', description: 'List all projects with optional archive filter')] class ListProjectsTool { public function __construct( private readonly ProjectRepository $projectRepository, + private readonly Security $security, ) {} public function __invoke(bool $archived = false): string { + if (!$this->security->isGranted('ROLE_USER')) { + throw new AccessDeniedException('Access denied: ROLE_USER required.'); + } + $projects = $this->projectRepository->findBy(['archived' => $archived], ['name' => 'ASC']); return json_encode(array_map(Serializer::project(...), $projects)); diff --git a/src/Mcp/Tool/Project/UpdateProjectTool.php b/src/Mcp/Tool/Project/UpdateProjectTool.php index 3ed6e27..d2d9460 100644 --- a/src/Mcp/Tool/Project/UpdateProjectTool.php +++ b/src/Mcp/Tool/Project/UpdateProjectTool.php @@ -10,6 +10,8 @@ use App\Repository\ProjectRepository; use Doctrine\ORM\EntityManagerInterface; use InvalidArgumentException; use Mcp\Capability\Attribute\McpTool; +use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\Security\Core\Exception\AccessDeniedException; use function sprintf; @@ -20,6 +22,7 @@ class UpdateProjectTool private readonly ProjectRepository $projectRepository, private readonly ClientRepository $clientRepository, private readonly EntityManagerInterface $entityManager, + private readonly Security $security, ) {} public function __invoke( @@ -31,6 +34,10 @@ class UpdateProjectTool ?int $clientId = null, ?bool $archived = null, ): string { + if (!$this->security->isGranted('ROLE_USER')) { + throw new AccessDeniedException('Access denied: ROLE_USER required.'); + } + $project = $this->projectRepository->find($id); if (null === $project) { diff --git a/src/Mcp/Tool/Reference/ListClientsTool.php b/src/Mcp/Tool/Reference/ListClientsTool.php index 8c29a65..29ef197 100644 --- a/src/Mcp/Tool/Reference/ListClientsTool.php +++ b/src/Mcp/Tool/Reference/ListClientsTool.php @@ -6,16 +6,23 @@ namespace App\Mcp\Tool\Reference; use App\Repository\ClientRepository; use Mcp\Capability\Attribute\McpTool; +use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\Security\Core\Exception\AccessDeniedException; #[McpTool(name: 'list-clients', description: 'List all clients with their IDs, names, and emails. Use this to discover valid client IDs for project parameters.')] class ListClientsTool { public function __construct( private readonly ClientRepository $clientRepository, + private readonly Security $security, ) {} public function __invoke(): string { + if (!$this->security->isGranted('ROLE_ADMIN')) { + throw new AccessDeniedException('Access denied: ROLE_ADMIN required.'); + } + $clients = $this->clientRepository->findBy([], ['name' => 'ASC']); return json_encode(array_map(fn ($client) => [ diff --git a/src/Mcp/Tool/Reference/ListUsersTool.php b/src/Mcp/Tool/Reference/ListUsersTool.php index 0115935..e602047 100644 --- a/src/Mcp/Tool/Reference/ListUsersTool.php +++ b/src/Mcp/Tool/Reference/ListUsersTool.php @@ -6,16 +6,23 @@ namespace App\Mcp\Tool\Reference; use App\Repository\UserRepository; use Mcp\Capability\Attribute\McpTool; +use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\Security\Core\Exception\AccessDeniedException; #[McpTool(name: 'list-users', description: 'List all users with their IDs and usernames. Use this to discover valid user IDs for assignee or time entry parameters.')] class ListUsersTool { public function __construct( private readonly UserRepository $userRepository, + private readonly Security $security, ) {} public function __invoke(): string { + if (!$this->security->isGranted('ROLE_ADMIN')) { + throw new AccessDeniedException('Access denied: ROLE_ADMIN required.'); + } + $users = $this->userRepository->findBy([], ['username' => 'ASC']); return json_encode(array_map(fn ($user) => [ diff --git a/src/Mcp/Tool/Task/CreateTaskTool.php b/src/Mcp/Tool/Task/CreateTaskTool.php index 9e71ea3..32a7568 100644 --- a/src/Mcp/Tool/Task/CreateTaskTool.php +++ b/src/Mcp/Tool/Task/CreateTaskTool.php @@ -17,6 +17,8 @@ use App\Repository\UserRepository; use Doctrine\ORM\EntityManagerInterface; use InvalidArgumentException; use Mcp\Capability\Attribute\McpTool; +use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\Security\Core\Exception\AccessDeniedException; use function sprintf; @@ -33,6 +35,7 @@ class CreateTaskTool private readonly TaskGroupRepository $taskGroupRepository, private readonly TaskTagRepository $taskTagRepository, private readonly UserRepository $userRepository, + private readonly Security $security, ) {} public function __invoke( @@ -46,6 +49,10 @@ class CreateTaskTool ?int $groupId = null, ?array $tagIds = null, ): string { + if (!$this->security->isGranted('ROLE_USER')) { + throw new AccessDeniedException('Access denied: ROLE_USER required.'); + } + $project = $this->projectRepository->find($projectId); if (null === $project) { throw new InvalidArgumentException(sprintf('Project with ID %d not found.', $projectId)); @@ -54,7 +61,6 @@ class CreateTaskTool $task = new Task(); $task->setProject($project); $task->setTitle($title); - $task->setNumber($this->taskRepository->findMaxNumberByProjectForUpdate($project) + 1); if (null !== $description) { $task->setDescription($description); @@ -104,8 +110,11 @@ class CreateTaskTool } } - $this->entityManager->persist($task); - $this->entityManager->flush(); + $this->entityManager->wrapInTransaction(function () use ($task, $project): void { + $task->setNumber($this->taskRepository->findMaxNumberByProjectForUpdate($project) + 1); + $this->entityManager->persist($task); + $this->entityManager->flush(); + }); return json_encode([ 'id' => $task->getId(), diff --git a/src/Mcp/Tool/Task/DeleteTaskTool.php b/src/Mcp/Tool/Task/DeleteTaskTool.php index 788d972..7c8644a 100644 --- a/src/Mcp/Tool/Task/DeleteTaskTool.php +++ b/src/Mcp/Tool/Task/DeleteTaskTool.php @@ -8,6 +8,8 @@ use App\Repository\TaskRepository; use Doctrine\ORM\EntityManagerInterface; use InvalidArgumentException; use Mcp\Capability\Attribute\McpTool; +use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\Security\Core\Exception\AccessDeniedException; use function sprintf; @@ -17,10 +19,15 @@ class DeleteTaskTool public function __construct( private readonly TaskRepository $taskRepository, private readonly EntityManagerInterface $entityManager, + private readonly Security $security, ) {} public function __invoke(int $id): string { + if (!$this->security->isGranted('ROLE_USER')) { + throw new AccessDeniedException('Access denied: ROLE_USER required.'); + } + $task = $this->taskRepository->find($id); if (null === $task) { diff --git a/src/Mcp/Tool/Task/GetTaskTool.php b/src/Mcp/Tool/Task/GetTaskTool.php index b0696cf..7a3440c 100644 --- a/src/Mcp/Tool/Task/GetTaskTool.php +++ b/src/Mcp/Tool/Task/GetTaskTool.php @@ -8,6 +8,8 @@ use App\Mcp\Tool\Serializer; use App\Repository\TaskRepository; use InvalidArgumentException; use Mcp\Capability\Attribute\McpTool; +use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\Security\Core\Exception\AccessDeniedException; use function sprintf; @@ -16,10 +18,15 @@ class GetTaskTool { public function __construct( private readonly TaskRepository $taskRepository, + private readonly Security $security, ) {} public function __invoke(int $id): string { + if (!$this->security->isGranted('ROLE_USER')) { + throw new AccessDeniedException('Access denied: ROLE_USER required.'); + } + $task = $this->taskRepository->find($id); if (null === $task) { diff --git a/src/Mcp/Tool/Task/ListTasksTool.php b/src/Mcp/Tool/Task/ListTasksTool.php index fa9b397..92de120 100644 --- a/src/Mcp/Tool/Task/ListTasksTool.php +++ b/src/Mcp/Tool/Task/ListTasksTool.php @@ -7,12 +7,15 @@ namespace App\Mcp\Tool\Task; use App\Mcp\Tool\Serializer; use App\Repository\TaskRepository; use Mcp\Capability\Attribute\McpTool; +use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\Security\Core\Exception\AccessDeniedException; #[McpTool(name: 'list-tasks', description: 'List tasks with optional filters by project, status, assignee, priority, group, tags, and archive state. Returns max 100 results by default, use filters to narrow down.')] class ListTasksTool { public function __construct( private readonly TaskRepository $taskRepository, + private readonly Security $security, ) {} public function __invoke( @@ -25,6 +28,10 @@ class ListTasksTool bool $archived = false, int $limit = 100, ): string { + if (!$this->security->isGranted('ROLE_USER')) { + throw new AccessDeniedException('Access denied: ROLE_USER required.'); + } + $limit = min($limit, 200); $qb = $this->taskRepository->createQueryBuilder('t') diff --git a/src/Mcp/Tool/Task/UpdateTaskTool.php b/src/Mcp/Tool/Task/UpdateTaskTool.php index 74efcad..4f8efbc 100644 --- a/src/Mcp/Tool/Task/UpdateTaskTool.php +++ b/src/Mcp/Tool/Task/UpdateTaskTool.php @@ -15,6 +15,8 @@ use App\Repository\UserRepository; use Doctrine\ORM\EntityManagerInterface; use InvalidArgumentException; use Mcp\Capability\Attribute\McpTool; +use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\Security\Core\Exception\AccessDeniedException; use function sprintf; @@ -30,6 +32,7 @@ class UpdateTaskTool private readonly TaskGroupRepository $taskGroupRepository, private readonly TaskTagRepository $taskTagRepository, private readonly UserRepository $userRepository, + private readonly Security $security, ) {} public function __invoke( @@ -44,6 +47,10 @@ class UpdateTaskTool ?array $tagIds = null, ?bool $archived = null, ): string { + if (!$this->security->isGranted('ROLE_USER')) { + throw new AccessDeniedException('Access denied: ROLE_USER required.'); + } + $task = $this->taskRepository->find($id); if (null === $task) { diff --git a/src/Mcp/Tool/TaskMeta/CreateGroupTool.php b/src/Mcp/Tool/TaskMeta/CreateGroupTool.php index f344dde..a65a2e7 100644 --- a/src/Mcp/Tool/TaskMeta/CreateGroupTool.php +++ b/src/Mcp/Tool/TaskMeta/CreateGroupTool.php @@ -10,6 +10,8 @@ use App\Repository\ProjectRepository; use Doctrine\ORM\EntityManagerInterface; use InvalidArgumentException; use Mcp\Capability\Attribute\McpTool; +use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\Security\Core\Exception\AccessDeniedException; use function sprintf; @@ -19,6 +21,7 @@ class CreateGroupTool public function __construct( private readonly EntityManagerInterface $entityManager, private readonly ProjectRepository $projectRepository, + private readonly Security $security, ) {} public function __invoke( @@ -27,6 +30,10 @@ class CreateGroupTool ?string $description = null, ?string $color = null, ): string { + if (!$this->security->isGranted('ROLE_USER')) { + throw new AccessDeniedException('Access denied: ROLE_USER required.'); + } + $project = $this->projectRepository->find($projectId); if (null === $project) { throw new InvalidArgumentException(sprintf('Project with ID %d not found.', $projectId)); diff --git a/src/Mcp/Tool/TaskMeta/ListEffortsTool.php b/src/Mcp/Tool/TaskMeta/ListEffortsTool.php index 1dbd4ad..c7d67d0 100644 --- a/src/Mcp/Tool/TaskMeta/ListEffortsTool.php +++ b/src/Mcp/Tool/TaskMeta/ListEffortsTool.php @@ -6,16 +6,23 @@ namespace App\Mcp\Tool\TaskMeta; use App\Repository\TaskEffortRepository; use Mcp\Capability\Attribute\McpTool; +use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\Security\Core\Exception\AccessDeniedException; #[McpTool(name: 'list-efforts', description: 'List all task effort levels. Efforts are global (shared across all projects).')] class ListEffortsTool { public function __construct( private readonly TaskEffortRepository $taskEffortRepository, + private readonly Security $security, ) {} public function __invoke(): string { + if (!$this->security->isGranted('ROLE_USER')) { + throw new AccessDeniedException('Access denied: ROLE_USER required.'); + } + $efforts = $this->taskEffortRepository->findBy([], ['label' => 'ASC']); return json_encode(array_map(fn ($e) => [ diff --git a/src/Mcp/Tool/TaskMeta/ListGroupsTool.php b/src/Mcp/Tool/TaskMeta/ListGroupsTool.php index d79d32b..373e7f1 100644 --- a/src/Mcp/Tool/TaskMeta/ListGroupsTool.php +++ b/src/Mcp/Tool/TaskMeta/ListGroupsTool.php @@ -7,16 +7,23 @@ namespace App\Mcp\Tool\TaskMeta; use App\Mcp\Tool\Serializer; use App\Repository\TaskGroupRepository; use Mcp\Capability\Attribute\McpTool; +use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\Security\Core\Exception\AccessDeniedException; #[McpTool(name: 'list-groups', description: 'List task groups, optionally filtered by project. Groups are per-project (each group belongs to one project).')] class ListGroupsTool { public function __construct( private readonly TaskGroupRepository $taskGroupRepository, + private readonly Security $security, ) {} public function __invoke(?int $projectId = null, bool $archived = false): string { + if (!$this->security->isGranted('ROLE_USER')) { + throw new AccessDeniedException('Access denied: ROLE_USER required.'); + } + $criteria = ['archived' => $archived]; if (null !== $projectId) { $criteria['project'] = $projectId; diff --git a/src/Mcp/Tool/TaskMeta/ListPrioritiesTool.php b/src/Mcp/Tool/TaskMeta/ListPrioritiesTool.php index 53f3dba..df06447 100644 --- a/src/Mcp/Tool/TaskMeta/ListPrioritiesTool.php +++ b/src/Mcp/Tool/TaskMeta/ListPrioritiesTool.php @@ -6,16 +6,23 @@ namespace App\Mcp\Tool\TaskMeta; use App\Repository\TaskPriorityRepository; use Mcp\Capability\Attribute\McpTool; +use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\Security\Core\Exception\AccessDeniedException; #[McpTool(name: 'list-priorities', description: 'List all task priorities. Priorities are global (shared across all projects).')] class ListPrioritiesTool { public function __construct( private readonly TaskPriorityRepository $taskPriorityRepository, + private readonly Security $security, ) {} public function __invoke(): string { + if (!$this->security->isGranted('ROLE_USER')) { + throw new AccessDeniedException('Access denied: ROLE_USER required.'); + } + $priorities = $this->taskPriorityRepository->findBy([], ['label' => 'ASC']); return json_encode(array_map(fn ($p) => [ diff --git a/src/Mcp/Tool/TaskMeta/ListStatusesTool.php b/src/Mcp/Tool/TaskMeta/ListStatusesTool.php index 9f09597..b516933 100644 --- a/src/Mcp/Tool/TaskMeta/ListStatusesTool.php +++ b/src/Mcp/Tool/TaskMeta/ListStatusesTool.php @@ -6,16 +6,23 @@ namespace App\Mcp\Tool\TaskMeta; use App\Repository\TaskStatusRepository; use Mcp\Capability\Attribute\McpTool; +use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\Security\Core\Exception\AccessDeniedException; #[McpTool(name: 'list-statuses', description: 'List all task statuses ordered by position. Statuses are global (shared across all projects). Use the returned IDs when creating or updating tasks.')] class ListStatusesTool { public function __construct( private readonly TaskStatusRepository $taskStatusRepository, + private readonly Security $security, ) {} public function __invoke(): string { + if (!$this->security->isGranted('ROLE_USER')) { + throw new AccessDeniedException('Access denied: ROLE_USER required.'); + } + $statuses = $this->taskStatusRepository->findBy([], ['position' => 'ASC']); return json_encode(array_map(fn ($s) => [ diff --git a/src/Mcp/Tool/TaskMeta/ListTagsTool.php b/src/Mcp/Tool/TaskMeta/ListTagsTool.php index b91f271..d43287d 100644 --- a/src/Mcp/Tool/TaskMeta/ListTagsTool.php +++ b/src/Mcp/Tool/TaskMeta/ListTagsTool.php @@ -6,16 +6,23 @@ namespace App\Mcp\Tool\TaskMeta; use App\Repository\TaskTagRepository; use Mcp\Capability\Attribute\McpTool; +use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\Security\Core\Exception\AccessDeniedException; #[McpTool(name: 'list-tags', description: 'List all task tags. Tags are global (shared across all projects).')] class ListTagsTool { public function __construct( private readonly TaskTagRepository $taskTagRepository, + private readonly Security $security, ) {} public function __invoke(): string { + if (!$this->security->isGranted('ROLE_USER')) { + throw new AccessDeniedException('Access denied: ROLE_USER required.'); + } + $tags = $this->taskTagRepository->findBy([], ['label' => 'ASC']); return json_encode(array_map(fn ($t) => [ diff --git a/src/Mcp/Tool/TaskMeta/UpdateGroupTool.php b/src/Mcp/Tool/TaskMeta/UpdateGroupTool.php index e1c7dc0..1b78ac0 100644 --- a/src/Mcp/Tool/TaskMeta/UpdateGroupTool.php +++ b/src/Mcp/Tool/TaskMeta/UpdateGroupTool.php @@ -9,6 +9,8 @@ use App\Repository\TaskGroupRepository; use Doctrine\ORM\EntityManagerInterface; use InvalidArgumentException; use Mcp\Capability\Attribute\McpTool; +use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\Security\Core\Exception\AccessDeniedException; use function sprintf; @@ -18,6 +20,7 @@ class UpdateGroupTool public function __construct( private readonly TaskGroupRepository $taskGroupRepository, private readonly EntityManagerInterface $entityManager, + private readonly Security $security, ) {} public function __invoke( @@ -27,6 +30,10 @@ class UpdateGroupTool ?string $color = null, ?bool $archived = null, ): string { + if (!$this->security->isGranted('ROLE_USER')) { + throw new AccessDeniedException('Access denied: ROLE_USER required.'); + } + $group = $this->taskGroupRepository->find($id); if (null === $group) { diff --git a/src/Mcp/Tool/TimeEntry/CreateTimeEntryTool.php b/src/Mcp/Tool/TimeEntry/CreateTimeEntryTool.php index 3d14def..84b702f 100644 --- a/src/Mcp/Tool/TimeEntry/CreateTimeEntryTool.php +++ b/src/Mcp/Tool/TimeEntry/CreateTimeEntryTool.php @@ -16,6 +16,8 @@ use DateTimeImmutable; use Doctrine\ORM\EntityManagerInterface; use InvalidArgumentException; use Mcp\Capability\Attribute\McpTool; +use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\Security\Core\Exception\AccessDeniedException; use function sprintf; @@ -30,6 +32,7 @@ class CreateTimeEntryTool private readonly TaskTagRepository $taskTagRepository, private readonly TimeEntryRepository $timeEntryRepository, private readonly ClientTicketRepository $clientTicketRepository, + private readonly Security $security, ) {} public function __invoke( @@ -43,6 +46,10 @@ class CreateTimeEntryTool ?string $description = null, ?int $clientTicketId = null, ): string { + if (!$this->security->isGranted('ROLE_USER')) { + throw new AccessDeniedException('Access denied: ROLE_USER required.'); + } + $user = $this->userRepository->find($userId); if (null === $user) { throw new InvalidArgumentException(sprintf('User with ID %d not found.', $userId)); diff --git a/src/Mcp/Tool/TimeEntry/DeleteTimeEntryTool.php b/src/Mcp/Tool/TimeEntry/DeleteTimeEntryTool.php index 2581b12..2e50a0f 100644 --- a/src/Mcp/Tool/TimeEntry/DeleteTimeEntryTool.php +++ b/src/Mcp/Tool/TimeEntry/DeleteTimeEntryTool.php @@ -8,6 +8,8 @@ use App\Repository\TimeEntryRepository; use Doctrine\ORM\EntityManagerInterface; use InvalidArgumentException; use Mcp\Capability\Attribute\McpTool; +use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\Security\Core\Exception\AccessDeniedException; use function sprintf; @@ -17,10 +19,15 @@ class DeleteTimeEntryTool public function __construct( private readonly TimeEntryRepository $timeEntryRepository, private readonly EntityManagerInterface $entityManager, + private readonly Security $security, ) {} public function __invoke(int $id): string { + if (!$this->security->isGranted('ROLE_USER')) { + throw new AccessDeniedException('Access denied: ROLE_USER required.'); + } + $entry = $this->timeEntryRepository->find($id); if (null === $entry) { diff --git a/src/Mcp/Tool/TimeEntry/ListTimeEntriesTool.php b/src/Mcp/Tool/TimeEntry/ListTimeEntriesTool.php index 09980f5..a89e8a8 100644 --- a/src/Mcp/Tool/TimeEntry/ListTimeEntriesTool.php +++ b/src/Mcp/Tool/TimeEntry/ListTimeEntriesTool.php @@ -8,12 +8,15 @@ use App\Mcp\Tool\Serializer; use App\Repository\TimeEntryRepository; use DateTimeImmutable; use Mcp\Capability\Attribute\McpTool; +use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\Security\Core\Exception\AccessDeniedException; #[McpTool(name: 'list-time-entries', description: 'List time entries with optional filters. Duration is computed in minutes and null for active timers.')] class ListTimeEntriesTool { public function __construct( private readonly TimeEntryRepository $timeEntryRepository, + private readonly Security $security, ) {} public function __invoke( @@ -25,6 +28,10 @@ class ListTimeEntriesTool ?string $endDate = null, int $limit = 100, ): string { + if (!$this->security->isGranted('ROLE_USER')) { + throw new AccessDeniedException('Access denied: ROLE_USER required.'); + } + $limit = min($limit, 200); $qb = $this->timeEntryRepository->createQueryBuilder('te') diff --git a/src/Mcp/Tool/TimeEntry/UpdateTimeEntryTool.php b/src/Mcp/Tool/TimeEntry/UpdateTimeEntryTool.php index a25f4b0..ccd3d54 100644 --- a/src/Mcp/Tool/TimeEntry/UpdateTimeEntryTool.php +++ b/src/Mcp/Tool/TimeEntry/UpdateTimeEntryTool.php @@ -14,6 +14,8 @@ use DateTimeImmutable; use Doctrine\ORM\EntityManagerInterface; use InvalidArgumentException; use Mcp\Capability\Attribute\McpTool; +use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\Security\Core\Exception\AccessDeniedException; use function sprintf; @@ -27,6 +29,7 @@ class UpdateTimeEntryTool private readonly TaskTagRepository $taskTagRepository, private readonly ClientTicketRepository $clientTicketRepository, private readonly EntityManagerInterface $entityManager, + private readonly Security $security, ) {} public function __invoke( @@ -40,6 +43,10 @@ class UpdateTimeEntryTool ?string $description = null, ?int $clientTicketId = null, ): string { + if (!$this->security->isGranted('ROLE_USER')) { + throw new AccessDeniedException('Access denied: ROLE_USER required.'); + } + $entry = $this->timeEntryRepository->find($id); if (null === $entry) {