added remember me feature #53 (#98)

cleanup event subscriber
This commit is contained in:
Kevin Papst
2018-01-17 19:55:26 +01:00
committed by GitHub
parent 7df21f3f5a
commit 9a161b8fd9
26 changed files with 193 additions and 116 deletions

8
composer.lock generated
View File

@@ -52,12 +52,12 @@
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/kevinpapst/AdminThemeBundle.git", "url": "https://github.com/kevinpapst/AdminThemeBundle.git",
"reference": "1aa9b1637b2f827fe79ca1edff26092840e30716" "reference": "d31084eff7888d76f7976700f9ac878f40c3bacd"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/kevinpapst/AdminThemeBundle/zipball/1aa9b1637b2f827fe79ca1edff26092840e30716", "url": "https://api.github.com/repos/kevinpapst/AdminThemeBundle/zipball/d31084eff7888d76f7976700f9ac878f40c3bacd",
"reference": "1aa9b1637b2f827fe79ca1edff26092840e30716", "reference": "d31084eff7888d76f7976700f9ac878f40c3bacd",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -111,7 +111,7 @@
"support": { "support": {
"source": "https://github.com/kevinpapst/AdminThemeBundle/tree/kevinpapst" "source": "https://github.com/kevinpapst/AdminThemeBundle/tree/kevinpapst"
}, },
"time": "2018-01-15T19:14:46+00:00" "time": "2018-01-17T12:31:37+00:00"
}, },
{ {
"name": "beberlei/DoctrineExtensions", "name": "beberlei/DoctrineExtensions",

View File

@@ -24,6 +24,11 @@ security:
# denying access is done in access_control or in your controllers # denying access is done in access_control or in your controllers
anonymous: ~ anonymous: ~
remember_me:
secret: '%kernel.secret%'
lifetime: 604800
path: /
# This allows the user to login by submitting a username and password # This allows the user to login by submitting a username and password
# Reference: http://symfony.com/doc/current/cookbook/security/form_login_setup.html # Reference: http://symfony.com/doc/current/cookbook/security/form_login_setup.html
form_login: form_login:

View File

@@ -1,7 +1,7 @@
# Put parameters here that don't need to change on each machine where the app is deployed # Put parameters here that don't need to change on each machine where the app is deployed
# https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration # https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration
parameters: parameters:
locale: 'en' locale: en
app_locales: en|de app_locales: en|de
services: services:
@@ -33,19 +33,12 @@ services:
# ================================================================================ # ================================================================================
# a route listener, that injects the locale through a URL directory # a route listener, that injects the locale through a URL directory
app.redirect_to_preferred_locale_listener: App\EventSubscriber\RedirectToLocaleSubscriber:
class: App\EventListener\RedirectToPreferredLocaleListener class: App\EventSubscriber\RedirectToLocaleSubscriber
arguments: ['@router', '%app_locales%', '%locale%'] arguments: ['@router', '%app_locales%', '%locale%']
tags: tags:
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest } - { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }
# additional menu entries for the timesheet bundle
timesheet.configure_menu_listener:
class: App\EventListener\Menu
tags:
- { name: kernel.event_listener, event: app.main_menu_configure, method: onMainMenuConfigure }
- { name: kernel.event_listener, event: app.admin_menu_configure, method: onAdminMenuConfigure }
# ================================================================================ # ================================================================================
# DATABASE # DATABASE
# ================================================================================ # ================================================================================
@@ -80,17 +73,6 @@ services:
# // same code using repository services # // same code using repository services
# $posts = $this->get('app.user_repository')->findAll(); # $posts = $this->get('app.user_repository')->findAll();
# ================================================================================
# EVENTS and LISTENER / SUBSCRIBER
# ================================================================================
# event-listener to populate the navigation
app.menu_builder:
class: App\EventListener\MenuBuilder
arguments: ["@event_dispatcher", "@security.authorization_checker"]
tags:
- { name: kernel.event_listener, event: theme.sidebar_setup_menu, method: onSetupNavbar }
# ================================================================================ # ================================================================================
# FORMS # FORMS
# ================================================================================ # ================================================================================
@@ -132,13 +114,6 @@ services:
App\Twig\Extensions: App\Twig\Extensions:
arguments: ['%app_locales%'] arguments: ['%app_locales%']
avanzu_admin_theme.navbar_user_listener:
class: App\EventListener\NavbarShowUserListener
arguments: ["@security.token_storage"]
tags:
- { name: kernel.event_listener, event: theme.navbar_user, method: onShowUser }
- { name: kernel.event_listener, event: theme.sidebar_user, method: onShowUser }
# avanzu_admin_theme.navbar_task_listener: # avanzu_admin_theme.navbar_task_listener:
# class: "%avanzu_admin_theme.navbar_task_listener.class%" # class: "%avanzu_admin_theme.navbar_task_listener.class%"
# tags: # tags:

View File

@@ -79,6 +79,11 @@ footer.main-footer {
font-size: 80%; font-size: 80%;
} }
/* ================================ LOGIN-PAGE ================================ */
.login-page form label {
padding-left: 5px;
}
/* ================================ DATA-TABLES ================================ */ /* ================================ DATA-TABLES ================================ */

View File

@@ -30,6 +30,7 @@ use App\Repository\Query\ActivityQuery;
* *
* @Route("/admin/activity") * @Route("/admin/activity")
* @Security("has_role('ROLE_ADMIN')") * @Security("has_role('ROLE_ADMIN')")
* @Security("is_granted('IS_AUTHENTICATED_FULLY')")
* *
* @author Kevin Papst <kevin@kevinpapst.de> * @author Kevin Papst <kevin@kevinpapst.de>
*/ */

View File

@@ -28,6 +28,7 @@ use App\Repository\Query\CustomerQuery;
* *
* @Route("/admin/customer") * @Route("/admin/customer")
* @Security("has_role('ROLE_ADMIN')") * @Security("has_role('ROLE_ADMIN')")
* @Security("is_granted('IS_AUTHENTICATED_FULLY')")
* *
* @author Kevin Papst <kevin@kevinpapst.de> * @author Kevin Papst <kevin@kevinpapst.de>
*/ */

View File

@@ -29,6 +29,7 @@ use App\Repository\Query\ProjectQuery;
* *
* @Route("/admin/project") * @Route("/admin/project")
* @Security("has_role('ROLE_ADMIN')") * @Security("has_role('ROLE_ADMIN')")
* @Security("is_granted('IS_AUTHENTICATED_FULLY')")
* *
* @author Kevin Papst <kevin@kevinpapst.de> * @author Kevin Papst <kevin@kevinpapst.de>
*/ */

View File

@@ -20,7 +20,6 @@ use App\Entity\Timesheet;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;
use App\Form\TimesheetAdminForm; use App\Form\TimesheetAdminForm;
/** /**
@@ -28,6 +27,7 @@ use App\Form\TimesheetAdminForm;
* *
* @Route("/team/timesheet") * @Route("/team/timesheet")
* @Security("has_role('ROLE_TEAMLEAD')") * @Security("has_role('ROLE_TEAMLEAD')")
* @Security("is_granted('IS_AUTHENTICATED_FULLY')")
* *
* @author Kevin Papst <kevin@kevinpapst.de> * @author Kevin Papst <kevin@kevinpapst.de>
*/ */
@@ -41,7 +41,6 @@ class TimesheetController extends AbstractController
* @Route("/", defaults={"page": 1}, name="admin_timesheet") * @Route("/", defaults={"page": 1}, name="admin_timesheet")
* @Route("/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_timesheet_paginated") * @Route("/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_timesheet_paginated")
* @Method("GET") * @Method("GET")
* @Cache(smaxage="10")
* *
* @param $page * @param $page
* @param Request $request * @param Request $request

View File

@@ -28,6 +28,7 @@ use Symfony\Component\HttpFoundation\Request;
* *
* @Route("/admin/user") * @Route("/admin/user")
* @Security("has_role('ROLE_SUPER_ADMIN')") * @Security("has_role('ROLE_SUPER_ADMIN')")
* @Security("is_granted('IS_AUTHENTICATED_FULLY')")
* *
* @author Kevin Papst <kevin@kevinpapst.de> * @author Kevin Papst <kevin@kevinpapst.de>
*/ */
@@ -61,7 +62,6 @@ class UserController extends AbstractController
* @Route("/", defaults={"page": 1}, name="admin_user") * @Route("/", defaults={"page": 1}, name="admin_user")
* @Route("/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_user_paginated") * @Route("/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_user_paginated")
* @Method("GET") * @Method("GET")
* @Security("is_granted('view_all', user)")
*/ */
public function indexAction($page, Request $request) public function indexAction($page, Request $request)
{ {

View File

@@ -31,23 +31,16 @@ abstract class ConfigureMenuEvent extends Event
* @var SidebarMenuEvent * @var SidebarMenuEvent
*/ */
private $event; private $event;
/**
* @var AuthorizationChecker
*/
private $auth;
/** /**
* ConfigureMenuEvent constructor. * ConfigureMenuEvent constructor.
* @param AuthorizationChecker $auth
* @param Request $request * @param Request $request
* @param SidebarMenuEvent $event * @param SidebarMenuEvent $event
*/ */
public function __construct( public function __construct(
AuthorizationChecker $auth,
Request $request, Request $request,
SidebarMenuEvent $event SidebarMenuEvent $event
) { ) {
$this->auth = $auth;
$this->request = $request; $this->request = $request;
$this->event = $event; $this->event = $event;
} }
@@ -67,12 +60,4 @@ abstract class ConfigureMenuEvent extends Event
{ {
return $this->event; return $this->event;
} }
/**
* @return AuthorizationChecker
*/
public function getAuth()
{
return $this->auth;
}
} }

View File

@@ -9,42 +9,53 @@
* file that was distributed with this source code. * file that was distributed with this source code.
*/ */
namespace App\EventListener; namespace App\EventSubscriber;
use App\Event\ConfigureMainMenuEvent; use App\Event\ConfigureMainMenuEvent;
use App\Event\ConfigureAdminMenuEvent; use App\Event\ConfigureAdminMenuEvent;
use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationChecker;
use Avanzu\AdminThemeBundle\Model\MenuItemModel; use Avanzu\AdminThemeBundle\Model\MenuItemModel;
use Avanzu\AdminThemeBundle\Event\SidebarMenuEvent; use Avanzu\AdminThemeBundle\Event\SidebarMenuEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
/** /**
* Class MenuBuilder configures the main navigation. * Class MenuBuilder configures the main navigation.
* *
* @author Kevin Papst <kevin@kevinpapst.de> * @author Kevin Papst <kevin@kevinpapst.de>
*/ */
class MenuBuilder class MenuBuilderSubscriber implements EventSubscriberInterface
{ {
/** /**
* @var EventDispatcherInterface * @var EventDispatcherInterface
*/ */
private $eventDispatcher; private $eventDispatcher;
/** /**
* @var AuthorizationChecker * @var AuthorizationCheckerInterface
*/ */
private $security; private $security;
/** /**
* MenuBuilder constructor. * MenuBuilderSubscriber constructor.
* @param EventDispatcherInterface $dispatcher * @param EventDispatcherInterface $dispatcher
* @param AuthorizationChecker $security * @param AuthorizationCheckerInterface $security
*/ */
public function __construct(EventDispatcherInterface $dispatcher, AuthorizationChecker $security) public function __construct(EventDispatcherInterface $dispatcher, AuthorizationCheckerInterface $security)
{ {
$this->eventDispatcher = $dispatcher; $this->eventDispatcher = $dispatcher;
$this->security = $security; $this->security = $security;
} }
/**
* @return array
*/
public static function getSubscribedEvents(): array
{
return [
'theme.sidebar_setup_menu' => ['onSetupNavbar', 100],
];
}
/** /**
* Generate the main menu. * Generate the main menu.
* *
@@ -53,9 +64,8 @@ class MenuBuilder
public function onSetupNavbar(SidebarMenuEvent $event) public function onSetupNavbar(SidebarMenuEvent $event)
{ {
$request = $event->getRequest(); $request = $event->getRequest();
$isLoggedIn = $this->security->isGranted('IS_AUTHENTICATED_FULLY'); $isLoggedIn = $this->security->isGranted('IS_AUTHENTICATED_REMEMBERED');
$isTeamlead = $isLoggedIn && $this->security->isGranted('ROLE_TEAMLEAD'); $isTeamlead = $isLoggedIn && $this->security->isGranted('ROLE_TEAMLEAD');
$isSuperAdmin = $isLoggedIn && $this->security->isGranted('ROLE_SUPER_ADMIN');
$event->addItem( $event->addItem(
new MenuItemModel('dashboard', 'menu.homepage', 'dashboard', [], 'fa fa-dashboard') new MenuItemModel('dashboard', 'menu.homepage', 'dashboard', [], 'fa fa-dashboard')
@@ -64,7 +74,6 @@ class MenuBuilder
$this->eventDispatcher->dispatch( $this->eventDispatcher->dispatch(
ConfigureMainMenuEvent::CONFIGURE, ConfigureMainMenuEvent::CONFIGURE,
new ConfigureMainMenuEvent( new ConfigureMainMenuEvent(
$this->security,
$request, $request,
$event $event
) )
@@ -74,16 +83,9 @@ class MenuBuilder
$admin = new MenuItemModel('admin', 'menu.admin', '', [], 'fa fa-wrench'); $admin = new MenuItemModel('admin', 'menu.admin', '', [], 'fa fa-wrench');
$event->addItem($admin); $event->addItem($admin);
if ($isSuperAdmin) {
$admin->addChild(
new MenuItemModel('user_admin', 'menu.admin_user', 'admin_user', [], 'fa fa-user')
);
}
$this->eventDispatcher->dispatch( $this->eventDispatcher->dispatch(
ConfigureAdminMenuEvent::CONFIGURE, ConfigureAdminMenuEvent::CONFIGURE,
new ConfigureAdminMenuEvent( new ConfigureAdminMenuEvent(
$this->security,
$request, $request,
$event $event
) )

View File

@@ -9,29 +9,55 @@
* file that was distributed with this source code. * file that was distributed with this source code.
*/ */
namespace App\EventListener; namespace App\EventSubscriber;
use App\Event\ConfigureMainMenuEvent; use App\Event\ConfigureMainMenuEvent;
use App\Event\ConfigureAdminMenuEvent; use App\Event\ConfigureAdminMenuEvent;
use Symfony\Component\Security\Core\Authorization\AuthorizationChecker; use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Avanzu\AdminThemeBundle\Model\MenuItemModel; use Avanzu\AdminThemeBundle\Model\MenuItemModel;
use Avanzu\AdminThemeBundle\Event\SidebarMenuEvent; use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
/** /**
* Menus for timesheet * Menu event subscriber for timesheet, customer, projects, activities.
* This is a sample implementation for developer who want to add new navigation entries in their bundles.
* *
* @author Kevin Papst <kevin@kevinpapst.de> * @author Kevin Papst <kevin@kevinpapst.de>
*/ */
class Menu class MenuSubscriber implements EventSubscriberInterface
{ {
/**
* @var AuthorizationCheckerInterface
*/
private $security;
/**
* MenuSubscriber constructor.
* @param AuthorizationCheckerInterface $security
*/
public function __construct(AuthorizationCheckerInterface $security)
{
$this->security = $security;
}
/**
* @return array
*/
public static function getSubscribedEvents(): array
{
return [
ConfigureMainMenuEvent::CONFIGURE => ['onMainMenuConfigure', 100],
ConfigureAdminMenuEvent::CONFIGURE => ['onAdminMenuConfigure', 100],
];
}
/** /**
* @param \App\Event\ConfigureMainMenuEvent $event * @param \App\Event\ConfigureMainMenuEvent $event
*/ */
public function onMainMenuConfigure(ConfigureMainMenuEvent $event) public function onMainMenuConfigure(ConfigureMainMenuEvent $event)
{ {
$auth = $event->getAuth(); $auth = $this->security;
$isLoggedIn = $auth->isGranted('IS_AUTHENTICATED_FULLY'); $isLoggedIn = $auth->isGranted('IS_AUTHENTICATED_REMEMBERED');
$isUser = $isLoggedIn && $auth->isGranted('ROLE_USER'); $isUser = $isLoggedIn && $auth->isGranted('ROLE_USER');
if (!$isLoggedIn || !$isUser) { if (!$isLoggedIn || !$isUser) {
@@ -50,13 +76,9 @@ class Menu
public function onAdminMenuConfigure(ConfigureAdminMenuEvent $event) public function onAdminMenuConfigure(ConfigureAdminMenuEvent $event)
{ {
$menu = $event->getAdminMenu(); $menu = $event->getAdminMenu();
$auth = $event->getAuth(); $auth = $this->security;
if (!$auth->isGranted('IS_AUTHENTICATED_FULLY')) { if (!$auth->isGranted('IS_AUTHENTICATED_REMEMBERED') || !$auth->isGranted('ROLE_TEAMLEAD')) {
return;
}
if (!$auth->isGranted('ROLE_TEAMLEAD')) {
return; return;
} }
@@ -68,13 +90,18 @@ class Menu
return; return;
} }
if ($auth->isGranted('ROLE_SUPER_ADMIN')) {
$menu->addChild(
new MenuItemModel('user_admin', 'menu.admin_user', 'admin_user', [], 'fa fa-user')
);
}
$menu->addChild( $menu->addChild(
new MenuItemModel('customer_admin', 'menu.admin_customer', 'admin_customer', [], 'fa fa-users') new MenuItemModel('customer_admin', 'menu.admin_customer', 'admin_customer', [], 'fa fa-users')
)->addChild( )->addChild(
new MenuItemModel('project_admin', 'menu.admin_project', 'admin_project', [], 'fa fa-book') new MenuItemModel('project_admin', 'menu.admin_project', 'admin_project', [], 'fa fa-book')
)->addChild( )->addChild(
new MenuItemModel('activity_admin', 'menu.admin_activity', 'admin_activity', [], 'fa fa-tasks') new MenuItemModel('activity_admin', 'menu.admin_activity', 'admin_activity', [], 'fa fa-tasks')
) );
;
} }
} }

View File

@@ -9,19 +9,20 @@
* file that was distributed with this source code. * file that was distributed with this source code.
*/ */
namespace App\EventListener; namespace App\EventSubscriber;
use App\Entity\User; use App\Entity\User;
use Avanzu\AdminThemeBundle\Event\ShowUserEvent; use Avanzu\AdminThemeBundle\Event\ShowUserEvent;
use Avanzu\AdminThemeBundle\Model\UserModel; use Avanzu\AdminThemeBundle\Model\UserModel;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
/** /**
* Class NavbarShowUserListener * Class NavbarShowUserSubscriber
* *
* @author Kevin Papst <kevin@kevinpapst.de> * @author Kevin Papst <kevin@kevinpapst.de>
*/ */
class NavbarShowUserListener class NavbarShowUserSubscriber implements EventSubscriberInterface
{ {
/** /**
* @var TokenStorageInterface * @var TokenStorageInterface
@@ -37,6 +38,17 @@ class NavbarShowUserListener
$this->storage = $tokenStorage; $this->storage = $tokenStorage;
} }
/**
* @return array
*/
public static function getSubscribedEvents(): array
{
return [
'theme.navbar_user' => ['onShowUser', 100],
'theme.sidebar_user' => ['onShowUser', 100],
];
}
/** /**
* @param ShowUserEvent $event * @param ShowUserEvent $event
*/ */

View File

@@ -9,7 +9,7 @@
* file that was distributed with this source code. * file that was distributed with this source code.
*/ */
namespace App\EventListener; namespace App\EventSubscriber;
use Symfony\Component\HttpKernel\Event\GetResponseEvent; use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\RedirectResponse;
@@ -23,7 +23,7 @@ use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
* *
* @author Oleg Voronkovich <oleg-voronkovich@yandex.ru> * @author Oleg Voronkovich <oleg-voronkovich@yandex.ru>
*/ */
class RedirectToPreferredLocaleListener class RedirectToLocaleSubscriber
{ {
/** /**
* @var UrlGeneratorInterface * @var UrlGeneratorInterface

View File

@@ -36,6 +36,19 @@ abstract class AbstractVoter extends Voter
$this->decisionManager = $decisionManager; $this->decisionManager = $decisionManager;
} }
/**
* @param TokenInterface $token
* @return bool
*/
protected function isFullyAuthenticated(TokenInterface $token)
{
if ($this->decisionManager->decide($token, ['IS_AUTHENTICATED_FULLY'])) {
return true;
}
return false;
}
/** /**
* @param string $role * @param string $role
* @param TokenInterface $token * @param TokenInterface $token
@@ -43,7 +56,7 @@ abstract class AbstractVoter extends Voter
*/ */
protected function hasRole($role, TokenInterface $token) protected function hasRole($role, TokenInterface $token)
{ {
if ($this->decisionManager->decide($token, array($role))) { if ($this->decisionManager->decide($token, [$role])) {
return true; return true;
} }

View File

@@ -27,6 +27,12 @@ class ActivityVoter extends AbstractVoter
const EDIT = 'edit'; const EDIT = 'edit';
const DELETE = 'delete'; const DELETE = 'delete';
const ALLOWED_ATTRIBUTES = [
self::VIEW,
self::EDIT,
self::DELETE
];
/** /**
* @param string $attribute * @param string $attribute
* @param mixed $subject * @param mixed $subject
@@ -34,7 +40,7 @@ class ActivityVoter extends AbstractVoter
*/ */
protected function supports($attribute, $subject) protected function supports($attribute, $subject)
{ {
if (!in_array($attribute, array(self::VIEW, self::EDIT, self::DELETE))) { if (!in_array($attribute, self::ALLOWED_ATTRIBUTES)) {
return false; return false;
} }
@@ -107,6 +113,6 @@ class ActivityVoter extends AbstractVoter
*/ */
protected function canDelete(TokenInterface $token) protected function canDelete(TokenInterface $token)
{ {
return $this->hasRole('ROLE_ADMIN', $token); return $this->isFullyAuthenticated($token) && $this->hasRole('ROLE_ADMIN', $token);
} }
} }

View File

@@ -27,6 +27,12 @@ class CustomerVoter extends AbstractVoter
const EDIT = 'edit'; const EDIT = 'edit';
const DELETE = 'delete'; const DELETE = 'delete';
const ALLOWED_ATTRIBUTES = [
self::VIEW,
self::EDIT,
self::DELETE
];
/** /**
* @param string $attribute * @param string $attribute
* @param mixed $subject * @param mixed $subject
@@ -34,7 +40,7 @@ class CustomerVoter extends AbstractVoter
*/ */
protected function supports($attribute, $subject) protected function supports($attribute, $subject)
{ {
if (!in_array($attribute, array(self::VIEW, self::EDIT, self::DELETE))) { if (!in_array($attribute, self::ALLOWED_ATTRIBUTES)) {
return false; return false;
} }
@@ -107,6 +113,6 @@ class CustomerVoter extends AbstractVoter
*/ */
protected function canDelete(TokenInterface $token) protected function canDelete(TokenInterface $token)
{ {
return $this->hasRole('ROLE_ADMIN', $token); return $this->isFullyAuthenticated($token) && $this->hasRole('ROLE_ADMIN', $token);
} }
} }

View File

@@ -27,6 +27,12 @@ class ProjectVoter extends AbstractVoter
const EDIT = 'edit'; const EDIT = 'edit';
const DELETE = 'delete'; const DELETE = 'delete';
const ALLOWED_ATTRIBUTES = [
self::VIEW,
self::EDIT,
self::DELETE
];
/** /**
* @param string $attribute * @param string $attribute
* @param mixed $subject * @param mixed $subject
@@ -34,7 +40,7 @@ class ProjectVoter extends AbstractVoter
*/ */
protected function supports($attribute, $subject) protected function supports($attribute, $subject)
{ {
if (!in_array($attribute, array(self::VIEW, self::EDIT, self::DELETE))) { if (!in_array($attribute, self::ALLOWED_ATTRIBUTES)) {
return false; return false;
} }
@@ -107,6 +113,6 @@ class ProjectVoter extends AbstractVoter
*/ */
protected function canDelete(TokenInterface $token) protected function canDelete(TokenInterface $token)
{ {
return $this->hasRole('ROLE_ADMIN', $token); return $this->isFullyAuthenticated($token) && $this->hasRole('ROLE_ADMIN', $token);
} }
} }

View File

@@ -30,6 +30,14 @@ class TimesheetVoter extends AbstractVoter
const EDIT = 'edit'; const EDIT = 'edit';
const DELETE = 'delete'; const DELETE = 'delete';
const ALLOWED_ATTRIBUTES = [
self::START,
self::STOP,
self::VIEW,
self::EDIT,
self::DELETE
];
/** /**
* @param string $attribute * @param string $attribute
* @param mixed $subject * @param mixed $subject
@@ -37,7 +45,7 @@ class TimesheetVoter extends AbstractVoter
*/ */
protected function supports($attribute, $subject) protected function supports($attribute, $subject)
{ {
if (!in_array($attribute, array(self::START, self::STOP, self::VIEW, self::EDIT, self::DELETE))) { if (!in_array($attribute, self::ALLOWED_ATTRIBUTES)) {
return false; return false;
} }
@@ -146,6 +154,10 @@ class TimesheetVoter extends AbstractVoter
*/ */
protected function canDelete(Timesheet $timesheet, User $user, TokenInterface $token) protected function canDelete(Timesheet $timesheet, User $user, TokenInterface $token)
{ {
if (!$this->isFullyAuthenticated($token)) {
return false;
}
return $this->isOwnOrAdmin($timesheet, $user, $token); return $this->isOwnOrAdmin($timesheet, $user, $token);
} }

View File

@@ -28,11 +28,9 @@ class UserVoter extends AbstractVoter
const PASSWORD = 'password'; const PASSWORD = 'password';
const ROLES = 'roles'; const ROLES = 'roles';
const PREFERENCES = 'preferences'; const PREFERENCES = 'preferences';
const VIEW_ALL = 'view_all';
const ALLOWED_ATTRIBUTES = [ const ALLOWED_ATTRIBUTES = [
self::VIEW, self::VIEW,
self::VIEW_ALL,
self::EDIT, self::EDIT,
self::CREATE, self::CREATE,
self::ROLES, self::ROLES,
@@ -82,7 +80,6 @@ class UserVoter extends AbstractVoter
return $this->canEdit($subject, $user, $token); return $this->canEdit($subject, $user, $token);
case self::DELETE: case self::DELETE:
return $this->canDelete($subject, $user, $token); return $this->canDelete($subject, $user, $token);
case self::VIEW_ALL:
case self::CREATE: // create actually passes in the current user as $subject, not the new one case self::CREATE: // create actually passes in the current user as $subject, not the new one
case self::ROLES: case self::ROLES:
return $this->canAdminUsers($token); return $this->canAdminUsers($token);
@@ -135,6 +132,6 @@ class UserVoter extends AbstractVoter
*/ */
protected function canAdminUsers(TokenInterface $token) protected function canAdminUsers(TokenInterface $token)
{ {
return $this->hasRole('ROLE_SUPER_ADMIN', $token); return $this->isFullyAuthenticated($token) && $this->hasRole('ROLE_SUPER_ADMIN', $token);
} }
} }

View File

@@ -48,13 +48,13 @@
{% endblock %} {% endblock %}
{% block avanzu_navbar_messages %} {% block avanzu_navbar_messages %}
{% if app.user is not null and is_granted('IS_AUTHENTICATED_FULLY') %} {% if app.user is not null and is_granted('IS_AUTHENTICATED_REMEMBERED') %}
{{ render(controller('App\\Controller\\TimesheetController::activeEntriesAction')) }} {{ render(controller('App\\Controller\\TimesheetController::activeEntriesAction')) }}
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block avanzu_navbar_notifications %} {% block avanzu_navbar_notifications %}
{% if app.user is not null and is_granted('IS_AUTHENTICATED_FULLY') %} {% if app.user is not null and is_granted('IS_AUTHENTICATED_REMEMBERED') %}
{{ render(controller('App\\Controller\\ActivityController::recentActivitiesAction')) }} {{ render(controller('App\\Controller\\ActivityController::recentActivitiesAction')) }}
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View File

@@ -2,6 +2,10 @@
{% block avanzu_login_title %}{{ 'browser.title'|trans }}{% endblock %} {% block avanzu_login_title %}{{ 'browser.title'|trans }}{% endblock %}
{% block avanzu_logo_login %}<b>Kimai</b><br>TimeTracking{% endblock %} {% block avanzu_logo_login %}<b>Kimai</b><br>TimeTracking{% endblock %}
{% block avanzu_head %}
<link rel="stylesheet" href="{{ asset('css/kimai.css') }}">
{% endblock %}
{% block avanzu_login_box_msg %} {% block avanzu_login_box_msg %}
{{ 'security.title.login'|trans }} {{ 'security.title.login'|trans }}
@@ -21,13 +25,12 @@
</div> </div>
<div class="row"> <div class="row">
<div class="col-xs-8"> <div class="col-xs-8">
{# <div class="checkbox icheck">
<div class="checkbox icheck"> <label>
<label> <input id="remember_me" name="_remember_me" type="checkbox">
<input type="checkbox"> {{ 'Remember Me'|trans({}, 'AvanzuAdminTheme') }} <label for="remember_me">{{ 'Remember Me'|trans({}, 'AvanzuAdminTheme') }}</label>
</label> </label>
</div> </div>
#}
</div> </div>
<div class="col-xs-4"> <div class="col-xs-4">
<button type="submit" class="btn btn-primary btn-block btn-flat">{{ 'action.sign_in'|trans }}</button> <button type="submit" class="btn btn-primary btn-block btn-flat">{{ 'action.sign_in'|trans }}</button>

View File

@@ -38,10 +38,7 @@
<td class="hidden-xs hidden-sm">{{ widgets.label_activity(entry.activity) }}</td> <td class="hidden-xs hidden-sm">{{ widgets.label_activity(entry.activity) }}</td>
<td class="hidden-xs hidden-sm">{{ entry.description }}</td> <td class="hidden-xs hidden-sm">{{ entry.description }}</td>
<td> <td>
{% set actionButtons = {} %} {% set actionButtons = {'edit': path('timesheet_edit', {'id' : entry.id, 'page': page})} %}
{% if is_granted('edit', entry) %}
{% set actionButtons = {'edit': path('timesheet_edit', {'id' : entry.id, 'page': page})}|merge(actionButtons) %}
{% endif %}
{% if entry.end %} {% if entry.end %}
{% if is_granted('start', entry.activity) %} {% if is_granted('start', entry.activity) %}
{% set actionButtons = {'repeat': path('timesheet_start', {'id' : entry.activity.id})}|merge(actionButtons) %} {% set actionButtons = {'repeat': path('timesheet_start', {'id' : entry.activity.id})}|merge(actionButtons) %}
@@ -51,9 +48,7 @@
{% set actionButtons = {'stop': path('timesheet_stop', {'id' : entry.id})}|merge(actionButtons) %} {% set actionButtons = {'stop': path('timesheet_stop', {'id' : entry.id})}|merge(actionButtons) %}
{% endif %} {% endif %}
{% endif %} {% endif %}
{% if is_granted('delete', entry) %} {% set actionButtons = actionButtons|merge({'trash': path('timesheet_delete', {'id' : entry.id, 'page': page})}) %}
{% set actionButtons = actionButtons|merge({'trash': path('timesheet_delete', {'id' : entry.id, 'page': page})}) %}
{% endif %}
{{ widgets.button_group(actionButtons) }} {{ widgets.button_group(actionButtons) }}
</td> </td>
</tr> </tr>

View File

@@ -6,6 +6,10 @@
<source>Toggle navigation</source> <source>Toggle navigation</source>
<target>Ansicht der Seitenleiste umschalten</target> <target>Ansicht der Seitenleiste umschalten</target>
</trans-unit> </trans-unit>
<trans-unit id="Remember Me">
<source>Remember Me</source>
<target>Dauerhaft anmelden</target>
</trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>

View File

@@ -6,6 +6,10 @@
<source>Toggle navigation</source> <source>Toggle navigation</source>
<target>Toggle navigation</target> <target>Toggle navigation</target>
</trans-unit> </trans-unit>
<trans-unit id="Remember Me">
<source>Remember Me</source>
<target>Remember Me</target>
</trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>

View File

@@ -2,7 +2,7 @@
There are multiple pre-defined roles in Kimai, which define the ACLs. A user can only inherit one role, where the roles extend each user. There are multiple pre-defined roles in Kimai, which define the ACLs. A user can only inherit one role, where the roles extend each user.
## Roles ## Roles & Permissions
| Role name | extends | Gives permission for | | Role name | extends | Gives permission for |
|---|---|---| |---|---|---|
@@ -12,3 +12,21 @@ There are multiple pre-defined roles in Kimai, which define the ACLs. A user can
| ROLE_ADMIN | ROLE_TEAMLEAD | All of the above, plus: editing customers, editing projects, editing activities | | ROLE_ADMIN | ROLE_TEAMLEAD | All of the above, plus: editing customers, editing projects, editing activities |
| ROLE_SUPER_ADMIN | ROLE_ADMIN | All of the above, plus: editing users | | ROLE_SUPER_ADMIN | ROLE_ADMIN | All of the above, plus: editing users |
# Login & Authentication
- User can login with their username or email
- If you activate the `Remember me` option, you can use use the most common functions within the next days without a new login
## Remember me login
If you have chosen to login with the `Remember me` option, your login will be extended to one week (default value).
After coming back and being remembered you have access to all the following features:
- view your own timesheet
- start and stop new records
- edit existing records
If you are an administrator, you will see all your allowed options in the menu, but will be redirected to the login
form when you try to access them. This is a security feature to prevent abuse in case you forgot to logout in public
environments.
The default period for the `Remember me` option can be changed in the config file [security.yaml](config/packages/security.yaml).