diff --git a/config/packages/kimai.yaml b/config/packages/kimai.yaml index b50736cc..96a66036 100644 --- a/config/packages/kimai.yaml +++ b/config/packages/kimai.yaml @@ -105,15 +105,16 @@ kimai: TEAMS: ['view_team','create_team','edit_team','delete_team'] LOCKDOWN: ['lockdown_grace_timesheet','lockdown_override_timesheet'] REPORTING: ['view_reporting','view_other_reporting','project_reporting','customer_reporting'] + EVERYONE: ['api_access'] # permissions which are deactivated, as these features are hidden for now # brave users can try to activate them and be surprised what happens REGISTER_BETA: [] # mapping a "role name" to an array of "set names" maps: - ROLE_USER: ['TIMESHEET','PROFILE'] - ROLE_TEAMLEAD: ['ACTIVITIES_TEAMLEAD','PROJECTS_TEAMLEAD','CUSTOMERS_TEAMLEAD','TIMESHEET_OTHER','INVOICE','TIMESHEET','PROFILE','EXPORT','BILLABLE','TAGS','REPORTING'] - ROLE_ADMIN: ['ACTIVITIES','PROJECTS','CUSTOMERS','INVOICE','INVOICE_ADMIN','TIMESHEET','TIMESHEET_OTHER','PROFILE','TEAMS','RATE','RATE_OTHER','EXPORT','BILLABLE','TAGS','LOCKDOWN','REPORTING'] - ROLE_SUPER_ADMIN: ['ACTIVITIES','PROJECTS','CUSTOMERS','INVOICE','INVOICE_ADMIN','TIMESHEET','TIMESHEET_OTHER','PROFILE','PROFILE_OTHER','USER','TEAMS','RATE','RATE_OTHER','EXPORT','BILLABLE','TAGS','LOCKDOWN','REPORTING'] + ROLE_USER: ['TIMESHEET','PROFILE', 'EVERYONE'] + ROLE_TEAMLEAD: ['ACTIVITIES_TEAMLEAD','PROJECTS_TEAMLEAD','CUSTOMERS_TEAMLEAD','TIMESHEET_OTHER','INVOICE','TIMESHEET','PROFILE','EXPORT','BILLABLE','TAGS','REPORTING', 'EVERYONE'] + ROLE_ADMIN: ['ACTIVITIES','PROJECTS','CUSTOMERS','INVOICE','INVOICE_ADMIN','TIMESHEET','TIMESHEET_OTHER','PROFILE','TEAMS','RATE','RATE_OTHER','EXPORT','BILLABLE','TAGS','LOCKDOWN','REPORTING', 'EVERYONE'] + ROLE_SUPER_ADMIN: ['ACTIVITIES','PROJECTS','CUSTOMERS','INVOICE','INVOICE_ADMIN','TIMESHEET','TIMESHEET_OTHER','PROFILE','PROFILE_OTHER','USER','TEAMS','RATE','RATE_OTHER','EXPORT','BILLABLE','TAGS','LOCKDOWN','REPORTING', 'EVERYONE'] # mapping a "role name" to an array of "permission names" roles: ROLE_USER: ['view_team_member','time_team_project','create_tag','view_reporting'] diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 03c9cdf1..f2159d83 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -20,6 +20,7 @@ security: api: access_token: token_handler: App\API\Authentication\AccessTokenHandler + success_handler: App\API\Authentication\AccessTokenSuccessHandler remember_me: false request_matcher: App\API\Authentication\ApiRequestMatcher user_checker: App\Security\UserChecker diff --git a/src/API/ActionsController.php b/src/API/ActionsController.php index 5f5bc955..a5ec4c08 100644 --- a/src/API/ActionsController.php +++ b/src/API/ActionsController.php @@ -26,7 +26,7 @@ use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; use Symfony\Contracts\Translation\TranslatorInterface; #[Route(path: '/actions')] -#[IsGranted('IS_AUTHENTICATED')] +#[IsGranted('API')] #[OA\Tag(name: 'Actions')] final class ActionsController extends BaseApiController { diff --git a/src/API/ActivityController.php b/src/API/ActivityController.php index 0ba5f52f..4d6e2780 100644 --- a/src/API/ActivityController.php +++ b/src/API/ActivityController.php @@ -33,7 +33,7 @@ use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Attribute\IsGranted; #[Route(path: '/activities')] -#[IsGranted('IS_AUTHENTICATED')] +#[IsGranted('API')] #[OA\Tag(name: 'Activity')] final class ActivityController extends BaseApiController { diff --git a/src/API/Authentication/AccessTokenSuccessHandler.php b/src/API/Authentication/AccessTokenSuccessHandler.php new file mode 100644 index 00000000..c763d596 --- /dev/null +++ b/src/API/Authentication/AccessTokenSuccessHandler.php @@ -0,0 +1,25 @@ +setAttribute('api-token', true); + + return null; + } +} diff --git a/src/API/ConfigurationController.php b/src/API/ConfigurationController.php index 44cb469c..c0c16900 100644 --- a/src/API/ConfigurationController.php +++ b/src/API/ConfigurationController.php @@ -19,11 +19,11 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Attribute\IsGranted; -#[IsGranted('IS_AUTHENTICATED')] +#[IsGranted('API')] #[OA\Tag(name: 'Default')] final class ConfigurationController extends BaseApiController { - public function __construct(private ViewHandlerInterface $viewHandler) + public function __construct(private readonly ViewHandlerInterface $viewHandler) { } diff --git a/src/API/CustomerController.php b/src/API/CustomerController.php index c4c3fcd2..ef839453 100644 --- a/src/API/CustomerController.php +++ b/src/API/CustomerController.php @@ -33,7 +33,7 @@ use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Attribute\IsGranted; #[Route(path: '/customers')] -#[IsGranted('IS_AUTHENTICATED')] +#[IsGranted('API')] #[OA\Tag(name: 'Customer')] final class CustomerController extends BaseApiController { diff --git a/src/API/ProjectController.php b/src/API/ProjectController.php index 3cc853d8..80e787da 100644 --- a/src/API/ProjectController.php +++ b/src/API/ProjectController.php @@ -35,7 +35,7 @@ use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\Validator\Constraints; #[Route(path: '/projects')] -#[IsGranted('IS_AUTHENTICATED')] +#[IsGranted('API')] #[OA\Tag(name: 'Project')] final class ProjectController extends BaseApiController { diff --git a/src/API/StatusController.php b/src/API/StatusController.php index a34a1c19..b2edb545 100644 --- a/src/API/StatusController.php +++ b/src/API/StatusController.php @@ -20,11 +20,11 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Attribute\IsGranted; -#[IsGranted('IS_AUTHENTICATED')] +#[IsGranted('API')] #[OA\Tag(name: 'Default')] final class StatusController extends BaseApiController { - public function __construct(private ViewHandlerInterface $viewHandler) + public function __construct(private readonly ViewHandlerInterface $viewHandler) { } diff --git a/src/API/TagController.php b/src/API/TagController.php index b3eace3f..333ce49a 100644 --- a/src/API/TagController.php +++ b/src/API/TagController.php @@ -23,7 +23,7 @@ use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Attribute\IsGranted; #[Route(path: '/tags')] -#[IsGranted('IS_AUTHENTICATED')] +#[IsGranted('API')] #[OA\Tag(name: 'Tag')] final class TagController extends BaseApiController { @@ -31,7 +31,10 @@ final class TagController extends BaseApiController public const GROUPS_ENTITY = ['Default', 'Entity', 'Tag']; public const GROUPS_FORM = ['Default', 'Entity', 'Tag']; - public function __construct(private ViewHandlerInterface $viewHandler, private TagRepository $repository) + public function __construct( + private readonly ViewHandlerInterface $viewHandler, + private readonly TagRepository $repository + ) { } diff --git a/src/API/TeamController.php b/src/API/TeamController.php index ed2fa3d7..d04af22f 100644 --- a/src/API/TeamController.php +++ b/src/API/TeamController.php @@ -30,7 +30,7 @@ use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Attribute\IsGranted; #[Route(path: '/teams')] -#[IsGranted('IS_AUTHENTICATED')] +#[IsGranted('API')] #[OA\Tag(name: 'Team')] final class TeamController extends BaseApiController { @@ -38,7 +38,10 @@ final class TeamController extends BaseApiController public const GROUPS_FORM = ['Default', 'Entity', 'Team', 'Team_Entity', 'Not_Expanded']; public const GROUPS_COLLECTION = ['Default', 'Collection', 'Team']; - public function __construct(private ViewHandlerInterface $viewHandler, private TeamRepository $repository) + public function __construct( + private readonly ViewHandlerInterface $viewHandler, + private readonly TeamRepository $repository + ) { } diff --git a/src/API/TimesheetController.php b/src/API/TimesheetController.php index 7a9f9606..99625dd7 100644 --- a/src/API/TimesheetController.php +++ b/src/API/TimesheetController.php @@ -42,7 +42,7 @@ use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\Validator\Constraints; #[Route(path: '/timesheets')] -#[IsGranted('IS_AUTHENTICATED')] +#[IsGranted('API')] #[OA\Tag(name: 'Timesheet')] final class TimesheetController extends BaseApiController { diff --git a/src/API/UserController.php b/src/API/UserController.php index ce373162..13c74649 100644 --- a/src/API/UserController.php +++ b/src/API/UserController.php @@ -33,7 +33,7 @@ use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Attribute\IsGranted; #[Route(path: '/users')] -#[IsGranted('IS_AUTHENTICATED')] +#[IsGranted('API')] #[OA\Tag(name: 'User')] final class UserController extends BaseApiController { diff --git a/src/Voter/ApiVoter.php b/src/Voter/ApiVoter.php new file mode 100644 index 00000000..d78d8692 --- /dev/null +++ b/src/Voter/ApiVoter.php @@ -0,0 +1,53 @@ + + */ +final class ApiVoter extends Voter +{ + public function __construct(private readonly RolePermissionManager $permissionManager) + { + } + + public function supportsAttribute(string $attribute): bool + { + return $attribute === 'API'; + } + + public function supportsType(string $subjectType): bool + { + return $subjectType === 'null'; + } + + protected function supports(string $attribute, mixed $subject): bool + { + return $subject === null && $this->supportsAttribute($attribute); + } + + protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool + { + $user = $token->getUser(); + + if (!$user instanceof User) { + return false; + } + + return $this->permissionManager->hasRolePermission($user, 'api_access'); + } +} diff --git a/tests/Controller/PermissionControllerTest.php b/tests/Controller/PermissionControllerTest.php index 8b543742..ffeb32d9 100644 --- a/tests/Controller/PermissionControllerTest.php +++ b/tests/Controller/PermissionControllerTest.php @@ -34,7 +34,7 @@ class PermissionControllerTest extends ControllerBaseTest $client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN); $this->assertAccessIsGranted($client, '/admin/permissions'); $this->assertHasDataTable($client); - $this->assertDataTableRowCount($client, 'datatable_user_admin_permissions', 132); + $this->assertDataTableRowCount($client, 'datatable_user_admin_permissions', 133); $this->assertPageActions($client, [ 'create modal-ajax-form' => $this->createUrl('/admin/permissions/roles/create'), ]);