added "api_access" permission for limiting API access (#4779)
This commit is contained in:
@@ -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']
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
25
src/API/Authentication/AccessTokenSuccessHandler.php
Normal file
25
src/API/Authentication/AccessTokenSuccessHandler.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\API\Authentication;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
|
||||
|
||||
final class AccessTokenSuccessHandler implements AuthenticationSuccessHandlerInterface
|
||||
{
|
||||
public function onAuthenticationSuccess(Request $request, TokenInterface $token): ?Response
|
||||
{
|
||||
$token->setAttribute('api-token', true);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
53
src/Voter/ApiVoter.php
Normal file
53
src/Voter/ApiVoter.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Voter;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Security\RolePermissionManager;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||
|
||||
/**
|
||||
* A voter to check permissions on API.
|
||||
*
|
||||
* @extends Voter<string, null>
|
||||
*/
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -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'),
|
||||
]);
|
||||
|
||||
Reference in New Issue
Block a user