Refactor authentication system (#2602)
Make auth configuration available via UI, remove FOSUserBundle and SAML-Bundle dependency
This commit is contained in:
@@ -12,10 +12,19 @@ Perform EACH version specific task between your version and the new one, otherwi
|
||||
|
||||
**ATTENTION**
|
||||
- This release bumps the minimum required [PHP version to 7.3](https://www.kimai.org/blog/2021/php8-support-php72-dropped/)
|
||||
- Self-registration is disabled by default
|
||||
- Self-registration now always requires email confirmation
|
||||
- All plugins that use own databases need to be updated as well
|
||||
|
||||
**LDAP & SAML**
|
||||
- SAML users need to activate it by setting the new `kimai.saml.activate: true` config
|
||||
- LDAP and SAML users need to remove the complete `security` section from their `local.yaml`
|
||||
- Please verify your config with the [LDAP](https://www.kimai.org/documentation/ldap.html) and [SAML](https://www.kimai.org/documentation/saml.html) documentation
|
||||
|
||||
**DEVELOPER**
|
||||
PHP 8 compatibility forced to upgrade MANY libraries, including but not limited to:
|
||||
|
||||
- Removed FOSUserBundle and hslavich/oneloginsaml
|
||||
- Doctrine Migrations, whose new major version forces the plugin updates
|
||||
- Gedmo v3 (which include BC breaks in definitions)
|
||||
- Doctrine DBAL and others, which required PHP 7.3 as well
|
||||
|
||||
@@ -27,11 +27,9 @@
|
||||
"friendsofsymfony/rest-bundle": "^3.0",
|
||||
"gedmo/doctrine-extensions": "^3.0",
|
||||
"handcraftedinthealps/rest-routing-bundle": "^1.0",
|
||||
"hslavich/oneloginsaml-bundle": "^1.4",
|
||||
"jms/metadata": "^2.0",
|
||||
"jms/serializer-bundle": "^3.9",
|
||||
"kevinpapst/adminlte-bundle": "^3.3",
|
||||
"kimai/user-bundle": "^2.0",
|
||||
"kevinpapst/adminlte-bundle": "dev-master",
|
||||
"laravolt/avatar": "^4.0",
|
||||
"league/csv": "^9.4",
|
||||
"league/html-to-markdown": "^5.0",
|
||||
|
||||
1000
composer.lock
generated
1000
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -15,7 +15,6 @@ return [
|
||||
DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true],
|
||||
KevinPapst\AdminLTEBundle\AdminLTEBundle::class => ['all' => true],
|
||||
JMS\SerializerBundle\JMSSerializerBundle::class => ['all' => true],
|
||||
FOS\UserBundle\FOSUserBundle::class => ['all' => true],
|
||||
FOS\RestBundle\FOSRestBundle::class => ['all' => true],
|
||||
Nelmio\ApiDocBundle\NelmioApiDocBundle::class => ['all' => true],
|
||||
Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true],
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
fos_user:
|
||||
db_driver: orm
|
||||
firewall_name: secured_area
|
||||
user_class: App\Entity\User
|
||||
from_email:
|
||||
address: '%env(MAILER_FROM)%'
|
||||
sender_name: "Kimai 2"
|
||||
registration:
|
||||
confirmation:
|
||||
enabled: false
|
||||
resetting:
|
||||
retry_ttl: 7200
|
||||
token_ttl: 86400
|
||||
service:
|
||||
mailer: App\Mail\UserMails
|
||||
@@ -10,16 +10,11 @@ jms_serializer:
|
||||
xml_serialization:
|
||||
format_output: '%kernel.debug%'
|
||||
metadata:
|
||||
directories:
|
||||
FOSUB:
|
||||
namespace_prefix: "FOS\\UserBundle"
|
||||
path: "%kernel.root_dir%/../config/serializer/FOS/UserBundle"
|
||||
warmup:
|
||||
paths:
|
||||
included:
|
||||
- "%kernel.root_dir%/Entity/"
|
||||
- "%kernel.root_dir%/API/Model/"
|
||||
- "%kernel.root_dir%/../vendor/kimai/user-bundle/Model"
|
||||
excluded: []
|
||||
property_naming:
|
||||
id: 'jms_serializer.identical_property_naming_strategy'
|
||||
|
||||
@@ -5,7 +5,7 @@ security:
|
||||
providers:
|
||||
chain_provider:
|
||||
chain:
|
||||
providers: [kimai_internal]
|
||||
providers: [kimai_ldap,kimai_internal]
|
||||
kimai_ldap:
|
||||
id: App\Ldap\LdapUserProvider
|
||||
kimai_internal:
|
||||
@@ -17,6 +17,8 @@ security:
|
||||
security: false
|
||||
|
||||
secured_area:
|
||||
kimai_saml: ~
|
||||
kimai_ldap: ~
|
||||
pattern: ^/
|
||||
user_checker: App\Security\UserChecker
|
||||
anonymous: true
|
||||
|
||||
@@ -12,7 +12,16 @@ app.api:
|
||||
type: rest
|
||||
prefix: /api
|
||||
|
||||
security:
|
||||
auth:
|
||||
resource: '../../src/Controller/Auth/'
|
||||
type: annotation
|
||||
prefix: /auth
|
||||
|
||||
security:
|
||||
resource: '../../src/Controller/Security/'
|
||||
type: annotation
|
||||
prefix: /{_locale}
|
||||
requirements:
|
||||
_locale: '%app_locales%'
|
||||
defaults:
|
||||
_locale: '%locale%'
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
# Expose security related features like login and logout
|
||||
fos_user_security:
|
||||
prefix: /{_locale}
|
||||
resource: "@FOSUserBundle/Resources/config/routing/security.xml"
|
||||
|
||||
# The features "user registration" and "password-reset" are enabled by default.
|
||||
#
|
||||
# You can disable them by setting the config keys in file "config/kimai.yaml":
|
||||
# - kimai.user.registration: false
|
||||
# - kimai.user.password_reset: false
|
||||
#
|
||||
# The routes for these functions are added dynamically in src/Kernel.php
|
||||
@@ -1,35 +0,0 @@
|
||||
FOS\UserBundle\Model\User:
|
||||
exclusion_policy: All
|
||||
properties:
|
||||
username:
|
||||
include: true
|
||||
groups: [Default]
|
||||
enabled:
|
||||
include: true
|
||||
groups: [Default]
|
||||
roles:
|
||||
type: array<string>
|
||||
include: true
|
||||
groups: [User_Entity]
|
||||
groups:
|
||||
exclude: true
|
||||
email:
|
||||
exclude: true
|
||||
emailCanonical:
|
||||
exclude: true
|
||||
usernameCanonical:
|
||||
exclude: true
|
||||
password:
|
||||
exclude: true
|
||||
plainPassword:
|
||||
exclude: true
|
||||
registeredAt:
|
||||
exclude: true
|
||||
lastLogin:
|
||||
exclude: true
|
||||
confirmationToken:
|
||||
exclude: true
|
||||
passwordRequestedAt:
|
||||
exclude: true
|
||||
salt:
|
||||
exclude: true
|
||||
@@ -1,32 +1,18 @@
|
||||
# ================================================================================
|
||||
# SAML Services
|
||||
# ================================================================================
|
||||
|
||||
services:
|
||||
|
||||
# ================================================================================
|
||||
# SAML
|
||||
# ================================================================================
|
||||
|
||||
App\Saml\SamlAuth:
|
||||
alias: onelogin_auth
|
||||
|
||||
OneLogin\Saml2\Auth:
|
||||
alias: onelogin_auth
|
||||
|
||||
onelogin_auth:
|
||||
class: App\Saml\SamlAuth
|
||||
arguments: ['@request_stack', '%kimai.saml.connection%']
|
||||
|
||||
App\Saml\User\SamlUserFactory:
|
||||
arguments: ['%kimai.saml%']
|
||||
|
||||
kimai.saml_listener:
|
||||
class: Hslavich\OneloginSamlBundle\Security\Firewall\SamlListener
|
||||
class: App\Saml\Firewall\SamlListener
|
||||
parent: security.authentication.listener.abstract
|
||||
abstract: true
|
||||
calls:
|
||||
- [setOneLoginAuth, ["@onelogin_auth"]]
|
||||
- [setAuth, ['@App\Saml\SamlAuthFactory']]
|
||||
|
||||
App\Saml\Provider\SamlProvider:
|
||||
arguments: ['@App\Repository\UserRepository', '', '@App\Saml\SamlTokenFactory', '@App\Saml\User\SamlUserFactory']
|
||||
arguments: ['@App\Repository\UserRepository', '', '@App\Saml\SamlTokenFactory', '@App\Saml\User\SamlUserFactory', '@App\Configuration\SystemConfiguration']
|
||||
|
||||
App\Saml\Security\SamlAuthenticationSuccessHandler:
|
||||
parent: security.authentication.success_handler
|
||||
|
||||
@@ -32,6 +32,9 @@ services:
|
||||
# APPLICATION CORE
|
||||
# ================================================================================
|
||||
|
||||
security.user.provider.chain:
|
||||
class: App\Security\KimaiUserProvider
|
||||
|
||||
App\EventSubscriber\RedirectToLocaleSubscriber:
|
||||
arguments: ['@router', '%app_locales%', '%locale%']
|
||||
|
||||
@@ -57,9 +60,6 @@ services:
|
||||
arguments:
|
||||
$settings: '%kimai.theme%'
|
||||
|
||||
App\Configuration\LdapConfiguration:
|
||||
arguments: ['%kimai.ldap%']
|
||||
|
||||
App\Utils\MPdfConverter:
|
||||
arguments: ['%kernel.cache_dir%']
|
||||
|
||||
|
||||
@@ -25,5 +25,8 @@ services:
|
||||
# required for the importer command test
|
||||
App\Importer\ImporterService:
|
||||
public: true
|
||||
arguments:
|
||||
[ '@App\Customer\CustomerService', '@App\Project\ProjectService' ]
|
||||
arguments: ['@App\Customer\CustomerService', '@App\Project\ProjectService']
|
||||
|
||||
App\User\UserService:
|
||||
public: true
|
||||
arguments: ['@App\Repository\UserRepository', '@event_dispatcher', '@validator', '@App\Configuration\SystemConfiguration', '@security.password_encoder']
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
App\Entity\User:
|
||||
properties:
|
||||
roles:
|
||||
- App\Validator\Constraints\Role: { groups: [RolesUpdate] }
|
||||
username:
|
||||
- NotBlank: { groups: [Registration, UserCreate, Profile] }
|
||||
- Length: { min: 2, max: 60, groups: [Registration, UserCreate, Profile] }
|
||||
email:
|
||||
- NotBlank: { groups: [Registration, UserCreate, Profile] }
|
||||
- Email: { groups: [Registration, UserCreate, Profile] }
|
||||
plainPassword:
|
||||
- NotBlank: { groups: [Registration, PasswordUpdate, UserCreate] }
|
||||
- Length: { min: 8, max: 60, groups: [Registration, PasswordUpdate, UserCreate] }
|
||||
plainApiToken:
|
||||
- NotBlank: { groups: [ApiTokenUpdate] }
|
||||
- Length: { min: 8, max: 60, groups: [ApiTokenUpdate] }
|
||||
@@ -25,6 +25,7 @@
|
||||
<env name="DATABASE_URL" value="mysql://kimai2_test:kimai2_test@127.0.0.1:3306/kimai2_test"/>
|
||||
<env name="CORS_ALLOW_ORIGIN" value="^https?://localhost(:[0-9]+)?$"/>
|
||||
<env name="MAILER_URL" value="null://null"/>
|
||||
<env name="MAILER_FROM" value="kimai@example.com"/>
|
||||
|
||||
<!--
|
||||
REINSTALL THE TEST DATABASE, AFTER CHANGING STRUCTURE?
|
||||
|
||||
@@ -48,8 +48,9 @@ abstract class AbstractResetCommand extends Command
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->setName('kimai:reset-' . $this->getEnvName())
|
||||
->setDescription('Resets the dev environment')
|
||||
->setName('kimai:reset:' . $this->getEnvName())
|
||||
->setAliases(['kimai:reset-' . $this->getEnvName()])
|
||||
->setDescription('Resets the "' . $this->getEnvName() . '" environment')
|
||||
->setHelp(
|
||||
<<<EOT
|
||||
This command will drop and re-create the database and its schemas, load data and clear the cache.
|
||||
|
||||
69
src/Command/AbstractRoleCommand.php
Normal file
69
src/Command/AbstractRoleCommand.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?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\Command;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\User\UserService;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
abstract class AbstractRoleCommand extends Command
|
||||
{
|
||||
private $userService;
|
||||
|
||||
public function __construct(UserService $userService)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->userService = $userService;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->setDefinition([
|
||||
new InputArgument('username', InputArgument::REQUIRED, 'The username'),
|
||||
new InputArgument('role', InputArgument::OPTIONAL, 'The role'),
|
||||
new InputOption('super', null, InputOption::VALUE_NONE, 'Instead specifying role, use this to quickly add the super administrator role'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$username = $input->getArgument('username');
|
||||
$role = $input->getArgument('role');
|
||||
$super = (true === $input->getOption('super'));
|
||||
|
||||
if (null !== $role && $super) {
|
||||
throw new \InvalidArgumentException('You can pass either the role or the --super option (but not both simultaneously).');
|
||||
}
|
||||
|
||||
if (null === $role && !$super) {
|
||||
throw new \RuntimeException('Not enough arguments, pass a role or use --super.');
|
||||
}
|
||||
|
||||
$user = $this->userService->findUserByUsernameOrThrowException($username);
|
||||
|
||||
$this->executeRoleCommand($this->userService, new SymfonyStyle($input, $output), $user, $super, $role);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
abstract protected function executeRoleCommand(UserService $manipulator, SymfonyStyle $output, User $user, bool $super, $role);
|
||||
}
|
||||
70
src/Command/ActivateUserCommand.php
Normal file
70
src/Command/ActivateUserCommand.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?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\Command;
|
||||
|
||||
use App\User\UserService;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
class ActivateUserCommand extends Command
|
||||
{
|
||||
private $userService;
|
||||
|
||||
public function __construct(UserService $userService)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->userService = $userService;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->setName('kimai:user:activate')
|
||||
->setAliases(['fos:user:activate'])
|
||||
->setDescription('Activate a user')
|
||||
->setDefinition([
|
||||
new InputArgument('username', InputArgument::REQUIRED, 'The username'),
|
||||
])
|
||||
->setHelp(
|
||||
<<<'EOT'
|
||||
The <info>kimai:user:activate</info> command activates a user (so they will be able to log in):
|
||||
|
||||
<info>php %command.full_name% susan_super</info>
|
||||
EOT
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$username = $input->getArgument('username');
|
||||
$user = $this->userService->findUserByUsernameOrThrowException($username);
|
||||
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
if (!$user->isEnabled()) {
|
||||
$user->setEnabled(true);
|
||||
$this->userService->updateUser($user);
|
||||
$io->success(sprintf('User "%s" has been activated.', $username));
|
||||
} else {
|
||||
$io->warning(sprintf('User "%s" is already active.', $username));
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
80
src/Command/ChangePasswordCommand.php
Normal file
80
src/Command/ChangePasswordCommand.php
Normal file
@@ -0,0 +1,80 @@
|
||||
<?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\Command;
|
||||
|
||||
use App\User\UserService;
|
||||
use App\Utils\CommandStyle;
|
||||
use App\Validator\ValidationFailedException;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class ChangePasswordCommand extends Command
|
||||
{
|
||||
private $userService;
|
||||
|
||||
public function __construct(UserService $userService)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->userService = $userService;
|
||||
}
|
||||
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->setName('kimai:user:password')
|
||||
->setAliases(['fos:user:change-password'])
|
||||
->setDescription('Change the password of a user.')
|
||||
->setDefinition([
|
||||
new InputArgument('username', InputArgument::REQUIRED, 'The username'),
|
||||
new InputArgument('password', InputArgument::REQUIRED, 'The password'),
|
||||
])
|
||||
->setHelp(
|
||||
<<<'EOT'
|
||||
The <info>kimai:user:password</info> command changes the password of a user:
|
||||
|
||||
<info>php %command.full_name% matthieu</info>
|
||||
|
||||
This interactive shell will first ask you for a password.
|
||||
|
||||
You can alternatively specify the password as a second argument:
|
||||
|
||||
<info>php %command.full_name% susan_super newpassword</info>
|
||||
|
||||
EOT
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$username = $input->getArgument('username');
|
||||
$password = $input->getArgument('password');
|
||||
|
||||
$user = $this->userService->findUserByUsernameOrThrowException($username);
|
||||
|
||||
$io = new CommandStyle($input, $output);
|
||||
|
||||
try {
|
||||
$user->setPlainPassword($password);
|
||||
$this->userService->updateUser($user, ['PasswordUpdate']);
|
||||
$io->success(sprintf('Changed password for user "%s".', $username));
|
||||
} catch (ValidationFailedException $ex) {
|
||||
$io->validationError($ex);
|
||||
|
||||
return 2;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -10,39 +10,24 @@
|
||||
namespace App\Command;
|
||||
|
||||
use App\Entity\User;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use App\User\UserService;
|
||||
use App\Utils\CommandStyle;
|
||||
use App\Validator\ValidationFailedException;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\QuestionHelper;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Question\Question;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
|
||||
final class CreateUserCommand extends Command
|
||||
{
|
||||
/**
|
||||
* @var UserPasswordEncoderInterface
|
||||
*/
|
||||
private $encoder;
|
||||
/**
|
||||
* @var ManagerRegistry
|
||||
*/
|
||||
private $doctrine;
|
||||
/**
|
||||
* @var ValidatorInterface
|
||||
*/
|
||||
private $validator;
|
||||
private $userService;
|
||||
|
||||
public function __construct(UserPasswordEncoderInterface $encoder, ManagerRegistry $registry, ValidatorInterface $validator)
|
||||
public function __construct(UserService $userService)
|
||||
{
|
||||
$this->encoder = $encoder;
|
||||
$this->doctrine = $registry;
|
||||
$this->validator = $validator;
|
||||
|
||||
parent::__construct();
|
||||
$this->userService = $userService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -53,7 +38,8 @@ final class CreateUserCommand extends Command
|
||||
$roles = implode(',', [User::DEFAULT_ROLE, User::ROLE_ADMIN]);
|
||||
|
||||
$this
|
||||
->setName('kimai:create-user')
|
||||
->setName('kimai:user:create')
|
||||
->setAliases(['kimai:create-user'])
|
||||
->setDescription('Create a new user')
|
||||
->setHelp('This command allows you to create a new user.')
|
||||
->addArgument('username', InputArgument::REQUIRED, 'A name for the new user (must be unique)')
|
||||
@@ -73,7 +59,7 @@ final class CreateUserCommand extends Command
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$io = new CommandStyle($input, $output);
|
||||
|
||||
$username = $input->getArgument('username');
|
||||
$email = $input->getArgument('email');
|
||||
@@ -87,41 +73,18 @@ final class CreateUserCommand extends Command
|
||||
|
||||
$role = $role ?: User::DEFAULT_ROLE;
|
||||
|
||||
$user = new User();
|
||||
$user->setUsername($username)
|
||||
->setPlainPassword($password)
|
||||
->setEmail($email)
|
||||
->setEnabled(true)
|
||||
->setRoles(explode(',', $role))
|
||||
;
|
||||
|
||||
$errors = $this->validator->validate($user, null, ['Registration']);
|
||||
if ($errors->count() > 0) {
|
||||
/** @var \Symfony\Component\Validator\ConstraintViolation $error */
|
||||
foreach ($errors as $error) {
|
||||
$value = $error->getInvalidValue();
|
||||
$io->error(
|
||||
$error->getPropertyPath()
|
||||
. ' (' . (\is_array($value) ? implode(',', $value) : $value) . ')'
|
||||
. "\n "
|
||||
. $error->getMessage()
|
||||
);
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
$user = $this->userService->createNewUser();
|
||||
$user->setUsername($username);
|
||||
$user->setPlainPassword($password);
|
||||
$user->setEmail($email);
|
||||
$user->setEnabled(true);
|
||||
$user->setRoles(explode(',', $role));
|
||||
|
||||
try {
|
||||
$pwd = $this->encoder->encodePassword($user, $user->getPlainPassword());
|
||||
$user->setPassword($pwd);
|
||||
|
||||
$entityManager = $this->doctrine->getManager();
|
||||
$entityManager->persist($user);
|
||||
$entityManager->flush();
|
||||
$io->success('Success! Created user: ' . $user->getUsername());
|
||||
} catch (\Exception $ex) {
|
||||
$io->error('Failed to create user: ' . $user->getUsername());
|
||||
$io->error('Reason: ' . $ex->getMessage());
|
||||
$this->userService->saveNewUser($user);
|
||||
$io->success(sprintf('Success! Created user: %s', $username));
|
||||
} catch (ValidationFailedException $ex) {
|
||||
$io->validationError($ex);
|
||||
|
||||
return 2;
|
||||
}
|
||||
|
||||
70
src/Command/DeactivateUserCommand.php
Normal file
70
src/Command/DeactivateUserCommand.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?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\Command;
|
||||
|
||||
use App\User\UserService;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
class DeactivateUserCommand extends Command
|
||||
{
|
||||
private $userService;
|
||||
|
||||
public function __construct(UserService $userService)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->userService = $userService;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->setName('kimai:user:deactivate')
|
||||
->setAliases(['fos:user:deactivate'])
|
||||
->setDescription('Deactivate a user')
|
||||
->setDefinition([
|
||||
new InputArgument('username', InputArgument::REQUIRED, 'The username'),
|
||||
])
|
||||
->setHelp(
|
||||
<<<'EOT'
|
||||
The <info>kimai:user:deactivate</info> command deactivates a user (will not be able to log in)
|
||||
|
||||
<info>php %command.full_name% susan_super</info>
|
||||
EOT
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$username = $input->getArgument('username');
|
||||
$user = $this->userService->findUserByUsernameOrThrowException($username);
|
||||
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
if ($user->isEnabled()) {
|
||||
$user->setEnabled(false);
|
||||
$this->userService->updateUser($user);
|
||||
$io->success(sprintf('User "%s" has been deactivated.', $username));
|
||||
} else {
|
||||
$io->warning(sprintf('User "%s" is already deactivated.', $username));
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
63
src/Command/DemoteUserCommand.php
Normal file
63
src/Command/DemoteUserCommand.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?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\Command;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\User\UserService;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
class DemoteUserCommand extends AbstractRoleCommand
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function configure()
|
||||
{
|
||||
parent::configure();
|
||||
|
||||
$this
|
||||
->setName('kimai:user:demote')
|
||||
->setAliases(['fos:user:demote'])
|
||||
->setDescription('Demote a user by removing a role')
|
||||
->setHelp(
|
||||
<<<'EOT'
|
||||
The <info>kimai:user:demote</info> command demotes a user by removing a role
|
||||
|
||||
<info>php %command.full_name% susan_super ROLE_TEAMLEAD</info>
|
||||
<info>php %command.full_name% --super susan_super</info>
|
||||
EOT
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function executeRoleCommand(UserService $manipulator, SymfonyStyle $output, User $user, bool $super, $role)
|
||||
{
|
||||
$username = $user->getUsername();
|
||||
if ($super) {
|
||||
if ($user->isSuperAdmin()) {
|
||||
$user->setSuperAdmin(false);
|
||||
$manipulator->updateUser($user);
|
||||
$output->success(sprintf('Super administrator role has been removed from the user "%s".', $username));
|
||||
} else {
|
||||
$output->warning(sprintf('User "%s" doesn\'t have the super administrator role.', $username));
|
||||
}
|
||||
} else {
|
||||
if ($user->hasRole($role)) {
|
||||
$user->removeRole($role);
|
||||
$manipulator->updateUser($user);
|
||||
$output->success(sprintf('Role "%s" has been removed from user "%s".', $role, $username));
|
||||
} else {
|
||||
$output->warning(sprintf('User "%s" didn\'t have "%s" role.', $username, $role));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -136,7 +136,8 @@ final class KimaiImporterCommand extends Command
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->setName('kimai:import-v1')
|
||||
->setName('kimai:import:v1')
|
||||
->setAliases(['kimai:import-v1'])
|
||||
->setDescription('Import data from a Kimai v1 installation')
|
||||
->setHelp('This command allows you to import the most important data from a Kimi v1 installation.')
|
||||
->addArgument(
|
||||
|
||||
63
src/Command/PromoteUserCommand.php
Normal file
63
src/Command/PromoteUserCommand.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?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\Command;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\User\UserService;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
class PromoteUserCommand extends AbstractRoleCommand
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function configure()
|
||||
{
|
||||
parent::configure();
|
||||
|
||||
$this
|
||||
->setName('kimai:user:promote')
|
||||
->setAliases(['fos:user:promote'])
|
||||
->setDescription('Promotes a user by adding a role')
|
||||
->setHelp(
|
||||
<<<'EOT'
|
||||
The <info>kimai:user:promote</info> command promotes a user by adding a role
|
||||
|
||||
<info>php %command.full_name% susan_super ROLE_TEAMLEAD</info>
|
||||
<info>php %command.full_name% --super susan_super</info>
|
||||
EOT
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function executeRoleCommand(UserService $manipulator, SymfonyStyle $output, User $user, bool $super, $role)
|
||||
{
|
||||
$username = $user->getUsername();
|
||||
if ($super) {
|
||||
if (!$user->isSuperAdmin()) {
|
||||
$user->setSuperAdmin(true);
|
||||
$manipulator->updateUser($user);
|
||||
$output->success(sprintf('User "%s" has been promoted as a super administrator.', $username));
|
||||
} else {
|
||||
$output->warning(sprintf('User "%s" does already have the super administrator role.', $username));
|
||||
}
|
||||
} else {
|
||||
if (!$user->hasRole($role)) {
|
||||
$user->addRole($role);
|
||||
$manipulator->updateUser($user);
|
||||
$output->success(sprintf('Role "%s" has been added to user "%s".', $role, $username));
|
||||
} else {
|
||||
$output->warning(sprintf('User "%s" did already have "%s" role.', $username, $role));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -110,13 +110,13 @@ class ResetTestCommand extends AbstractResetCommand
|
||||
}
|
||||
$user->setUsername($userConf[9]);
|
||||
if ($userConf[10] !== null) {
|
||||
$user->setUsernameCanonical($userConf[10]);
|
||||
// removed field: UsernameCanonical
|
||||
}
|
||||
if ($userConf[11] !== null) {
|
||||
$user->setEmail($userConf[11]);
|
||||
}
|
||||
if ($userConf[12] !== null) {
|
||||
$user->setEmailCanonical($userConf[12]);
|
||||
// removed field: EmailCanonical
|
||||
}
|
||||
if ($userConf[17] !== null) {
|
||||
$user->setApiToken($userConf[17]);
|
||||
|
||||
@@ -9,30 +9,32 @@
|
||||
|
||||
namespace App\Configuration;
|
||||
|
||||
class LdapConfiguration
|
||||
final class LdapConfiguration
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $settings = [];
|
||||
private $configuration;
|
||||
|
||||
public function __construct(array $settings)
|
||||
public function __construct(SystemConfiguration $configuration)
|
||||
{
|
||||
$this->settings = $settings;
|
||||
$this->configuration = $configuration;
|
||||
}
|
||||
|
||||
public function isActivated(): bool
|
||||
{
|
||||
return $this->configuration->isLdapActive();
|
||||
}
|
||||
|
||||
public function getRoleParameters(): array
|
||||
{
|
||||
return (array) $this->settings['role'];
|
||||
return $this->configuration->getLdapRoleParameters();
|
||||
}
|
||||
|
||||
public function getUserParameters(): array
|
||||
{
|
||||
return (array) $this->settings['user'];
|
||||
return $this->configuration->getLdapUserParameters();
|
||||
}
|
||||
|
||||
public function getConnectionParameters(): array
|
||||
{
|
||||
return (array) $this->settings['connection'];
|
||||
return $this->configuration->getLdapConnectionParameters();
|
||||
}
|
||||
}
|
||||
|
||||
50
src/Configuration/SamlConfiguration.php
Normal file
50
src/Configuration/SamlConfiguration.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?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\Configuration;
|
||||
|
||||
final class SamlConfiguration
|
||||
{
|
||||
private $configuration;
|
||||
|
||||
public function __construct(SystemConfiguration $configuration)
|
||||
{
|
||||
$this->configuration = $configuration;
|
||||
}
|
||||
|
||||
public function isActivated(): bool
|
||||
{
|
||||
return $this->configuration->isSamlActive();
|
||||
}
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return $this->configuration->getSamlTitle();
|
||||
}
|
||||
|
||||
public function getAttributeMapping(): array
|
||||
{
|
||||
return $this->configuration->getSamlAttributeMapping();
|
||||
}
|
||||
|
||||
public function getRolesAttribute(): ?string
|
||||
{
|
||||
return $this->configuration->getSamlRolesAttribute();
|
||||
}
|
||||
|
||||
public function getRolesMapping(): array
|
||||
{
|
||||
return $this->configuration->getSamlRolesMapping();
|
||||
}
|
||||
|
||||
public function getConnection(): array
|
||||
{
|
||||
return $this->configuration->getSamlConnection();
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,42 @@ class SystemConfiguration implements SystemBundleConfiguration
|
||||
return $repository->getConfiguration();
|
||||
}
|
||||
|
||||
// ========== Login form ==========
|
||||
|
||||
public function isLoginFormActive(): bool
|
||||
{
|
||||
if ($this->isLdapActive()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// if SAML is active, the login form can be deactivated
|
||||
if (!$this->isSamlActive()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (bool) $this->find('user.login');
|
||||
}
|
||||
|
||||
public function isSelfRegistrationActive(): bool
|
||||
{
|
||||
return (bool) $this->find('user.registration');
|
||||
}
|
||||
|
||||
public function getPasswordResetTokenLifetime(): int
|
||||
{
|
||||
return (int) $this->find('user.password_reset_token_ttl');
|
||||
}
|
||||
|
||||
public function getPasswordResetRetryLifetime(): int
|
||||
{
|
||||
return (int) $this->find('user.password_reset_retry_ttl');
|
||||
}
|
||||
|
||||
public function isPasswordResetActive(): bool
|
||||
{
|
||||
return (bool) $this->find('user.password_reset');
|
||||
}
|
||||
|
||||
// ========== SAML configurations ==========
|
||||
|
||||
public function isSamlActive(): bool
|
||||
@@ -30,6 +66,53 @@ class SystemConfiguration implements SystemBundleConfiguration
|
||||
return (bool) $this->find('saml.activate');
|
||||
}
|
||||
|
||||
public function getSamlTitle(): string
|
||||
{
|
||||
return (string) $this->find('saml.title');
|
||||
}
|
||||
|
||||
public function getSamlAttributeMapping(): array
|
||||
{
|
||||
return (array) $this->find('saml.mapping');
|
||||
}
|
||||
|
||||
public function getSamlRolesAttribute(): ?string
|
||||
{
|
||||
return (string) $this->find('saml.roles.attribute');
|
||||
}
|
||||
|
||||
public function getSamlRolesMapping(): array
|
||||
{
|
||||
return (array) $this->find('saml.roles.mapping');
|
||||
}
|
||||
|
||||
public function getSamlConnection(): array
|
||||
{
|
||||
return (array) $this->find('saml.connection');
|
||||
}
|
||||
|
||||
// ========== LDAP configurations ==========
|
||||
|
||||
public function isLdapActive(): bool
|
||||
{
|
||||
return (bool) $this->find('ldap.activate');
|
||||
}
|
||||
|
||||
public function getLdapRoleParameters(): array
|
||||
{
|
||||
return (array) $this->find('ldap.role');
|
||||
}
|
||||
|
||||
public function getLdapUserParameters(): array
|
||||
{
|
||||
return (array) $this->find('ldap.user');
|
||||
}
|
||||
|
||||
public function getLdapConnectionParameters(): array
|
||||
{
|
||||
return (array) $this->find('ldap.connection');
|
||||
}
|
||||
|
||||
// ========== Calendar configurations ==========
|
||||
|
||||
public function getCalendarBusinessDays(): array
|
||||
@@ -121,6 +204,7 @@ class SystemConfiguration implements SystemBundleConfiguration
|
||||
return $this->find('defaults.user.language');
|
||||
}
|
||||
|
||||
// TODO this is only used to display the hourly rate in the user profile
|
||||
public function getUserDefaultCurrency(): string
|
||||
{
|
||||
return $this->find('defaults.user.currency');
|
||||
|
||||
@@ -20,7 +20,6 @@ use Psr\Log\LoggerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController as BaseAbstractController;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Translation\DataCollectorTranslator;
|
||||
use Symfony\Contracts\Service\ServiceSubscriberInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
@@ -35,18 +34,12 @@ abstract class AbstractController extends BaseAbstractController implements Serv
|
||||
*/
|
||||
public const ROLE_ADMIN = User::ROLE_ADMIN;
|
||||
|
||||
/**
|
||||
* @return DataCollectorTranslator
|
||||
*/
|
||||
private function getTranslator()
|
||||
protected function getTranslator(): TranslatorInterface
|
||||
{
|
||||
return $this->container->get('translator');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LoggerInterface $logger
|
||||
*/
|
||||
private function getLogger()
|
||||
private function getLogger(): LoggerInterface
|
||||
{
|
||||
return $this->container->get('logger');
|
||||
}
|
||||
@@ -57,7 +50,7 @@ abstract class AbstractController extends BaseAbstractController implements Serv
|
||||
* @param string $translationKey
|
||||
* @param array $parameter
|
||||
*/
|
||||
protected function flashSuccess($translationKey, $parameter = [])
|
||||
protected function flashSuccess(string $translationKey, array $parameter = []): void
|
||||
{
|
||||
$this->addFlashTranslated('success', $translationKey, $parameter);
|
||||
}
|
||||
@@ -68,7 +61,7 @@ abstract class AbstractController extends BaseAbstractController implements Serv
|
||||
* @param string $translationKey
|
||||
* @param array $parameter
|
||||
*/
|
||||
protected function flashWarning($translationKey, $parameter = [])
|
||||
protected function flashWarning(string $translationKey, array $parameter = []): void
|
||||
{
|
||||
$this->addFlashTranslated('warning', $translationKey, $parameter);
|
||||
}
|
||||
@@ -79,7 +72,7 @@ abstract class AbstractController extends BaseAbstractController implements Serv
|
||||
* @param string $translationKey
|
||||
* @param array $parameter
|
||||
*/
|
||||
protected function flashError($translationKey, $parameter = [])
|
||||
protected function flashError(string $translationKey, array $parameter = []): void
|
||||
{
|
||||
$this->addFlashTranslated('error', $translationKey, $parameter);
|
||||
}
|
||||
@@ -89,7 +82,7 @@ abstract class AbstractController extends BaseAbstractController implements Serv
|
||||
*
|
||||
* @param \Exception $exception
|
||||
*/
|
||||
protected function flashUpdateException(\Exception $exception)
|
||||
protected function flashUpdateException(\Exception $exception): void
|
||||
{
|
||||
$this->flashException($exception, 'action.update.error');
|
||||
}
|
||||
@@ -99,7 +92,7 @@ abstract class AbstractController extends BaseAbstractController implements Serv
|
||||
*
|
||||
* @param \Exception $exception
|
||||
*/
|
||||
protected function flashDeleteException(\Exception $exception)
|
||||
protected function flashDeleteException(\Exception $exception): void
|
||||
{
|
||||
$this->flashException($exception, 'action.delete.error');
|
||||
}
|
||||
@@ -111,7 +104,7 @@ abstract class AbstractController extends BaseAbstractController implements Serv
|
||||
* @param string $translationKey
|
||||
* @param array $parameter
|
||||
*/
|
||||
protected function flashException(\Exception $exception, string $translationKey, array $parameter = [])
|
||||
protected function flashException(\Exception $exception, string $translationKey, array $parameter = []): void
|
||||
{
|
||||
$this->logException($exception);
|
||||
|
||||
@@ -129,7 +122,7 @@ abstract class AbstractController extends BaseAbstractController implements Serv
|
||||
* @param string $message
|
||||
* @param array $parameter
|
||||
*/
|
||||
protected function addFlashTranslated(string $type, string $message, array $parameter = [])
|
||||
protected function addFlashTranslated(string $type, string $message, array $parameter = []): void
|
||||
{
|
||||
if (!empty($parameter)) {
|
||||
foreach ($parameter as $key => $value) {
|
||||
@@ -145,7 +138,7 @@ abstract class AbstractController extends BaseAbstractController implements Serv
|
||||
$this->addFlash($type, $message);
|
||||
}
|
||||
|
||||
protected function logException(\Exception $ex)
|
||||
protected function logException(\Exception $ex): void
|
||||
{
|
||||
$this->getLogger()->critical($ex->getMessage());
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
namespace App\Controller\Auth;
|
||||
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Saml\SamlAuth;
|
||||
use App\Saml\SamlAuthFactory;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
@@ -22,12 +22,12 @@ use Symfony\Component\Security\Core\Security;
|
||||
*/
|
||||
final class SamlController extends AbstractController
|
||||
{
|
||||
private $oneLoginAuth;
|
||||
private $authFactory;
|
||||
private $systemConfiguration;
|
||||
|
||||
public function __construct(SamlAuth $oneLoginAuth, SystemConfiguration $systemConfiguration)
|
||||
public function __construct(SamlAuthFactory $authFactory, SystemConfiguration $systemConfiguration)
|
||||
{
|
||||
$this->oneLoginAuth = $oneLoginAuth;
|
||||
$this->authFactory = $authFactory;
|
||||
$this->systemConfiguration = $systemConfiguration;
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ final class SamlController extends AbstractController
|
||||
throw new \RuntimeException($error);
|
||||
}
|
||||
|
||||
$this->oneLoginAuth->login($session->get('_security.main.target_path'));
|
||||
$this->authFactory->create()->login($session->get('_security.main.target_path'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -71,7 +71,7 @@ final class SamlController extends AbstractController
|
||||
throw $this->createNotFoundException('SAML deactivated');
|
||||
}
|
||||
|
||||
$metadata = $this->oneLoginAuth->getSettings()->getSPMetadata();
|
||||
$metadata = $this->authFactory->create()->getSettings()->getSPMetadata();
|
||||
|
||||
$response = new Response($metadata);
|
||||
$response->headers->set('Content-Type', 'xml');
|
||||
|
||||
@@ -18,15 +18,15 @@ use App\Form\UserPasswordType;
|
||||
use App\Form\UserPreferencesForm;
|
||||
use App\Form\UserRolesType;
|
||||
use App\Form\UserTeamsType;
|
||||
use App\Repository\TeamRepository;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\User\UserService;
|
||||
use App\Utils\LocaleSettings;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
|
||||
|
||||
/**
|
||||
* User profile controller
|
||||
@@ -36,29 +36,10 @@ use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
|
||||
*/
|
||||
final class ProfileController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @var EventDispatcherInterface
|
||||
*/
|
||||
private $dispatcher;
|
||||
/**
|
||||
* @var UserPasswordEncoderInterface
|
||||
*/
|
||||
private $encoder;
|
||||
/**
|
||||
* @var TeamRepository
|
||||
*/
|
||||
private $teams;
|
||||
|
||||
public function __construct(UserPasswordEncoderInterface $encoder, EventDispatcherInterface $dispatcher)
|
||||
{
|
||||
$this->encoder = $encoder;
|
||||
$this->dispatcher = $dispatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/", name="my_profile", methods={"GET"})
|
||||
*/
|
||||
public function profileAction()
|
||||
public function profileAction(): Response
|
||||
{
|
||||
return $this->redirectToRoute('user_profile', ['username' => $this->getUser()->getUsername()]);
|
||||
}
|
||||
@@ -67,7 +48,7 @@ final class ProfileController extends AbstractController
|
||||
* @Route(path="/{username}", name="user_profile", methods={"GET"})
|
||||
* @Security("is_granted('view', profile)")
|
||||
*/
|
||||
public function indexAction(User $profile, TimesheetRepository $repository, LocaleSettings $localeSettings)
|
||||
public function indexAction(User $profile, TimesheetRepository $repository, LocaleSettings $localeSettings): Response
|
||||
{
|
||||
$userStats = $repository->getUserStatistics($profile);
|
||||
|
||||
@@ -91,7 +72,7 @@ final class ProfileController extends AbstractController
|
||||
* @Route(path="/{username}/edit", name="user_profile_edit", methods={"GET", "POST"})
|
||||
* @Security("is_granted('edit', profile)")
|
||||
*/
|
||||
public function editAction(User $profile, Request $request)
|
||||
public function editAction(User $profile, Request $request): Response
|
||||
{
|
||||
$form = $this->createEditForm($profile);
|
||||
$form->handleRequest($request);
|
||||
@@ -117,18 +98,13 @@ final class ProfileController extends AbstractController
|
||||
* @Route(path="/{username}/password", name="user_profile_password", methods={"GET", "POST"})
|
||||
* @Security("is_granted('password', profile)")
|
||||
*/
|
||||
public function passwordAction(User $profile, Request $request)
|
||||
public function passwordAction(User $profile, Request $request, UserService $userService): Response
|
||||
{
|
||||
$form = $this->createPasswordForm($profile);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$password = $this->encoder->encodePassword($profile, $profile->getPlainPassword());
|
||||
$profile->setPassword($password);
|
||||
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->persist($profile);
|
||||
$entityManager->flush();
|
||||
$userService->updateUser($profile);
|
||||
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
@@ -146,18 +122,13 @@ final class ProfileController extends AbstractController
|
||||
* @Route(path="/{username}/api-token", name="user_profile_api_token", methods={"GET", "POST"})
|
||||
* @Security("is_granted('api-token', profile)")
|
||||
*/
|
||||
public function apiTokenAction(User $profile, Request $request)
|
||||
public function apiTokenAction(User $profile, Request $request, UserService $userService): Response
|
||||
{
|
||||
$form = $this->createApiTokenForm($profile);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$password = $this->encoder->encodePassword($profile, $profile->getPlainApiToken());
|
||||
$profile->setApiToken($password);
|
||||
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->persist($profile);
|
||||
$entityManager->flush();
|
||||
$userService->updateUser($profile);
|
||||
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
@@ -175,7 +146,7 @@ final class ProfileController extends AbstractController
|
||||
* @Route(path="/{username}/roles", name="user_profile_roles", methods={"GET", "POST"})
|
||||
* @Security("is_granted('roles', profile)")
|
||||
*/
|
||||
public function rolesAction(User $profile, Request $request)
|
||||
public function rolesAction(User $profile, Request $request): Response
|
||||
{
|
||||
$isSuperAdmin = $profile->isSuperAdmin();
|
||||
|
||||
@@ -209,7 +180,7 @@ final class ProfileController extends AbstractController
|
||||
* @Route(path="/{username}/teams", name="user_profile_teams", methods={"GET", "POST"})
|
||||
* @Security("is_granted('teams', profile)")
|
||||
*/
|
||||
public function teamsAction(User $profile, Request $request)
|
||||
public function teamsAction(User $profile, Request $request): Response
|
||||
{
|
||||
$form = $this->createTeamsForm($profile);
|
||||
$form->handleRequest($request);
|
||||
@@ -235,11 +206,11 @@ final class ProfileController extends AbstractController
|
||||
* @Route(path="/{username}/prefs", name="user_profile_preferences", methods={"GET", "POST"})
|
||||
* @Security("is_granted('preferences', profile)")
|
||||
*/
|
||||
public function preferencesAction(User $profile, Request $request)
|
||||
public function preferencesAction(User $profile, Request $request, EventDispatcherInterface $dispatcher): Response
|
||||
{
|
||||
// we need to prepare the user preferences, which is done via an EventSubscriber
|
||||
$event = new PrepareUserEvent($profile);
|
||||
$this->dispatcher->dispatch($event);
|
||||
$dispatcher->dispatch($event);
|
||||
|
||||
$original = [];
|
||||
foreach ($profile->getPreferences() as $preference) {
|
||||
|
||||
195
src/Controller/Security/PasswordResetController.php
Normal file
195
src/Controller/Security/PasswordResetController.php
Normal file
@@ -0,0 +1,195 @@
|
||||
<?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\Controller\Security;
|
||||
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Controller\AbstractController;
|
||||
use App\Entity\User;
|
||||
use App\Event\EmailEvent;
|
||||
use App\Event\EmailPasswordResetEvent;
|
||||
use App\Form\PasswordResetForm;
|
||||
use App\User\LoginManager;
|
||||
use App\User\UserService;
|
||||
use DateTime;
|
||||
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Mime\Address;
|
||||
use Symfony\Component\Mime\Email;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
/**
|
||||
* @Route(path="/resetting")
|
||||
*/
|
||||
final class PasswordResetController extends AbstractController
|
||||
{
|
||||
private $eventDispatcher;
|
||||
private $userService;
|
||||
private $configuration;
|
||||
|
||||
public function __construct(EventDispatcherInterface $eventDispatcher, UserService $userService, SystemConfiguration $configuration)
|
||||
{
|
||||
$this->eventDispatcher = $eventDispatcher;
|
||||
$this->userService = $userService;
|
||||
$this->configuration = $configuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request reset user password: show form.
|
||||
*
|
||||
* @Route(path="/request", name="fos_user_resetting_request", methods={"GET"})
|
||||
*/
|
||||
public function requestAction(): Response
|
||||
{
|
||||
if (!$this->configuration->isPasswordResetActive()) {
|
||||
throw $this->createNotFoundException();
|
||||
}
|
||||
|
||||
return $this->render('security/password-reset/request.html.twig');
|
||||
}
|
||||
|
||||
/**
|
||||
* Request reset user password: submit form and send email.
|
||||
*
|
||||
* @Route(path="/send-email", name="fos_user_resetting_send_email", methods={"POST"})
|
||||
*/
|
||||
public function sendEmailAction(Request $request): Response
|
||||
{
|
||||
if (!$this->configuration->isPasswordResetActive()) {
|
||||
throw $this->createNotFoundException();
|
||||
}
|
||||
|
||||
$username = $request->request->get('username');
|
||||
$user = $this->userService->findUserByUsernameOrEmail($username);
|
||||
|
||||
if (null !== $user && !$user->isPasswordRequestNonExpired($this->configuration->getPasswordResetRetryLifetime())) {
|
||||
if (!$user->isInternalUser()) {
|
||||
throw $this->createAccessDeniedException(
|
||||
sprintf('The user "%s" tried to reset the password, but it is registered as "%s" auth-type.', $user->getUsername(), $user->getAuth())
|
||||
);
|
||||
}
|
||||
|
||||
if (null === $user->getConfirmationToken()) {
|
||||
$user->setConfirmationToken($this->userService->generateSecurityToken());
|
||||
}
|
||||
|
||||
$mail = $this->generateResettingEmailMessage($user);
|
||||
$event = new EmailPasswordResetEvent($user, $mail);
|
||||
$this->eventDispatcher->dispatch($event);
|
||||
|
||||
// this will finally send the email
|
||||
$this->eventDispatcher->dispatch(new EmailEvent($event->getEmail()));
|
||||
|
||||
$user->setPasswordRequestedAt(new DateTime());
|
||||
$this->userService->updateUser($user);
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('fos_user_resetting_check_email', ['username' => $username]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tell the user to check his email provider.
|
||||
*
|
||||
* @Route(path="/check-email", name="fos_user_resetting_check_email", methods={"GET"})
|
||||
*/
|
||||
public function checkEmailAction(Request $request): Response
|
||||
{
|
||||
if (!$this->configuration->isPasswordResetActive()) {
|
||||
throw $this->createNotFoundException();
|
||||
}
|
||||
|
||||
$username = $request->query->get('username');
|
||||
|
||||
if (empty($username)) {
|
||||
// the user does not come from the sendEmail action
|
||||
return $this->redirectToRoute('fos_user_resetting_request');
|
||||
}
|
||||
|
||||
return $this->render('security/password-reset/check_email.html.twig', [
|
||||
'tokenLifetime' => ceil($this->configuration->getPasswordResetRetryLifetime() / 3600),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset user password.
|
||||
*
|
||||
* @Route(path="/reset/{token}", name="fos_user_resetting_reset", methods={"GET", "POST"})
|
||||
*/
|
||||
public function resetAction(Request $request, LoginManager $loginManager, ?string $token): Response
|
||||
{
|
||||
if (!$this->configuration->isPasswordResetActive()) {
|
||||
throw $this->createNotFoundException();
|
||||
}
|
||||
|
||||
$user = $this->userService->findUserByConfirmationToken($token);
|
||||
|
||||
if (null === $user) {
|
||||
return $this->redirectToRoute('fos_user_security_login');
|
||||
}
|
||||
|
||||
if (!$user->isPasswordRequestNonExpired($this->configuration->getPasswordResetTokenLifetime())) {
|
||||
return $this->redirectToRoute('fos_user_resetting_request');
|
||||
}
|
||||
|
||||
$form = $this->createResetForm();
|
||||
$form->setData($user);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$user->setConfirmationToken(null);
|
||||
$user->setPasswordRequestedAt(null);
|
||||
$user->setEnabled(true);
|
||||
|
||||
$this->userService->updateUser($user);
|
||||
|
||||
$response = $this->redirectToRoute('my_profile');
|
||||
$loginManager->logInUser($user, $response);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
return $this->render('security/password-reset/reset.html.twig', [
|
||||
'token' => $token,
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function createResetForm(): FormInterface
|
||||
{
|
||||
$options = ['validation_groups' => ['ResetPassword', 'Default']];
|
||||
|
||||
return $this->createFormBuilder()->create('fos_user_resetting_form', PasswordResetForm::class, $options)->getForm();
|
||||
}
|
||||
|
||||
private function generateResettingEmailMessage(User $user): Email
|
||||
{
|
||||
$username = $user->getDisplayName();
|
||||
$language = $user->getLanguage();
|
||||
|
||||
$url = $this->generateUrl('fos_user_resetting_reset', ['token' => $user->getConfirmationToken()], UrlGeneratorInterface::ABSOLUTE_URL);
|
||||
|
||||
return (new TemplatedEmail())
|
||||
->to(new Address($user->getEmail()))
|
||||
->subject(
|
||||
$this->getTranslator()->trans('reset.subject', ['%username%' => $username], 'email', $language)
|
||||
)
|
||||
->htmlTemplate('emails/password-reset.html.twig')
|
||||
->context([
|
||||
'user' => $user,
|
||||
'username' => $username,
|
||||
'confirmationUrl' => $url,
|
||||
])
|
||||
;
|
||||
}
|
||||
}
|
||||
84
src/Controller/Security/SecurityController.php
Normal file
84
src/Controller/Security/SecurityController.php
Normal file
@@ -0,0 +1,84 @@
|
||||
<?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\Controller\Security;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
|
||||
|
||||
final class SecurityController extends AbstractController
|
||||
{
|
||||
private $tokenManager;
|
||||
|
||||
public function __construct(CsrfTokenManagerInterface $tokenManager)
|
||||
{
|
||||
$this->tokenManager = $tokenManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/login", name="fos_user_security_login", methods={"GET", "POST"})
|
||||
*/
|
||||
public function loginAction(Request $request): Response
|
||||
{
|
||||
/** @var SessionInterface $session */
|
||||
$session = $request->getSession();
|
||||
|
||||
$authErrorKey = Security::AUTHENTICATION_ERROR;
|
||||
$lastUsernameKey = Security::LAST_USERNAME;
|
||||
|
||||
// get the error if any (works with forward and redirect -- see below)
|
||||
if ($request->attributes->has($authErrorKey)) {
|
||||
$error = $request->attributes->get($authErrorKey);
|
||||
} elseif (null !== $session && $session->has($authErrorKey)) {
|
||||
$error = $session->get($authErrorKey);
|
||||
$session->remove($authErrorKey);
|
||||
} else {
|
||||
$error = null;
|
||||
}
|
||||
|
||||
if (!$error instanceof AuthenticationException) {
|
||||
$error = null; // The value does not come from the security component.
|
||||
}
|
||||
|
||||
$lastUsername = '';
|
||||
if ($request->hasSession()) {
|
||||
$lastUsername = $session->get($lastUsernameKey);
|
||||
}
|
||||
|
||||
$csrfToken = $this->tokenManager->getToken('authenticate')->getValue();
|
||||
|
||||
return $this->render('security/login.html.twig', [
|
||||
'last_username' => $lastUsername,
|
||||
'error' => $error,
|
||||
'csrf_token' => $csrfToken,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/login_check", name="fos_user_security_check", methods={"POST"})
|
||||
*/
|
||||
public function checkAction()
|
||||
{
|
||||
throw new \RuntimeException('You must configure the check path to be handled by the firewall using form_login in your security firewall configuration.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/logout", name="fos_user_security_logout", methods={"GET", "POST"})
|
||||
*/
|
||||
public function logoutAction()
|
||||
{
|
||||
throw new \RuntimeException('You must activate the logout in your security firewall configuration.');
|
||||
}
|
||||
}
|
||||
212
src/Controller/Security/SelfRegistrationController.php
Normal file
212
src/Controller/Security/SelfRegistrationController.php
Normal file
@@ -0,0 +1,212 @@
|
||||
<?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\Controller\Security;
|
||||
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Controller\AbstractController;
|
||||
use App\Entity\User;
|
||||
use App\Event\EmailEvent;
|
||||
use App\Event\EmailSelfRegistrationEvent;
|
||||
use App\Form\SelfRegistrationForm;
|
||||
use App\User\LoginManager;
|
||||
use App\User\UserService;
|
||||
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
||||
use Symfony\Component\Mime\Address;
|
||||
use Symfony\Component\Mime\Email;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
/**
|
||||
* @Route(path="/register")
|
||||
*/
|
||||
class SelfRegistrationController extends AbstractController
|
||||
{
|
||||
private $eventDispatcher;
|
||||
private $userService;
|
||||
private $tokenStorage;
|
||||
private $configuration;
|
||||
|
||||
public function __construct(EventDispatcherInterface $eventDispatcher, UserService $userService, TokenStorageInterface $tokenStorage, SystemConfiguration $configuration)
|
||||
{
|
||||
$this->eventDispatcher = $eventDispatcher;
|
||||
$this->userService = $userService;
|
||||
$this->tokenStorage = $tokenStorage;
|
||||
$this->configuration = $configuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/", name="fos_user_registration_register", methods={"GET", "POST"})
|
||||
*/
|
||||
public function registerAction(Request $request): Response
|
||||
{
|
||||
if (!$this->configuration->isSelfRegistrationActive()) {
|
||||
throw $this->createNotFoundException();
|
||||
}
|
||||
|
||||
$user = $this->userService->createNewUser();
|
||||
$user->setLanguage($request->getLocale());
|
||||
|
||||
$form = $this->createSelfRegistrationForm();
|
||||
$form->setData($user);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$user->setEnabled(false);
|
||||
$user->setConfirmationToken($this->userService->generateSecurityToken());
|
||||
|
||||
$mail = $this->generateConfirmationEmail($user);
|
||||
$event = new EmailSelfRegistrationEvent($user, $mail);
|
||||
$this->eventDispatcher->dispatch($event);
|
||||
|
||||
// this will finally send the email
|
||||
$this->eventDispatcher->dispatch(new EmailEvent($event->getEmail()));
|
||||
|
||||
$request->getSession()->set('fos_user_send_confirmation_email/email', $user->getEmail());
|
||||
|
||||
$this->userService->saveNewUser($user);
|
||||
|
||||
return $this->redirectToRoute('fos_user_registration_check_email');
|
||||
}
|
||||
|
||||
return $this->render('security/self-registration/register.html.twig', [
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tell the user to check their email provider.
|
||||
*
|
||||
* @Route(path="/check-email", name="fos_user_registration_check_email", methods={"GET"})
|
||||
*/
|
||||
public function checkEmailAction(Request $request): Response
|
||||
{
|
||||
if (!$this->configuration->isSelfRegistrationActive()) {
|
||||
throw $this->createNotFoundException();
|
||||
}
|
||||
|
||||
$email = $request->getSession()->get('fos_user_send_confirmation_email/email');
|
||||
|
||||
if (empty($email)) {
|
||||
return $this->redirectToRoute('fos_user_registration_register');
|
||||
}
|
||||
|
||||
$request->getSession()->remove('fos_user_send_confirmation_email/email');
|
||||
$user = $this->userService->findUserByEmail($email);
|
||||
|
||||
if (null === $user) {
|
||||
return $this->redirectToRoute('fos_user_security_login');
|
||||
}
|
||||
|
||||
return $this->render('security/self-registration/check_email.html.twig', [
|
||||
'user' => $user,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Receive the confirmation token from user email provider, login the user.
|
||||
*
|
||||
* @Route(path="/confirm/{token}", name="fos_user_registration_confirm", methods={"GET"})
|
||||
*/
|
||||
public function confirmAction(LoginManager $loginManager, ?string $token): Response
|
||||
{
|
||||
if (!$this->configuration->isSelfRegistrationActive()) {
|
||||
throw $this->createNotFoundException();
|
||||
}
|
||||
|
||||
$user = $this->userService->findUserByConfirmationToken($token);
|
||||
|
||||
if (null === $user) {
|
||||
return $this->redirectToRoute('fos_user_security_login');
|
||||
}
|
||||
|
||||
$user->setConfirmationToken(null);
|
||||
$user->setEnabled(true);
|
||||
|
||||
$this->userService->updateUser($user);
|
||||
|
||||
$response = $this->redirectToRoute('fos_user_registration_confirmed');
|
||||
$loginManager->logInUser($user, $response);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tell the user his account is now confirmed.
|
||||
*
|
||||
* @Route(path="/confirmed", name="fos_user_registration_confirmed", methods={"GET"})
|
||||
*/
|
||||
public function confirmedAction(Request $request): Response
|
||||
{
|
||||
if (!$this->configuration->isSelfRegistrationActive()) {
|
||||
throw $this->createNotFoundException();
|
||||
}
|
||||
|
||||
$user = $this->getUser();
|
||||
if ($user === null) {
|
||||
throw $this->createAccessDeniedException('This user does not have access to this section.');
|
||||
}
|
||||
|
||||
return $this->render('security/self-registration/confirmed.html.twig', [
|
||||
'user' => $user,
|
||||
'targetUrl' => $this->getTargetUrlFromSession($request->getSession()),
|
||||
]);
|
||||
}
|
||||
|
||||
private function createSelfRegistrationForm(): FormInterface
|
||||
{
|
||||
$options = ['validation_groups' => ['Registration', 'Default']];
|
||||
|
||||
return $this->createFormBuilder()->create('fos_user_registration_form', SelfRegistrationForm::class, $options)->getForm();
|
||||
}
|
||||
|
||||
private function getTargetUrlFromSession(SessionInterface $session): ?string
|
||||
{
|
||||
$token = $this->tokenStorage->getToken();
|
||||
if (!method_exists($token, 'getProviderKey')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$key = sprintf('_security.%s.target_path', $token->getProviderKey());
|
||||
|
||||
if ($session->has($key)) {
|
||||
return $session->get($key);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function generateConfirmationEmail(User $user): Email
|
||||
{
|
||||
$username = $user->getDisplayName();
|
||||
$language = $user->getLanguage();
|
||||
|
||||
$url = $this->generateUrl('fos_user_registration_confirm', ['token' => $user->getConfirmationToken()], UrlGeneratorInterface::ABSOLUTE_URL);
|
||||
|
||||
return (new TemplatedEmail())
|
||||
->to(new Address($user->getEmail()))
|
||||
->subject(
|
||||
$this->getTranslator()->trans('registration.subject', ['%username%' => $username], 'email', $language)
|
||||
)
|
||||
->htmlTemplate('emails/confirmation.html.twig')
|
||||
->context([
|
||||
'user' => $user,
|
||||
'username' => $username,
|
||||
'confirmationUrl' => $url,
|
||||
])
|
||||
;
|
||||
}
|
||||
}
|
||||
@@ -265,7 +265,60 @@ final class SystemConfigurationController extends AbstractController
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
$authentication = (new SystemConfigurationModel())
|
||||
->setSection(SystemConfigurationModel::SECTION_AUTHENTICATION)
|
||||
->setConfiguration([
|
||||
(new Configuration())
|
||||
->setName('user.login')
|
||||
->setLabel('user_auth_login')
|
||||
->setTranslationDomain('system-configuration')
|
||||
->setType(YesNoType::class),
|
||||
(new Configuration())
|
||||
->setName('user.registration')
|
||||
->setLabel('user_auth_registration')
|
||||
->setTranslationDomain('system-configuration')
|
||||
->setType(YesNoType::class),
|
||||
(new Configuration())
|
||||
->setName('user.password_reset')
|
||||
->setTranslationDomain('system-configuration')
|
||||
->setLabel('user_auth_password_reset')
|
||||
->setType(YesNoType::class),
|
||||
(new Configuration())
|
||||
->setName('user.password_reset_retry_ttl')
|
||||
->setTranslationDomain('system-configuration')
|
||||
->setLabel('user_auth_password_reset_retry_ttl')
|
||||
->setConstraints([new NotNull(), new GreaterThanOrEqual(['value' => 60])])
|
||||
->setType(IntegerType::class),
|
||||
(new Configuration())
|
||||
->setName('user.password_reset_token_ttl')
|
||||
->setTranslationDomain('system-configuration')
|
||||
->setLabel('user_auth_password_reset_token_ttl')
|
||||
->setConstraints([new NotNull(), new GreaterThanOrEqual(['value' => 60])])
|
||||
->setType(IntegerType::class),
|
||||
/*
|
||||
(new Configuration())
|
||||
->setName('ldap.activate')
|
||||
->setLabel('ldap_activate')
|
||||
->setTranslationDomain('system-configuration')
|
||||
->setType(YesNoType::class),
|
||||
(new Configuration())
|
||||
->setName('saml.activate')
|
||||
->setLabel('saml_activate')
|
||||
->setTranslationDomain('system-configuration')
|
||||
->setType(YesNoType::class),
|
||||
*/
|
||||
]);
|
||||
|
||||
if (!$this->configurations->isSamlActive()) {
|
||||
$authentication->getConfigurationByName('user.login')->setEnabled(false);
|
||||
}
|
||||
|
||||
if (!$this->configurations->isPasswordResetActive()) {
|
||||
$authentication->getConfigurationByName('user.password_reset_retry_ttl')->setEnabled(false);
|
||||
$authentication->getConfigurationByName('user.password_reset_token_ttl')->setEnabled(false);
|
||||
}
|
||||
|
||||
$configurationModels = [
|
||||
(new SystemConfigurationModel())
|
||||
->setSection(SystemConfigurationModel::SECTION_TIMESHEET)
|
||||
->setConfiguration([
|
||||
@@ -399,6 +452,7 @@ final class SystemConfigurationController extends AbstractController
|
||||
->setType(YesNoType::class)
|
||||
->setTranslationDomain('system-configuration'),
|
||||
]),
|
||||
$authentication,
|
||||
(new SystemConfigurationModel())
|
||||
->setSection(SystemConfigurationModel::SECTION_FORM_CUSTOMER)
|
||||
->setConfiguration([
|
||||
@@ -539,5 +593,7 @@ final class SystemConfigurationController extends AbstractController
|
||||
->setOptions(['input' => 'string']),
|
||||
]),
|
||||
];
|
||||
|
||||
return $configurationModels;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,14 +70,24 @@ class AppExtension extends Extension
|
||||
|
||||
$this->createPermissionParameter($config['permissions'], $container);
|
||||
$this->createThemeParameter($config['theme'], $container);
|
||||
$this->createUserParameter($config['user'], $container);
|
||||
$container->setParameter('kimai.saml', $config['saml']);
|
||||
$container->setParameter('kimai.saml.connection', $config['saml']['connection']);
|
||||
$container->setParameter('kimai.timesheet', $config['timesheet']); // @deprecated since 1.13
|
||||
$container->setParameter('kimai.timesheet.rates', $config['timesheet']['rates']);
|
||||
$container->setParameter('kimai.timesheet.rounding', $config['timesheet']['rounding']);
|
||||
|
||||
$this->setLdapParameter($config['ldap'], $container);
|
||||
if (!isset($config['ldap']['connection']['baseDn'])) {
|
||||
$config['ldap']['connection']['baseDn'] = $config['ldap']['user']['baseDn'];
|
||||
}
|
||||
|
||||
if (empty($config['ldap']['connection']['accountFilterFormat']) && $config['ldap']['connection']['bindRequiresDn']) {
|
||||
$filter = '';
|
||||
if (!empty($config['ldap']['user']['filter'])) {
|
||||
$filter = $config['ldap']['user']['filter'];
|
||||
}
|
||||
$config['ldap']['connection']['accountFilterFormat'] = '(&' . $filter . '(' . $config['ldap']['user']['usernameAttribute'] . '=%s))';
|
||||
}
|
||||
|
||||
// @deprecated since 1.15
|
||||
$container->setParameter('kimai.ldap', $config['ldap']);
|
||||
|
||||
// translation files, which can overwrite the default kimai translations
|
||||
$localTranslations = [];
|
||||
@@ -129,23 +139,6 @@ class AppExtension extends Extension
|
||||
$container->setParameter('kimai.languages', $config);
|
||||
}
|
||||
|
||||
protected function setLdapParameter(array $config, ContainerBuilder $container)
|
||||
{
|
||||
if (!isset($config['connection']['baseDn'])) {
|
||||
$config['connection']['baseDn'] = $config['user']['baseDn'];
|
||||
}
|
||||
|
||||
if (empty($config['connection']['accountFilterFormat']) && $config['connection']['bindRequiresDn']) {
|
||||
$filter = '';
|
||||
if (!empty($config['user']['filter'])) {
|
||||
$filter = $config['user']['filter'];
|
||||
}
|
||||
$config['connection']['accountFilterFormat'] = '(&' . $filter . '(' . $config['user']['usernameAttribute'] . '=%s))';
|
||||
}
|
||||
|
||||
$container->setParameter('kimai.ldap', $config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs some pre-compilation on the configured permissions from kimai.yaml
|
||||
* to save us from constant array lookups from during runtime.
|
||||
@@ -229,27 +222,6 @@ class AppExtension extends Extension
|
||||
$container->setParameter('kimai.theme.show_about', $config['show_about']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $config
|
||||
* @param ContainerBuilder $container
|
||||
*/
|
||||
protected function createUserParameter(array $config, ContainerBuilder $container)
|
||||
{
|
||||
if (!$config['registration']) {
|
||||
$routes = $container->getParameter('admin_lte_theme.routes');
|
||||
$routes['adminlte_registration'] = null;
|
||||
$container->setParameter('admin_lte_theme.routes', $routes);
|
||||
}
|
||||
|
||||
if (!$config['password_reset']) {
|
||||
$routes = $container->getParameter('admin_lte_theme.routes');
|
||||
$routes['adminlte_password_reset'] = null;
|
||||
$container->setParameter('admin_lte_theme.routes', $routes);
|
||||
}
|
||||
|
||||
$container->setParameter('kimai.fosuser', $config);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
namespace App\DependencyInjection\Compiler;
|
||||
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Configuration\ThemeConfiguration;
|
||||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
@@ -29,8 +30,8 @@ class TwigContextCompilerPass implements CompilerPassInterface
|
||||
$theme = $container->getDefinition(ThemeConfiguration::class);
|
||||
$twig->addMethodCall('addGlobal', ['kimai_context', $theme]);
|
||||
|
||||
$saml = $container->getParameter('kimai.saml');
|
||||
$twig->addMethodCall('addGlobal', ['saml', $saml]);
|
||||
$config = $container->getDefinition(SystemConfiguration::class);
|
||||
$twig->addMethodCall('addGlobal', ['kimai_config', $config]);
|
||||
|
||||
$definition = $container->getDefinition('twig.loader.native_filesystem');
|
||||
|
||||
|
||||
@@ -512,12 +512,21 @@ class Configuration implements ConfigurationInterface
|
||||
$node
|
||||
->addDefaultsIfNotSet()
|
||||
->children()
|
||||
->booleanNode('registration')
|
||||
->booleanNode('login')
|
||||
->defaultTrue()
|
||||
->end()
|
||||
->booleanNode('registration')
|
||||
->defaultFalse()
|
||||
->end()
|
||||
->booleanNode('password_reset')
|
||||
->defaultTrue()
|
||||
->end()
|
||||
->integerNode('password_reset_retry_ttl')
|
||||
->defaultValue(7200)
|
||||
->end()
|
||||
->integerNode('password_reset_token_ttl')
|
||||
->defaultValue(86400)
|
||||
->end()
|
||||
->end()
|
||||
;
|
||||
|
||||
@@ -670,6 +679,9 @@ class Configuration implements ConfigurationInterface
|
||||
$node
|
||||
->addDefaultsIfNotSet()
|
||||
->children()
|
||||
->booleanNode('activate')
|
||||
->defaultFalse()
|
||||
->end()
|
||||
->arrayNode('connection')
|
||||
->addDefaultsIfNotSet()
|
||||
->children()
|
||||
@@ -767,13 +779,13 @@ class Configuration implements ConfigurationInterface
|
||||
->end()
|
||||
->validate()
|
||||
->ifTrue(static function ($v) {
|
||||
return null !== $v['connection']['host'] && !\extension_loaded('ldap');
|
||||
return $v['activate'] && !\extension_loaded('ldap');
|
||||
})
|
||||
->thenInvalid('LDAP is activated, but the LDAP PHP extension is not loaded.')
|
||||
->end()
|
||||
->validate()
|
||||
->ifTrue(static function ($v) {
|
||||
return null !== $v['connection']['host'] && empty($v['user']['baseDn']);
|
||||
return $v['activate'] && empty($v['user']['baseDn']);
|
||||
})
|
||||
->thenInvalid('The "ldap.user.baseDn" config must be set if LDAP is activated.')
|
||||
->end()
|
||||
|
||||
@@ -12,14 +12,15 @@ namespace App\Entity;
|
||||
use App\Constants;
|
||||
use App\Export\Annotation as Exporter;
|
||||
use App\Utils\StringHelper;
|
||||
use App\Validator\Constraints as Constraints;
|
||||
use DateTime;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Exception;
|
||||
use FOS\UserBundle\Model\User as BaseUser;
|
||||
use JMS\Serializer\Annotation as Serializer;
|
||||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
use Symfony\Component\Security\Core\User\EquatableInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
@@ -33,6 +34,7 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
* )
|
||||
* @UniqueEntity("username")
|
||||
* @UniqueEntity("email")
|
||||
* @Constraints\User(groups={"UserCreate", "Registration", "Default"})
|
||||
*
|
||||
* @Serializer\ExclusionPolicy("all")
|
||||
* @Serializer\VirtualProperty(
|
||||
@@ -64,12 +66,12 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
* @ Exporter\Expose("teams", label="label.team", exp="object.getTeams().toArray()", type="array")
|
||||
* @Exporter\Expose("active", label="label.active", exp="object.isEnabled()", type="boolean")
|
||||
*/
|
||||
class User extends BaseUser implements UserInterface
|
||||
class User implements UserInterface, EquatableInterface, \Serializable
|
||||
{
|
||||
public const ROLE_USER = 'ROLE_USER';
|
||||
public const ROLE_TEAMLEAD = 'ROLE_TEAMLEAD';
|
||||
public const ROLE_ADMIN = 'ROLE_ADMIN';
|
||||
//public const ROLE_SUPER_ADMIN = 'ROLE_SUPER_ADMIN';
|
||||
public const ROLE_SUPER_ADMIN = 'ROLE_SUPER_ADMIN';
|
||||
|
||||
public const DEFAULT_ROLE = self::ROLE_USER;
|
||||
public const DEFAULT_LANGUAGE = Constants::DEFAULT_LOCALE;
|
||||
@@ -156,6 +158,8 @@ class User extends BaseUser implements UserInterface
|
||||
/**
|
||||
* @var string
|
||||
* @internal to be set via form, must not be persisted
|
||||
* @Assert\NotBlank(groups={"ApiTokenUpdate"})
|
||||
* @Assert\Length(min="8", max="60", groups={"ApiTokenUpdate"})
|
||||
*/
|
||||
private $plainApiToken;
|
||||
/**
|
||||
@@ -205,10 +209,79 @@ class User extends BaseUser implements UserInterface
|
||||
* @internal has no database mapping as the value is calculated from a permission
|
||||
*/
|
||||
private $isAllowedToSeeAllData = null;
|
||||
/**
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
*
|
||||
* @var string
|
||||
* @ORM\Column(name="username", type="string", length=180)
|
||||
* @Assert\NotBlank(groups={"Registration", "UserCreate", "Profile"})
|
||||
* @Assert\Length(min="2", max="60", groups={"Registration", "UserCreate", "Profile"})
|
||||
*/
|
||||
private $username;
|
||||
/**
|
||||
* @var string
|
||||
* @ORM\Column(name="email", type="string", length=180)
|
||||
* @Assert\NotBlank(groups={"Registration", "UserCreate", "Profile"})
|
||||
* @Assert\Length(min="2", max="180")
|
||||
* @Assert\Email(groups={"Registration", "UserCreate", "Profile"})
|
||||
*/
|
||||
private $email;
|
||||
/**
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
*
|
||||
* @var bool
|
||||
* @ORM\Column(name="enabled", type="boolean")
|
||||
*/
|
||||
private $enabled = false;
|
||||
/**
|
||||
* Encrypted password. Must be persisted.
|
||||
*
|
||||
* @var string
|
||||
* @ORM\Column(name="password", type="string")
|
||||
*/
|
||||
private $password;
|
||||
/**
|
||||
* Plain password. Used for model validation, not persisted.
|
||||
*
|
||||
* TODO make the password rules configurable
|
||||
*
|
||||
* @var string|null
|
||||
* @Assert\NotBlank(groups={"Registration", "PasswordUpdate", "UserCreate"})
|
||||
* @Assert\Length(min="8", max="60", groups={"Registration", "PasswordUpdate", "UserCreate", "ResetPassword", "ChangePassword"})
|
||||
*/
|
||||
private $plainPassword;
|
||||
/**
|
||||
* @var \DateTime|null
|
||||
* @ORM\Column(name="last_login", type="datetime", nullable=true)
|
||||
*/
|
||||
private $lastLogin;
|
||||
/**
|
||||
* Random string sent to the user email address in order to verify it.
|
||||
*
|
||||
* @var string|null
|
||||
* @ORM\Column(name="confirmation_token", type="string", length=180, unique=true, nullable=true)
|
||||
*/
|
||||
private $confirmationToken;
|
||||
/**
|
||||
* @var \DateTime|null
|
||||
* @ORM\Column(name="password_requested_at", type="datetime", nullable=true)
|
||||
*/
|
||||
private $passwordRequestedAt;
|
||||
/**
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"User_Entity"})
|
||||
* @Serializer\Type("array<string>")
|
||||
*
|
||||
* @var array
|
||||
* @ORM\Column(name="roles", type="array")
|
||||
* @Constraints\Role(groups={"RolesUpdate"})
|
||||
*/
|
||||
private $roles = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->registeredAt = new DateTime();
|
||||
$this->preferences = new ArrayCollection();
|
||||
$this->teams = new ArrayCollection();
|
||||
@@ -557,6 +630,265 @@ class User extends BaseUser implements UserInterface
|
||||
return $this->auth === null || $this->auth === self::AUTH_INTERNAL;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function addRole($role)
|
||||
{
|
||||
$role = strtoupper($role);
|
||||
if ($role === static::DEFAULT_ROLE) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
if (!\in_array($role, $this->roles, true)) {
|
||||
$this->roles[] = $role;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function eraseCredentials()
|
||||
{
|
||||
$this->plainPassword = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getUsername()
|
||||
{
|
||||
return $this->username;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getEmail()
|
||||
{
|
||||
return $this->email;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getPassword()
|
||||
{
|
||||
return $this->password;
|
||||
}
|
||||
|
||||
public function getPlainPassword(): ?string
|
||||
{
|
||||
return $this->plainPassword;
|
||||
}
|
||||
|
||||
public function getLastLogin(): ?DateTime
|
||||
{
|
||||
return $this->lastLogin;
|
||||
}
|
||||
|
||||
public function getConfirmationToken(): ?string
|
||||
{
|
||||
return $this->confirmationToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getRoles()
|
||||
{
|
||||
$roles = $this->roles;
|
||||
|
||||
// we need to make sure to have at least one role
|
||||
$roles[] = static::DEFAULT_ROLE;
|
||||
|
||||
return array_values(array_unique($roles));
|
||||
}
|
||||
|
||||
public function hasRole($role): bool
|
||||
{
|
||||
return \in_array(strtoupper($role), $this->getRoles(), true);
|
||||
}
|
||||
|
||||
public function setSuperAdmin(bool $isSuper): void
|
||||
{
|
||||
if (true === $isSuper) {
|
||||
$this->addRole(static::ROLE_SUPER_ADMIN);
|
||||
} else {
|
||||
$this->removeRole(static::ROLE_SUPER_ADMIN);
|
||||
}
|
||||
}
|
||||
|
||||
public function isSuperAdmin(): bool
|
||||
{
|
||||
return $this->hasRole(static::ROLE_SUPER_ADMIN);
|
||||
}
|
||||
|
||||
public function removeRole($role): User
|
||||
{
|
||||
if (false !== $key = array_search(strtoupper($role), $this->roles, true)) {
|
||||
unset($this->roles[$key]);
|
||||
$this->roles = array_values($this->roles);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setUsername($username): User
|
||||
{
|
||||
$this->username = $username;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setEmail($email): User
|
||||
{
|
||||
$this->email = $email;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isEnabled(): bool
|
||||
{
|
||||
return $this->enabled;
|
||||
}
|
||||
|
||||
public function setEnabled(bool $enabled): User
|
||||
{
|
||||
$this->enabled = $enabled;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setPassword($password): User
|
||||
{
|
||||
$this->password = $password;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setPlainPassword($password): User
|
||||
{
|
||||
$this->plainPassword = $password;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setLastLogin(\DateTime $time = null): User
|
||||
{
|
||||
$this->lastLogin = $time;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setConfirmationToken($confirmationToken): User
|
||||
{
|
||||
$this->confirmationToken = $confirmationToken;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setPasswordRequestedAt(\DateTime $date = null): User
|
||||
{
|
||||
$this->passwordRequestedAt = $date;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the timestamp that the user requested a password reset.
|
||||
*
|
||||
* @return DateTime|null
|
||||
*/
|
||||
public function getPasswordRequestedAt(): ?DateTime
|
||||
{
|
||||
return $this->passwordRequestedAt;
|
||||
}
|
||||
|
||||
public function isPasswordRequestNonExpired(int $seconds): bool
|
||||
{
|
||||
$date = $this->getPasswordRequestedAt();
|
||||
|
||||
if ($date === null || !($date instanceof DateTime)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $date->getTimestamp() + $seconds > time();
|
||||
}
|
||||
|
||||
public function setRoles(array $roles): User
|
||||
{
|
||||
$this->roles = [];
|
||||
|
||||
foreach ($roles as $role) {
|
||||
$this->addRole($role);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isEqualTo(UserInterface $user)
|
||||
{
|
||||
if (!$user instanceof self) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->password !== $user->getPassword()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->username !== $user->getUsername()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function serialize()
|
||||
{
|
||||
return serialize([
|
||||
$this->password,
|
||||
$this->username,
|
||||
$this->enabled,
|
||||
$this->id,
|
||||
$this->email,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function unserialize($serialized)
|
||||
{
|
||||
$data = unserialize($serialized);
|
||||
|
||||
// unserialize a user object from <= 1.14
|
||||
if (8 === \count($data)) {
|
||||
unset($data[1], $data[2], $data[7]);
|
||||
$data = array_values($data);
|
||||
}
|
||||
|
||||
list(
|
||||
$this->password,
|
||||
$this->username,
|
||||
$this->enabled,
|
||||
$this->id,
|
||||
$this->email) = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getSalt()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
|
||||
34
src/Event/AbstractUserEvent.php
Normal file
34
src/Event/AbstractUserEvent.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?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\Event;
|
||||
|
||||
use App\Entity\User;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* Base event class to used with user manipulations.
|
||||
*/
|
||||
abstract class AbstractUserEvent extends Event
|
||||
{
|
||||
/**
|
||||
* @var User
|
||||
*/
|
||||
private $user;
|
||||
|
||||
public function __construct(User $user)
|
||||
{
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
public function getUser(): User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
}
|
||||
28
src/Event/EmailEvent.php
Normal file
28
src/Event/EmailEvent.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?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\Event;
|
||||
|
||||
use Symfony\Component\Mime\Email;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
class EmailEvent extends Event
|
||||
{
|
||||
private $email;
|
||||
|
||||
public function __construct(Email $email)
|
||||
{
|
||||
$this->email = $email;
|
||||
}
|
||||
|
||||
public function getEmail(): Email
|
||||
{
|
||||
return $this->email;
|
||||
}
|
||||
}
|
||||
14
src/Event/EmailPasswordResetEvent.php
Normal file
14
src/Event/EmailPasswordResetEvent.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?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\Event;
|
||||
|
||||
final class EmailPasswordResetEvent extends UserEmailEvent
|
||||
{
|
||||
}
|
||||
14
src/Event/EmailSelfRegistrationEvent.php
Normal file
14
src/Event/EmailSelfRegistrationEvent.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?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\Event;
|
||||
|
||||
final class EmailSelfRegistrationEvent extends UserEmailEvent
|
||||
{
|
||||
}
|
||||
17
src/Event/UserCreateEvent.php
Normal file
17
src/Event/UserCreateEvent.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?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\Event;
|
||||
|
||||
/**
|
||||
* Triggered for new user instances, which might or might not be saved.
|
||||
*/
|
||||
final class UserCreateEvent extends AbstractUserEvent
|
||||
{
|
||||
}
|
||||
17
src/Event/UserCreatePostEvent.php
Normal file
17
src/Event/UserCreatePostEvent.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?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\Event;
|
||||
|
||||
/**
|
||||
* Triggered for new user instances, which were just saved.
|
||||
*/
|
||||
final class UserCreatePostEvent extends AbstractUserEvent
|
||||
{
|
||||
}
|
||||
17
src/Event/UserCreatePreEvent.php
Normal file
17
src/Event/UserCreatePreEvent.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?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\Event;
|
||||
|
||||
/**
|
||||
* Triggered for new user instances, which are just about to being saved.
|
||||
*/
|
||||
final class UserCreatePreEvent extends AbstractUserEvent
|
||||
{
|
||||
}
|
||||
29
src/Event/UserEmailEvent.php
Normal file
29
src/Event/UserEmailEvent.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?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\Event;
|
||||
|
||||
use App\Entity\User;
|
||||
use Symfony\Component\Mime\Email;
|
||||
|
||||
class UserEmailEvent extends EmailEvent
|
||||
{
|
||||
private $user;
|
||||
|
||||
public function __construct(User $user, Email $email)
|
||||
{
|
||||
parent::__construct($email);
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
public function getUser(): User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
}
|
||||
17
src/Event/UserInteractiveLoginEvent.php
Normal file
17
src/Event/UserInteractiveLoginEvent.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?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\Event;
|
||||
|
||||
/**
|
||||
* Triggered for programmatic logins (like password reset or registration).
|
||||
*/
|
||||
final class UserInteractiveLoginEvent extends AbstractUserEvent
|
||||
{
|
||||
}
|
||||
17
src/Event/UserUpdatePostEvent.php
Normal file
17
src/Event/UserUpdatePostEvent.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?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\Event;
|
||||
|
||||
/**
|
||||
* Triggered for user instances, which were just updated.
|
||||
*/
|
||||
final class UserUpdatePostEvent extends AbstractUserEvent
|
||||
{
|
||||
}
|
||||
17
src/Event/UserUpdatePreEvent.php
Normal file
17
src/Event/UserUpdatePreEvent.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?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\Event;
|
||||
|
||||
/**
|
||||
* Triggered for user instances, which are about to be updated.
|
||||
*/
|
||||
final class UserUpdatePreEvent extends AbstractUserEvent
|
||||
{
|
||||
}
|
||||
39
src/EventSubscriber/EmailSubscriber.php
Normal file
39
src/EventSubscriber/EmailSubscriber.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?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\EventSubscriber;
|
||||
|
||||
use App\Event\EmailEvent;
|
||||
use App\Mail\KimaiMailer;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
/**
|
||||
* Event subscriber to handle emails.
|
||||
*/
|
||||
final class EmailSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
private $mailer;
|
||||
|
||||
public function __construct(KimaiMailer $mailer)
|
||||
{
|
||||
$this->mailer = $mailer;
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
EmailEvent::class => ['onMailEvent', 100],
|
||||
];
|
||||
}
|
||||
|
||||
public function onMailEvent(EmailEvent $event)
|
||||
{
|
||||
$this->mailer->send($event->getEmail());
|
||||
}
|
||||
}
|
||||
56
src/EventSubscriber/LastLoginSubscriber.php
Normal file
56
src/EventSubscriber/LastLoginSubscriber.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?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\EventSubscriber;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Event\UserInteractiveLoginEvent;
|
||||
use App\Repository\UserRepository;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
|
||||
use Symfony\Component\Security\Http\SecurityEvents;
|
||||
|
||||
class LastLoginSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
private $repository;
|
||||
|
||||
public function __construct(UserRepository $repository)
|
||||
{
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public static function getSubscribedEvents()
|
||||
{
|
||||
return [
|
||||
UserInteractiveLoginEvent::class => 'onImplicitLogin',
|
||||
SecurityEvents::INTERACTIVE_LOGIN => 'onSecurityInteractiveLogin',
|
||||
];
|
||||
}
|
||||
|
||||
public function onImplicitLogin(UserInteractiveLoginEvent $event)
|
||||
{
|
||||
$user = $event->getUser();
|
||||
|
||||
$user->setLastLogin(new \DateTime());
|
||||
$this->repository->saveUser($user);
|
||||
}
|
||||
|
||||
public function onSecurityInteractiveLogin(InteractiveLoginEvent $event)
|
||||
{
|
||||
$user = $event->getAuthenticationToken()->getUser();
|
||||
|
||||
if ($user instanceof User) {
|
||||
$user->setLastLogin(new \DateTime());
|
||||
$this->repository->saveUser($user);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
<?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\EventSubscriber;
|
||||
|
||||
use App\Entity\User;
|
||||
use FOS\UserBundle\Event\FormEvent;
|
||||
use FOS\UserBundle\FOSUserEvents;
|
||||
use FOS\UserBundle\Model\UserManagerInterface;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
|
||||
/**
|
||||
* This class intercepts the registration to make sure:
|
||||
*
|
||||
* - the first-ever registered user will get the SUPER_ADMIN role
|
||||
* - the user uses the current request locale as initial language setting
|
||||
*/
|
||||
final class RegistrationSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
/**
|
||||
* @var UserManagerInterface
|
||||
*/
|
||||
private $userManager;
|
||||
/**
|
||||
* @var UrlGeneratorInterface
|
||||
*/
|
||||
private $router;
|
||||
|
||||
public function __construct(UserManagerInterface $userManager, UrlGeneratorInterface $router)
|
||||
{
|
||||
$this->userManager = $userManager;
|
||||
$this->router = $router;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
FOSUserEvents::REGISTRATION_SUCCESS => ['onRegistrationSuccess', 200],
|
||||
FOSUserEvents::RESETTING_RESET_SUCCESS => ['onResettingSuccess', 200],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormEvent $event
|
||||
*/
|
||||
public function onRegistrationSuccess(FormEvent $event)
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = $event->getForm()->getData();
|
||||
$roles = [User::ROLE_USER];
|
||||
|
||||
if (empty($this->userManager->findUsers())) {
|
||||
$roles = [User::ROLE_SUPER_ADMIN];
|
||||
}
|
||||
|
||||
$user->setLanguage($event->getRequest()->getLocale());
|
||||
$user->setRoles($roles);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormEvent $event
|
||||
*/
|
||||
public function onResettingSuccess(FormEvent $event)
|
||||
{
|
||||
$event->setResponse(new RedirectResponse($this->router->generate('my_profile')));
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
<?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\EventSubscriber;
|
||||
|
||||
use App\Entity\User;
|
||||
use FOS\UserBundle\Event\GetResponseNullableUserEvent;
|
||||
use FOS\UserBundle\FOSUserEvents;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
|
||||
/**
|
||||
* Makes sure that only internally registered users can reset their password.
|
||||
*/
|
||||
class ResetPasswordSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
FOSUserEvents::RESETTING_SEND_EMAIL_INITIALIZE => ['onInitializeResetPassword', 200]
|
||||
];
|
||||
}
|
||||
|
||||
public function onInitializeResetPassword(GetResponseNullableUserEvent $event)
|
||||
{
|
||||
$user = $event->getUser();
|
||||
if (!($user instanceof User)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// that is not nice :-D
|
||||
if (!$user->isInternalUser()) {
|
||||
throw new AccessDeniedHttpException(
|
||||
sprintf('The user "%s" tried to reset the password, but it is registered as "%s" auth-type.', $user->getUsername(), $user->getAuth())
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,7 +50,7 @@ final class Configuration
|
||||
*/
|
||||
private $constraints = [];
|
||||
|
||||
public function getName(): string
|
||||
public function getName(): ?string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ class SystemConfiguration
|
||||
public const SECTION_FORM_CUSTOMER = 'form_customer';
|
||||
public const SECTION_FORM_USER = 'form_user';
|
||||
public const SECTION_THEME = 'theme';
|
||||
public const SECTION_AUTHENTICATION = 'authentication';
|
||||
public const SECTION_CALENDAR = 'calendar';
|
||||
public const SECTION_BRANDING = 'branding';
|
||||
|
||||
@@ -50,6 +51,17 @@ class SystemConfiguration
|
||||
return $this->configuration;
|
||||
}
|
||||
|
||||
public function getConfigurationByName(string $name): ?Configuration
|
||||
{
|
||||
foreach ($this->configuration as $configuration) {
|
||||
if ($configuration->getName() === $name) {
|
||||
return $configuration;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Configuration[] $configuration
|
||||
* @return SystemConfiguration
|
||||
|
||||
57
src/Form/PasswordResetForm.php
Normal file
57
src/Form/PasswordResetForm.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?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\Form;
|
||||
|
||||
use App\Entity\User;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class PasswordResetForm extends AbstractType
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder->add('plainPassword', RepeatedType::class, [
|
||||
'type' => PasswordType::class,
|
||||
'options' => [
|
||||
'attr' => [
|
||||
'autocomplete' => 'new-password',
|
||||
],
|
||||
],
|
||||
'first_options' => ['label' => 'label.password'],
|
||||
'second_options' => ['label' => 'label.password_repeat'],
|
||||
'invalid_message' => 'The entered passwords don\'t match.',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => User::class,
|
||||
'csrf_token_id' => 'resetting',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getBlockPrefix()
|
||||
{
|
||||
return 'password_resetting';
|
||||
}
|
||||
}
|
||||
62
src/Form/SelfRegistrationForm.php
Normal file
62
src/Form/SelfRegistrationForm.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?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\Form;
|
||||
|
||||
use App\Entity\User;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\EmailType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class SelfRegistrationForm extends AbstractType
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder
|
||||
->add('email', EmailType::class, ['label' => 'label.email'])
|
||||
->add('username', null, ['label' => 'Username', 'translation_domain' => 'AdminLTEBundle'])
|
||||
->add('plainPassword', RepeatedType::class, [
|
||||
'type' => PasswordType::class,
|
||||
'options' => [
|
||||
'attr' => [
|
||||
'autocomplete' => 'new-password',
|
||||
],
|
||||
],
|
||||
'first_options' => ['label' => 'label.password'],
|
||||
'second_options' => ['label' => 'label.password_repeat'],
|
||||
'invalid_message' => 'The entered passwords don\'t match.',
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => User::class,
|
||||
'csrf_token_id' => 'registration',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getBlockPrefix()
|
||||
{
|
||||
return 'user_registration';
|
||||
}
|
||||
}
|
||||
@@ -205,9 +205,6 @@ class Kernel extends BaseKernel
|
||||
{
|
||||
$confDir = $this->getProjectDir() . '/config';
|
||||
|
||||
// some routes are based on app configs and will be imported manually
|
||||
$this->configureFosUserRoutes($routes);
|
||||
|
||||
// load bundle specific route files
|
||||
if (is_dir($confDir . '/routes/')) {
|
||||
$routes->import($confDir . '/routes/*' . self::CONFIG_EXTS, '/', 'glob');
|
||||
@@ -227,25 +224,4 @@ class Kernel extends BaseKernel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function configureFosUserRoutes(RouteCollectionBuilder $routes)
|
||||
{
|
||||
$features = $this->getContainer()->getParameter('kimai.fosuser');
|
||||
|
||||
// Expose the user registration feature
|
||||
if ($features['registration']) {
|
||||
$routes->import(
|
||||
'@FOSUserBundle/Resources/config/routing/registration.xml',
|
||||
'/{_locale}/register'
|
||||
);
|
||||
}
|
||||
|
||||
// Expose the users password-reset feature
|
||||
if ($features['password_reset']) {
|
||||
$routes->import(
|
||||
'@FOSUserBundle/Resources/config/routing/resetting.xml',
|
||||
'/{_locale}/resetting'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,12 +20,12 @@ use Symfony\Component\DependencyInjection\Reference;
|
||||
*/
|
||||
class FormLoginLdapFactory implements SecurityFactoryInterface
|
||||
{
|
||||
public function create(ContainerBuilder $container, $id, $config, $userProviderId, $defaultEntryPoint)
|
||||
public function create(ContainerBuilder $container, $id, $config, $userProviderId, $defaultEntryPointId)
|
||||
{
|
||||
$authProviderId = $this->createAuthProvider($container, $id, $userProviderId);
|
||||
$listenerId = $this->createListener($container, $id, $config);
|
||||
|
||||
return [$authProviderId, $listenerId, $defaultEntryPoint];
|
||||
return [$authProviderId, $listenerId, $defaultEntryPointId];
|
||||
}
|
||||
|
||||
public function getPosition()
|
||||
@@ -38,7 +38,7 @@ class FormLoginLdapFactory implements SecurityFactoryInterface
|
||||
return 'kimai_ldap';
|
||||
}
|
||||
|
||||
public function addConfiguration(NodeDefinition $node)
|
||||
public function addConfiguration(NodeDefinition $builder)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace App\Ldap;
|
||||
use App\Configuration\LdapConfiguration;
|
||||
use App\Entity\User;
|
||||
use Symfony\Component\Security\Core\Authentication\Provider\UserAuthenticationProvider;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationServiceException;
|
||||
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
|
||||
@@ -48,6 +49,15 @@ class LdapAuthenticationProvider extends UserAuthenticationProvider
|
||||
$this->userProvider = $userProvider;
|
||||
}
|
||||
|
||||
public function supports(TokenInterface $token)
|
||||
{
|
||||
if (!$this->config->isActivated()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return parent::supports($token);
|
||||
}
|
||||
|
||||
protected function retrieveUser($username, UsernamePasswordToken $token)
|
||||
{
|
||||
$user = $token->getUser();
|
||||
@@ -56,7 +66,7 @@ class LdapAuthenticationProvider extends UserAuthenticationProvider
|
||||
}
|
||||
|
||||
try {
|
||||
// this will always query the FOSUserBundle first...
|
||||
// this will always query the internal database first...
|
||||
// only first-time logins from LDAP user (not yet existing in local user database)
|
||||
// will actually hit the LdapUserProvider
|
||||
$user = $this->userProvider->loadUserByUsername($username);
|
||||
|
||||
@@ -15,29 +15,17 @@ use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
||||
/**
|
||||
* Inspired by https://github.com/Maks3w/FR3DLdapBundle @ MIT License
|
||||
*
|
||||
* @final
|
||||
*/
|
||||
class LdapManager
|
||||
{
|
||||
/**
|
||||
* @var LdapConfiguration
|
||||
*/
|
||||
protected $config;
|
||||
/**
|
||||
* @var LdapDriver
|
||||
*/
|
||||
protected $driver;
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $params = [];
|
||||
/**
|
||||
* @var LdapUserHydrator
|
||||
*/
|
||||
protected $hydrator;
|
||||
private $driver;
|
||||
private $hydrator;
|
||||
private $config;
|
||||
|
||||
public function __construct(LdapDriver $driver, LdapUserHydrator $hydrator, LdapConfiguration $config)
|
||||
{
|
||||
$this->params = $config->getUserParameters();
|
||||
$this->config = $config;
|
||||
$this->driver = $driver;
|
||||
$this->hydrator = $hydrator;
|
||||
@@ -52,7 +40,9 @@ class LdapManager
|
||||
*/
|
||||
public function findUserByUsername(string $username): ?UserInterface
|
||||
{
|
||||
return $this->findUserBy([$this->params['usernameAttribute'] => $username]);
|
||||
$params = $this->config->getUserParameters();
|
||||
|
||||
return $this->findUserBy([$params['usernameAttribute'] => $username]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -62,8 +52,9 @@ class LdapManager
|
||||
*/
|
||||
public function findUserBy(array $criteria): ?UserInterface
|
||||
{
|
||||
$params = $this->config->getUserParameters();
|
||||
$filter = $this->buildFilter($criteria);
|
||||
$entries = $this->driver->search($this->params['baseDn'], $filter);
|
||||
$entries = $this->driver->search($params['baseDn'], $filter);
|
||||
|
||||
if ($entries['count'] > 1) {
|
||||
throw new LdapDriverException('This search must only return a single user');
|
||||
@@ -77,10 +68,12 @@ class LdapManager
|
||||
return $this->hydrator->hydrate($entries[0]);
|
||||
}
|
||||
|
||||
protected function buildFilter(array $criteria, string $condition = '&'): string
|
||||
private function buildFilter(array $criteria, string $condition = '&'): string
|
||||
{
|
||||
$params = $this->config->getUserParameters();
|
||||
|
||||
$filters = [];
|
||||
$filters[] = $this->params['filter'];
|
||||
$filters[] = $params['filter'];
|
||||
foreach ($criteria as $key => $value) {
|
||||
$value = ldap_escape($value, '', LDAP_ESCAPE_FILTER);
|
||||
$filters[] = sprintf('(%s=%s)', $key, $value);
|
||||
@@ -118,7 +111,8 @@ class LdapManager
|
||||
}
|
||||
$user->setPreferenceValue('ldap.dn', $baseDn);
|
||||
|
||||
$entries = $this->driver->search($baseDn, $this->params['attributesFilter']);
|
||||
$params = $this->config->getUserParameters();
|
||||
$entries = $this->driver->search($baseDn, $params['attributesFilter']);
|
||||
|
||||
if ($entries['count'] > 1) {
|
||||
throw new LdapDriverException('This search must only return a single user');
|
||||
@@ -151,7 +145,7 @@ class LdapManager
|
||||
}
|
||||
}
|
||||
|
||||
protected function getRoles(string $dn, array $roleParameter): array
|
||||
private function getRoles(string $dn, array $roleParameter): array
|
||||
{
|
||||
$filter = $roleParameter['filter'] ?? '';
|
||||
|
||||
|
||||
@@ -20,21 +20,13 @@ use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
* Overwritten to be able to deactivate LDAP via config switch.
|
||||
*
|
||||
* Inspired by https://github.com/Maks3w/FR3DLdapBundle @ MIT License
|
||||
*
|
||||
* @final
|
||||
*/
|
||||
class LdapUserProvider implements UserProviderInterface
|
||||
{
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $activated = false;
|
||||
/**
|
||||
* @var LdapManager
|
||||
*/
|
||||
protected $ldapManager;
|
||||
/**
|
||||
* @var LoggerInterface|null
|
||||
*/
|
||||
protected $logger;
|
||||
private $ldapManager;
|
||||
private $logger;
|
||||
|
||||
public function __construct(LdapManager $ldapManager, LoggerInterface $logger = null)
|
||||
{
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
<?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\Mail;
|
||||
|
||||
use App\Entity\User;
|
||||
use FOS\UserBundle\Mailer\MailerInterface as FOSMailerInterface;
|
||||
use FOS\UserBundle\Model\UserInterface;
|
||||
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||
use Symfony\Component\Mime\Address;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
final class UserMails implements FOSMailerInterface
|
||||
{
|
||||
/**
|
||||
* @var KimaiMailer
|
||||
*/
|
||||
private $mailer;
|
||||
/**
|
||||
* @var UrlGeneratorInterface
|
||||
*/
|
||||
private $router;
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
public function __construct(KimaiMailer $mailer, UrlGeneratorInterface $router, TranslatorInterface $translator)
|
||||
{
|
||||
$this->mailer = $mailer;
|
||||
$this->router = $router;
|
||||
$this->translator = $translator;
|
||||
}
|
||||
|
||||
public function sendConfirmationEmailMessage(UserInterface $user)
|
||||
{
|
||||
$username = $user->getUsername();
|
||||
$language = User::DEFAULT_LANGUAGE;
|
||||
|
||||
if ($user instanceof User) {
|
||||
$username = $user->getDisplayName();
|
||||
$language = $user->getLanguage();
|
||||
}
|
||||
|
||||
$url = $this->router->generate('fos_user_registration_confirm', ['token' => $user->getConfirmationToken()], UrlGeneratorInterface::ABSOLUTE_URL);
|
||||
|
||||
$email = (new TemplatedEmail())
|
||||
->to(new Address($user->getEmail()))
|
||||
->subject(
|
||||
$this->translator->trans('registration.subject', ['%username%' => $username], 'email', $language)
|
||||
)
|
||||
->htmlTemplate('emails/confirmation.html.twig')
|
||||
->context([
|
||||
'user' => $user,
|
||||
'username' => $username,
|
||||
'confirmationUrl' => $url,
|
||||
])
|
||||
;
|
||||
|
||||
$this->mailer->send($email);
|
||||
}
|
||||
|
||||
public function sendResettingEmailMessage(UserInterface $user)
|
||||
{
|
||||
$username = $user->getUsername();
|
||||
$language = User::DEFAULT_LANGUAGE;
|
||||
|
||||
if ($user instanceof User) {
|
||||
$username = $user->getDisplayName();
|
||||
$language = $user->getLanguage();
|
||||
}
|
||||
|
||||
$url = $this->router->generate('fos_user_resetting_reset', ['token' => $user->getConfirmationToken()], UrlGeneratorInterface::ABSOLUTE_URL);
|
||||
|
||||
$email = (new TemplatedEmail())
|
||||
->to(new Address($user->getEmail()))
|
||||
->subject(
|
||||
$this->translator->trans('reset.subject', ['%username%' => $username], 'email', $language)
|
||||
)
|
||||
->htmlTemplate('emails/password-reset.html.twig')
|
||||
->context([
|
||||
'user' => $user,
|
||||
'username' => $username,
|
||||
'confirmationUrl' => $url,
|
||||
])
|
||||
;
|
||||
|
||||
$this->mailer->send($email);
|
||||
}
|
||||
}
|
||||
43
src/Migrations/Version20210605154245.php
Normal file
43
src/Migrations/Version20210605154245.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* 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 DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* @version 1.15
|
||||
*/
|
||||
final class Version20210605154245 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Cleans up User table';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$user = $schema->getTable('kimai2_users');
|
||||
$user->dropIndex('UNIQ_B9AC5BCE92FC23A8');
|
||||
$user->dropIndex('UNIQ_B9AC5BCEA0D96FBF');
|
||||
$user->dropColumn('username_canonical');
|
||||
$user->dropColumn('email_canonical');
|
||||
$user->dropColumn('salt');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE kimai2_users ADD username_canonical VARCHAR(180) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`, ADD email_canonical VARCHAR(180) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`, ADD salt VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`');
|
||||
$this->addSql('CREATE UNIQUE INDEX UNIQ_B9AC5BCE92FC23A8 ON kimai2_users (username_canonical)');
|
||||
$this->addSql('CREATE UNIQUE INDEX UNIQ_B9AC5BCEA0D96FBF ON kimai2_users (email_canonical)');
|
||||
}
|
||||
}
|
||||
@@ -262,7 +262,7 @@ class UserRepository extends EntityRepository implements UserLoaderInterface
|
||||
if ($query->getRole() !== null) {
|
||||
$rolesWhere = 'u.roles LIKE :role';
|
||||
$qb->setParameter('role', '%' . $query->getRole() . '%');
|
||||
// a hack as FOSUserBundle does not save the ROLE_USER in the database as it is the default role
|
||||
// a workaround, because ROLE_USER is not saved in the database
|
||||
if ($query->getRole() === User::ROLE_USER) {
|
||||
$rolesWhere .= ' OR u.roles LIKE :role1';
|
||||
$qb->setParameter('role1', '%{}');
|
||||
|
||||
75
src/Saml/Firewall/SamlListener.php
Normal file
75
src/Saml/Firewall/SamlListener.php
Normal file
@@ -0,0 +1,75 @@
|
||||
<?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\Saml\Firewall;
|
||||
|
||||
use App\Saml\SamlAuthFactory;
|
||||
use App\Saml\Token\SamlToken;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||
use Symfony\Component\Security\Http\Firewall\AbstractAuthenticationListener;
|
||||
|
||||
class SamlListener extends AbstractAuthenticationListener
|
||||
{
|
||||
/**
|
||||
* @var SamlAuthFactory
|
||||
*/
|
||||
protected $authFactory;
|
||||
|
||||
public function setAuth(SamlAuthFactory $authFactory): void
|
||||
{
|
||||
$this->authFactory = $authFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs authentication.
|
||||
*
|
||||
* @param Request $request A Request instance
|
||||
* @return TokenInterface|Response|null The authenticated token, null if full authentication is not possible, or a Response
|
||||
*
|
||||
* @throws AuthenticationException if the authentication fails
|
||||
* @throws \Exception if attribute set by "username_attribute" option not found
|
||||
*/
|
||||
protected function attemptAuthentication(Request $request)
|
||||
{
|
||||
$oneLoginAuth = $this->authFactory->create();
|
||||
|
||||
$oneLoginAuth->processResponse();
|
||||
if ($oneLoginAuth->getErrors()) {
|
||||
$this->logger->error($oneLoginAuth->getLastErrorReason());
|
||||
throw new AuthenticationException($oneLoginAuth->getLastErrorReason());
|
||||
}
|
||||
|
||||
$attributes = [];
|
||||
if (isset($this->options['use_attribute_friendly_name']) && $this->options['use_attribute_friendly_name']) {
|
||||
$attributes = $oneLoginAuth->getAttributesWithFriendlyName();
|
||||
} else {
|
||||
$attributes = $oneLoginAuth->getAttributes();
|
||||
}
|
||||
$attributes['sessionIndex'] = $oneLoginAuth->getSessionIndex();
|
||||
$token = new SamlToken();
|
||||
$token->setAttributes($attributes);
|
||||
|
||||
if (isset($this->options['username_attribute'])) {
|
||||
if (!\array_key_exists($this->options['username_attribute'], $attributes)) {
|
||||
$this->logger->error(sprintf('Found attributes: %s', print_r($attributes, true)));
|
||||
throw new \Exception(sprintf("Attribute '%s' not found in SAML data", $this->options['username_attribute']));
|
||||
}
|
||||
|
||||
$username = $attributes[$this->options['username_attribute']][0];
|
||||
} else {
|
||||
$username = $oneLoginAuth->getNameId();
|
||||
}
|
||||
$token->setUser($username);
|
||||
|
||||
return $this->authenticationManager->authenticate($token);
|
||||
}
|
||||
}
|
||||
@@ -9,8 +9,8 @@
|
||||
|
||||
namespace App\Saml\Logout;
|
||||
|
||||
use App\Saml\SamlAuth;
|
||||
use Hslavich\OneloginSamlBundle\Security\Authentication\Token\SamlTokenInterface;
|
||||
use App\Saml\SamlAuthFactory;
|
||||
use App\Saml\Token\SamlTokenInterface;
|
||||
use OneLogin\Saml2\Error;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
@@ -20,11 +20,11 @@ use Symfony\Component\Security\Http\Logout\LogoutHandlerInterface;
|
||||
final class SamlLogoutHandler implements LogoutHandlerInterface
|
||||
{
|
||||
/**
|
||||
* @var SamlAuth
|
||||
* @var SamlAuthFactory
|
||||
*/
|
||||
private $samlAuth;
|
||||
|
||||
public function __construct(SamlAuth $samlAuth)
|
||||
public function __construct(SamlAuthFactory $samlAuth)
|
||||
{
|
||||
$this->samlAuth = $samlAuth;
|
||||
}
|
||||
@@ -44,12 +44,14 @@ final class SamlLogoutHandler implements LogoutHandlerInterface
|
||||
return;
|
||||
}
|
||||
|
||||
$samlAuth = $this->samlAuth->create();
|
||||
|
||||
try {
|
||||
$this->samlAuth->processSLO();
|
||||
$samlAuth->processSLO();
|
||||
} catch (Error $e) {
|
||||
if (!empty($this->samlAuth->getSLOurl())) {
|
||||
if (!empty($samlAuth->getSLOurl())) {
|
||||
$sessionIndex = $token->hasAttribute('sessionIndex') ? $token->getAttribute('sessionIndex') : null;
|
||||
$this->samlAuth->logout(null, [], $token->getUsername(), $sessionIndex);
|
||||
$samlAuth->logout(null, [], $token->getUsername(), $sessionIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,11 +9,12 @@
|
||||
|
||||
namespace App\Saml\Provider;
|
||||
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Entity\User;
|
||||
use App\Repository\UserRepository;
|
||||
use App\Saml\SamlTokenFactory;
|
||||
use App\Saml\Token\SamlTokenInterface;
|
||||
use App\Saml\User\SamlUserFactory;
|
||||
use Hslavich\OneloginSamlBundle\Security\Authentication\Token\SamlTokenInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||
@@ -22,29 +23,19 @@ use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
|
||||
final class SamlProvider implements AuthenticationProviderInterface
|
||||
{
|
||||
/**
|
||||
* @var UserProviderInterface
|
||||
*/
|
||||
private $userProvider;
|
||||
/**
|
||||
* @var SamlUserFactory
|
||||
*/
|
||||
private $userFactory;
|
||||
/**
|
||||
* @var SamlTokenFactory
|
||||
*/
|
||||
private $tokenFactory;
|
||||
/**
|
||||
* @var UserRepository
|
||||
*/
|
||||
private $repository;
|
||||
private $configuration;
|
||||
|
||||
public function __construct(UserRepository $repository, UserProviderInterface $userProvider, SamlTokenFactory $tokenFactory, SamlUserFactory $userFactory)
|
||||
public function __construct(UserRepository $repository, UserProviderInterface $userProvider, SamlTokenFactory $tokenFactory, SamlUserFactory $userFactory, SystemConfiguration $configuration)
|
||||
{
|
||||
$this->repository = $repository;
|
||||
$this->userProvider = $userProvider;
|
||||
$this->tokenFactory = $tokenFactory;
|
||||
$this->userFactory = $userFactory;
|
||||
$this->configuration = $configuration;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -83,6 +74,10 @@ final class SamlProvider implements AuthenticationProviderInterface
|
||||
|
||||
public function supports(TokenInterface $token)
|
||||
{
|
||||
if (!$this->configuration->isSamlActive()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $token instanceof SamlTokenInterface;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
<?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\Saml;
|
||||
|
||||
use OneLogin\Saml2\Auth;
|
||||
use OneLogin\Saml2\Utils;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
|
||||
class SamlAuth extends Auth
|
||||
{
|
||||
public function __construct(RequestStack $request, array $settings = null)
|
||||
{
|
||||
parent::__construct($settings);
|
||||
|
||||
if (null !== $request->getMasterRequest() && $request->getMasterRequest()->isFromTrustedProxy()) {
|
||||
Utils::setProxyVars(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
39
src/Saml/SamlAuthFactory.php
Normal file
39
src/Saml/SamlAuthFactory.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?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\Saml;
|
||||
|
||||
use App\Configuration\SamlConfiguration;
|
||||
use OneLogin\Saml2\Auth;
|
||||
use OneLogin\Saml2\Utils;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
|
||||
/**
|
||||
* @final
|
||||
*/
|
||||
class SamlAuthFactory
|
||||
{
|
||||
private $request;
|
||||
private $configuration;
|
||||
|
||||
public function __construct(RequestStack $request, SamlConfiguration $configuration)
|
||||
{
|
||||
$this->request = $request;
|
||||
$this->configuration = $configuration;
|
||||
}
|
||||
|
||||
public function create(): Auth
|
||||
{
|
||||
if (null !== $this->request->getMasterRequest() && $this->request->getMasterRequest()->isFromTrustedProxy()) {
|
||||
Utils::setProxyVars(true);
|
||||
}
|
||||
|
||||
return new Auth($this->configuration->getConnection());
|
||||
}
|
||||
}
|
||||
@@ -9,15 +9,11 @@
|
||||
|
||||
namespace App\Saml;
|
||||
|
||||
use Hslavich\OneloginSamlBundle\Security\Authentication\Token\SamlToken;
|
||||
use Hslavich\OneloginSamlBundle\Security\Authentication\Token\SamlTokenFactoryInterface;
|
||||
use App\Saml\Token\SamlToken;
|
||||
|
||||
final class SamlTokenFactory implements SamlTokenFactoryInterface
|
||||
final class SamlTokenFactory
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function createToken($user, array $attributes, array $roles)
|
||||
public function createToken($user, array $attributes, array $roles): SamlToken
|
||||
{
|
||||
$token = new SamlToken($roles);
|
||||
$token->setUser($user);
|
||||
|
||||
20
src/Saml/Token/SamlToken.php
Normal file
20
src/Saml/Token/SamlToken.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?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\Saml\Token;
|
||||
|
||||
use Symfony\Component\Security\Core\Authentication\Token\AbstractToken;
|
||||
|
||||
class SamlToken extends AbstractToken implements SamlTokenInterface
|
||||
{
|
||||
public function getCredentials()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
16
src/Saml/Token/SamlTokenInterface.php
Normal file
16
src/Saml/Token/SamlTokenInterface.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?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\Saml\Token;
|
||||
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
|
||||
interface SamlTokenInterface extends TokenInterface
|
||||
{
|
||||
}
|
||||
@@ -9,38 +9,22 @@
|
||||
|
||||
namespace App\Saml\User;
|
||||
|
||||
use App\Configuration\SamlConfiguration;
|
||||
use App\Entity\User;
|
||||
use Hslavich\OneloginSamlBundle\Security\Authentication\Token\SamlTokenInterface;
|
||||
use Hslavich\OneloginSamlBundle\Security\User\SamlUserFactoryInterface;
|
||||
use App\Saml\Token\SamlTokenInterface;
|
||||
|
||||
final class SamlUserFactory implements SamlUserFactoryInterface
|
||||
final class SamlUserFactory
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $mapping;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $groupAttribute;
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $groupMapping;
|
||||
private $configuration;
|
||||
|
||||
public function __construct(array $attributes)
|
||||
public function __construct(SamlConfiguration $configuration)
|
||||
{
|
||||
$this->mapping = $attributes['mapping'];
|
||||
$this->groupAttribute = $attributes['roles']['attribute'];
|
||||
$this->groupMapping = $attributes['roles']['mapping'];
|
||||
$this->configuration = $configuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param SamlTokenInterface $token
|
||||
* @return User
|
||||
*/
|
||||
public function createUser(SamlTokenInterface $token)
|
||||
public function createUser(SamlTokenInterface $token): User
|
||||
{
|
||||
// Not using UserService: user settings should be set via SAML attributes
|
||||
$user = new User();
|
||||
$user->setEnabled(true);
|
||||
$user->setUsername($token->getUsername());
|
||||
@@ -52,17 +36,20 @@ final class SamlUserFactory implements SamlUserFactoryInterface
|
||||
|
||||
public function hydrateUser(User $user, SamlTokenInterface $token): void
|
||||
{
|
||||
$groupAttribute = $this->configuration->getRolesAttribute();
|
||||
$groupMapping = $this->configuration->getRolesMapping();
|
||||
|
||||
// extract user roles from a special saml attribute
|
||||
if (!empty($this->groupAttribute) && $token->hasAttribute($this->groupAttribute)) {
|
||||
if (!empty($groupAttribute) && $token->hasAttribute($groupAttribute)) {
|
||||
$groupMap = [];
|
||||
foreach ($this->groupMapping as $mapping) {
|
||||
foreach ($groupMapping as $mapping) {
|
||||
$field = $mapping['kimai'];
|
||||
$attribute = $mapping['saml'];
|
||||
$groupMap[$attribute] = $field;
|
||||
}
|
||||
|
||||
$roles = [];
|
||||
$samlGroups = $token->getAttribute($this->groupAttribute);
|
||||
$samlGroups = $token->getAttribute($groupAttribute);
|
||||
foreach ($samlGroups as $groupName) {
|
||||
if (\array_key_exists($groupName, $groupMap)) {
|
||||
$roles[] = $groupMap[$groupName];
|
||||
@@ -71,7 +58,9 @@ final class SamlUserFactory implements SamlUserFactoryInterface
|
||||
$user->setRoles($roles);
|
||||
}
|
||||
|
||||
foreach ($this->mapping as $mapping) {
|
||||
$mappingConfig = $this->configuration->getAttributeMapping();
|
||||
|
||||
foreach ($mappingConfig as $mapping) {
|
||||
$field = $mapping['kimai'];
|
||||
$attribute = $mapping['saml'];
|
||||
$value = $this->getPropertyValue($token, $attribute);
|
||||
|
||||
@@ -11,12 +11,14 @@ namespace App\Security;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Repository\UserRepository;
|
||||
use Exception;
|
||||
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
|
||||
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
|
||||
use Symfony\Component\Security\Core\User\UserInterface as SecurityUserInterface;
|
||||
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
|
||||
final class DoctrineUserProvider implements UserProviderInterface
|
||||
final class DoctrineUserProvider implements UserProviderInterface, PasswordUpgraderInterface
|
||||
{
|
||||
/**
|
||||
* @var UserRepository
|
||||
@@ -51,7 +53,7 @@ final class DoctrineUserProvider implements UserProviderInterface
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function refreshUser(SecurityUserInterface $user)
|
||||
public function refreshUser(UserInterface $user)
|
||||
{
|
||||
if (!$user instanceof User) {
|
||||
throw new UnsupportedUserException(sprintf('Expected an instance of %s, but got "%s".', User::class, \get_class($user)));
|
||||
@@ -74,4 +76,15 @@ final class DoctrineUserProvider implements UserProviderInterface
|
||||
{
|
||||
return $class === User::class;
|
||||
}
|
||||
|
||||
public function upgradePassword(UserInterface $user, string $newEncodedPassword): void
|
||||
{
|
||||
if ($user instanceof User) {
|
||||
try {
|
||||
$user->setPassword($newEncodedPassword);
|
||||
$this->repository->saveUser($user);
|
||||
} catch (Exception $e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
91
src/Security/KimaiUserProvider.php
Normal file
91
src/Security/KimaiUserProvider.php
Normal file
@@ -0,0 +1,91 @@
|
||||
<?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\Security;
|
||||
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Ldap\LdapUserProvider;
|
||||
use Symfony\Component\Security\Core\User\ChainUserProvider;
|
||||
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
|
||||
class KimaiUserProvider implements UserProviderInterface, PasswordUpgraderInterface
|
||||
{
|
||||
private $providers;
|
||||
private $provider;
|
||||
private $configuration;
|
||||
|
||||
/**
|
||||
* @param iterable|UserProviderInterface[] $providers
|
||||
*/
|
||||
public function __construct(iterable $providers, SystemConfiguration $configuration)
|
||||
{
|
||||
$this->providers = $providers;
|
||||
$this->configuration = $configuration;
|
||||
}
|
||||
|
||||
private function getInternalProvider(): ChainUserProvider
|
||||
{
|
||||
if ($this->provider === null) {
|
||||
$activated = [];
|
||||
foreach ($this->providers as $provider) {
|
||||
if ($provider instanceof LdapUserProvider) {
|
||||
if (!$this->configuration->isLdapActive()) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
$activated[] = $provider;
|
||||
}
|
||||
$this->provider = new ChainUserProvider(new \ArrayIterator($activated));
|
||||
}
|
||||
|
||||
return $this->provider;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getProviders()
|
||||
{
|
||||
return $this->getInternalProvider()->getProviders();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function loadUserByUsername($username)
|
||||
{
|
||||
return $this->getInternalProvider()->loadUserByUsername($username);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function refreshUser(UserInterface $user)
|
||||
{
|
||||
return $this->getInternalProvider()->refreshUser($user);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function supportsClass($class)
|
||||
{
|
||||
return $this->getInternalProvider()->supportsClass($class);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function upgradePassword(UserInterface $user, string $newEncodedPassword): void
|
||||
{
|
||||
$this->getInternalProvider()->upgradePassword($user, $newEncodedPassword);
|
||||
}
|
||||
}
|
||||
@@ -19,21 +19,16 @@ use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
use Symfony\Component\Security\Guard\AbstractGuardAuthenticator;
|
||||
use Symfony\Component\Security\Guard\PasswordAuthenticatedInterface;
|
||||
|
||||
class TokenAuthenticator extends AbstractGuardAuthenticator
|
||||
class TokenAuthenticator extends AbstractGuardAuthenticator implements PasswordAuthenticatedInterface
|
||||
{
|
||||
public const HEADER_USERNAME = 'X-AUTH-USER';
|
||||
public const HEADER_TOKEN = 'X-AUTH-TOKEN';
|
||||
public const HEADER_JAVASCRIPT = 'X-AUTH-SESSION';
|
||||
|
||||
/**
|
||||
* @var EncoderFactoryInterface
|
||||
*/
|
||||
protected $encoderFactory;
|
||||
private $encoderFactory;
|
||||
|
||||
/**
|
||||
* @param EncoderFactoryInterface $encoderFactory
|
||||
*/
|
||||
public function __construct(EncoderFactoryInterface $encoderFactory)
|
||||
{
|
||||
$this->encoderFactory = $encoderFactory;
|
||||
@@ -163,4 +158,13 @@ class TokenAuthenticator extends AbstractGuardAuthenticator
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getPassword($credentials): ?string
|
||||
{
|
||||
if (!\is_array($credentials) || !\array_key_exists('token', $credentials) || empty($credentials['token'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $credentials['token'];
|
||||
}
|
||||
}
|
||||
|
||||
72
src/User/LoginManager.php
Normal file
72
src/User/LoginManager.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?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\User;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Event\UserInteractiveLoginEvent;
|
||||
use App\Security\UserChecker;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
|
||||
use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface;
|
||||
use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategyInterface;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
final class LoginManager
|
||||
{
|
||||
private $tokenStorage;
|
||||
private $userChecker;
|
||||
private $sessionStrategy;
|
||||
private $requestStack;
|
||||
private $eventDispatcher;
|
||||
private $rememberMeService;
|
||||
|
||||
public function __construct(
|
||||
TokenStorageInterface $tokenStorage,
|
||||
UserChecker $userChecker,
|
||||
SessionAuthenticationStrategyInterface $sessionStrategy,
|
||||
RequestStack $requestStack,
|
||||
EventDispatcherInterface $eventDispatcher,
|
||||
RememberMeServicesInterface $rememberMeService = null
|
||||
) {
|
||||
$this->tokenStorage = $tokenStorage;
|
||||
$this->userChecker = $userChecker;
|
||||
$this->sessionStrategy = $sessionStrategy;
|
||||
$this->requestStack = $requestStack;
|
||||
$this->eventDispatcher = $eventDispatcher;
|
||||
$this->rememberMeService = $rememberMeService;
|
||||
}
|
||||
|
||||
public function logInUser(User $user, Response $response = null)
|
||||
{
|
||||
$this->userChecker->checkPreAuth($user);
|
||||
|
||||
$token = $this->createToken('secured_area', $user);
|
||||
$request = $this->requestStack->getCurrentRequest();
|
||||
|
||||
if (null !== $request) {
|
||||
$this->sessionStrategy->onAuthentication($request, $token);
|
||||
|
||||
if (null !== $response && null !== $this->rememberMeService) {
|
||||
$this->rememberMeService->loginSuccess($request, $response, $token);
|
||||
}
|
||||
}
|
||||
|
||||
$this->tokenStorage->setToken($token);
|
||||
|
||||
$this->eventDispatcher->dispatch(new UserInteractiveLoginEvent($user));
|
||||
}
|
||||
|
||||
private function createToken(string $firewall, User $user): UsernamePasswordToken
|
||||
{
|
||||
return new UsernamePasswordToken($user, null, $firewall, $user->getRoles());
|
||||
}
|
||||
}
|
||||
168
src/User/UserService.php
Normal file
168
src/User/UserService.php
Normal file
@@ -0,0 +1,168 @@
|
||||
<?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\User;
|
||||
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Entity\User;
|
||||
use App\Entity\UserPreference;
|
||||
use App\Event\UserCreateEvent;
|
||||
use App\Event\UserCreatePostEvent;
|
||||
use App\Event\UserCreatePreEvent;
|
||||
use App\Event\UserUpdatePostEvent;
|
||||
use App\Event\UserUpdatePreEvent;
|
||||
use App\Repository\UserRepository;
|
||||
use App\Validator\ValidationFailedException;
|
||||
use InvalidArgumentException;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
|
||||
/**
|
||||
* @final
|
||||
*/
|
||||
class UserService
|
||||
{
|
||||
private $repository;
|
||||
private $dispatcher;
|
||||
private $validator;
|
||||
private $configuration;
|
||||
private $encoderFactory;
|
||||
|
||||
public function __construct(UserRepository $repository, EventDispatcherInterface $dispatcher, ValidatorInterface $validator, SystemConfiguration $configuration, UserPasswordEncoderInterface $encoderFactory)
|
||||
{
|
||||
$this->repository = $repository;
|
||||
$this->dispatcher = $dispatcher;
|
||||
$this->validator = $validator;
|
||||
$this->configuration = $configuration;
|
||||
$this->encoderFactory = $encoderFactory;
|
||||
}
|
||||
|
||||
public function createNewUser(): User
|
||||
{
|
||||
$user = new User();
|
||||
$user->setEnabled(true);
|
||||
$user->setTimezone($this->configuration->getUserDefaultTimezone());
|
||||
$user->setLanguage($this->configuration->getUserDefaultLanguage());
|
||||
$user->setPreferenceValue(UserPreference::SKIN, $this->configuration->getUserDefaultTheme());
|
||||
|
||||
// Attention: PrepareUserEvent cannot be dispatched on console, as it calls isGranted()
|
||||
$this->dispatcher->dispatch(new UserCreateEvent($user));
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
public function saveNewUser(User $user): User
|
||||
{
|
||||
if (null !== $user->getId()) {
|
||||
throw new InvalidArgumentException('Cannot create user, already persisted');
|
||||
}
|
||||
|
||||
$this->validateUser($user, ['Registration', 'UserCreate']);
|
||||
|
||||
$this->hashPassword($user);
|
||||
$this->hashApiToken($user);
|
||||
|
||||
$this->dispatcher->dispatch(new UserCreatePreEvent($user));
|
||||
$this->repository->saveUser($user);
|
||||
$this->dispatcher->dispatch(new UserCreatePostEvent($user));
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @param string[] $groups
|
||||
* @throws ValidationFailedException
|
||||
*/
|
||||
private function validateUser(User $user, array $groups = []): void
|
||||
{
|
||||
$errors = $this->validator->validate($user, null, $groups);
|
||||
|
||||
if ($errors->count() > 0) {
|
||||
throw new ValidationFailedException($errors, 'Validation Failed');
|
||||
}
|
||||
}
|
||||
|
||||
public function updateUser(User $user, array $groups = []): User
|
||||
{
|
||||
$this->validateUser($user, $groups);
|
||||
|
||||
$this->hashPassword($user);
|
||||
$this->hashApiToken($user);
|
||||
|
||||
$this->dispatcher->dispatch(new UserUpdatePreEvent($user));
|
||||
$this->repository->saveUser($user);
|
||||
$this->dispatcher->dispatch(new UserUpdatePostEvent($user));
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
public function findUserByUsernameOrThrowException(string $username): User
|
||||
{
|
||||
$user = $this->findUserByName($username);
|
||||
|
||||
if ($user === null) {
|
||||
throw new \InvalidArgumentException(sprintf('User identified by "%s" username does not exist.', $username));
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
public function findUserByUsernameOrEmail(string $usernameOrEmail): ?User
|
||||
{
|
||||
return $this->repository->loadUserByUsername($usernameOrEmail);
|
||||
}
|
||||
|
||||
public function findUserByEmail(string $email): ?User
|
||||
{
|
||||
return $this->repository->findOneBy(['email' => $email]);
|
||||
}
|
||||
|
||||
public function findUserByName(string $name): ?User
|
||||
{
|
||||
return $this->repository->findOneBy(['username' => $name]);
|
||||
}
|
||||
|
||||
public function findUserByConfirmationToken(string $token): ?User
|
||||
{
|
||||
return $this->repository->findOneBy(['confirmationToken' => $token]);
|
||||
}
|
||||
|
||||
public function generateSecurityToken(): string
|
||||
{
|
||||
return rtrim(strtr(base64_encode(random_bytes(32)), '+/', '-_'), '=');
|
||||
}
|
||||
|
||||
private function hashPassword(User $user)
|
||||
{
|
||||
$plain = $user->getPlainPassword();
|
||||
|
||||
if ($plain === null || 0 === \strlen($plain)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$password = $this->encoderFactory->encodePassword($user, $plain);
|
||||
$user->setPassword($password);
|
||||
$user->eraseCredentials();
|
||||
}
|
||||
|
||||
private function hashApiToken(User $user)
|
||||
{
|
||||
$plain = $user->getPlainApiToken();
|
||||
|
||||
if ($plain === null || 0 === \strlen($plain)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$password = $this->encoderFactory->encodePassword($user, $plain);
|
||||
$user->setApiToken($password);
|
||||
$user->eraseCredentials();
|
||||
}
|
||||
}
|
||||
70
src/Utils/CommandStyle.php
Normal file
70
src/Utils/CommandStyle.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?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\Utils;
|
||||
|
||||
use App\Validator\ValidationFailedException;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
final class CommandStyle
|
||||
{
|
||||
private $input;
|
||||
private $output;
|
||||
private $style;
|
||||
|
||||
public function __construct(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$this->input = $input;
|
||||
$this->output = $output;
|
||||
}
|
||||
|
||||
private function getStyle(): SymfonyStyle
|
||||
{
|
||||
if ($this->style === null) {
|
||||
$this->style = new SymfonyStyle($this->input, $this->output);
|
||||
}
|
||||
|
||||
return $this->style;
|
||||
}
|
||||
|
||||
public function success($message): void
|
||||
{
|
||||
$this->getStyle()->success($message);
|
||||
}
|
||||
|
||||
public function error($message): void
|
||||
{
|
||||
$this->getStyle()->error($message);
|
||||
}
|
||||
|
||||
public function warning($message): void
|
||||
{
|
||||
$this->getStyle()->warning($message);
|
||||
}
|
||||
|
||||
public function validationError(ValidationFailedException $exception): void
|
||||
{
|
||||
$errors = $exception->getViolations();
|
||||
if ($errors->count() > 0) {
|
||||
$style = $this->getStyle();
|
||||
/** @var \Symfony\Component\Validator\ConstraintViolation $error */
|
||||
foreach ($errors as $error) {
|
||||
$value = $error->getInvalidValue();
|
||||
$style->error(
|
||||
$error->getPropertyPath()
|
||||
. ' (' . (\is_array($value) ? implode(',', $value) : $value) . ')'
|
||||
. "\n "
|
||||
. $error->getMessage()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
35
src/Validator/Constraints/User.php
Normal file
35
src/Validator/Constraints/User.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?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\Validator\Constraints;
|
||||
|
||||
use Doctrine\Common\Annotations\Annotation\Target;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
|
||||
/**
|
||||
* @Annotation
|
||||
* @Target({"CLASS", "PROPERTY", "METHOD", "ANNOTATION"})
|
||||
*/
|
||||
class User extends Constraint
|
||||
{
|
||||
public const USER_EXISTING_EMAIL = 'kimai-user-00';
|
||||
public const USER_EXISTING_NAME = 'kimai-user-01';
|
||||
|
||||
protected static $errorNames = [
|
||||
self::USER_EXISTING_EMAIL => 'The email is already used.',
|
||||
self::USER_EXISTING_NAME => 'The username is already used.',
|
||||
];
|
||||
|
||||
public $message = 'The user has invalid settings.';
|
||||
|
||||
public function getTargets()
|
||||
{
|
||||
return self::CLASS_CONSTRAINT;
|
||||
}
|
||||
}
|
||||
71
src/Validator/Constraints/UserValidator.php
Normal file
71
src/Validator/Constraints/UserValidator.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?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\Validator\Constraints;
|
||||
|
||||
use App\Entity\User as UserEntity;
|
||||
use App\User\UserService;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\ConstraintValidator;
|
||||
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||
|
||||
class UserValidator extends ConstraintValidator
|
||||
{
|
||||
private $userService;
|
||||
|
||||
public function __construct(UserService $userService)
|
||||
{
|
||||
$this->userService = $userService;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param UserEntity $value
|
||||
* @param Constraint $constraint
|
||||
*/
|
||||
public function validate($value, Constraint $constraint)
|
||||
{
|
||||
if (!($constraint instanceof User)) {
|
||||
throw new UnexpectedTypeException($constraint, User::class);
|
||||
}
|
||||
|
||||
if (!\is_object($value) || !($value instanceof UserEntity)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->validateUser($value, $this->context);
|
||||
}
|
||||
|
||||
protected function validateUser(UserEntity $user, ExecutionContextInterface $context)
|
||||
{
|
||||
if ($user->getEmail() !== null) {
|
||||
$existingByEmail = $this->userService->findUserByEmail($user->getEmail());
|
||||
|
||||
if (null !== $existingByEmail && $user->getId() !== $existingByEmail->getId()) {
|
||||
$context->buildViolation(User::getErrorName(User::USER_EXISTING_EMAIL))
|
||||
->atPath('email')
|
||||
->setTranslationDomain('validators')
|
||||
->setCode(User::USER_EXISTING_EMAIL)
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
|
||||
if ($user->getUsername() !== null) {
|
||||
$existingByName = $this->userService->findUserByName($user->getUsername());
|
||||
|
||||
if (null !== $existingByName && $user->getId() !== $existingByName->getId()) {
|
||||
$context->buildViolation(User::getErrorName(User::USER_EXISTING_NAME))
|
||||
->atPath('username')
|
||||
->setTranslationDomain('validators')
|
||||
->setCode(User::USER_EXISTING_NAME)
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -147,9 +147,6 @@
|
||||
"handcraftedinthealps/rest-routing-bundle": {
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"hslavich/oneloginsaml-bundle": {
|
||||
"version": "v1.4.1"
|
||||
},
|
||||
"illuminate/cache": {
|
||||
"version": "v6.0.4"
|
||||
},
|
||||
@@ -186,9 +183,6 @@
|
||||
"kevinpapst/adminlte-bundle": {
|
||||
"version": "0.3"
|
||||
},
|
||||
"kimai/user-bundle": {
|
||||
"version": "dev-master"
|
||||
},
|
||||
"laminas/laminas-code": {
|
||||
"version": "3.4.1"
|
||||
},
|
||||
@@ -636,6 +630,9 @@
|
||||
"symfony/polyfill-php80": {
|
||||
"version": "v1.17.1"
|
||||
},
|
||||
"symfony/polyfill-php81": {
|
||||
"version": "v1.23.0"
|
||||
},
|
||||
"symfony/polyfill-uuid": {
|
||||
"version": "v1.22.0"
|
||||
},
|
||||
|
||||
@@ -16,17 +16,12 @@
|
||||
|
||||
{% block page_content_start %}
|
||||
{% if app.session and app.session.started and app.session.flashbag.peekAll|length > 0 %}
|
||||
{% set close = adminlte_close_alert|default(true) %}
|
||||
{% set domain = 'flashmessages' %}
|
||||
<script type="text/javascript">
|
||||
document.addEventListener('kimai.initialized', function(options) {
|
||||
var ALERT = options.detail.kimai.getPlugin('alert');
|
||||
{% for type, messages in app.session.flashbag.all %}
|
||||
{% for message in messages %}
|
||||
{% if type == 'fos_user_success' %}
|
||||
{% set type = 'success' %}
|
||||
{% set domain = 'FOSUserBundle' %}
|
||||
{% endif %}
|
||||
{% if type == 'error' %}
|
||||
ALERT.error('{{ message|trans({}, domain) }}');
|
||||
{% elseif type == 'warning' %}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
{% extends '@AdminLTE/FOSUserBundle/Registration/confirmed.html.twig' %}
|
||||
|
||||
{% block logo_login %}{% include 'partials/logo_login.html.twig' %}{% endblock %}
|
||||
{% block title %}{{- get_title() -}}{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
{{ parent() }}
|
||||
{% include 'partials/head.html.twig' %}
|
||||
{% set event = trigger(constant('App\\Event\\ThemeEvent::HTML_HEAD')) %}
|
||||
{{ event.content|raw }}
|
||||
{% endblock %}
|
||||
|
||||
{% block stylesheets %}
|
||||
{{ encore_entry_link_tags('app') }}
|
||||
{% set event = trigger(constant('App\\Event\\ThemeEvent::STYLESHEET')) %}
|
||||
{{ event.content|raw }}
|
||||
{% endblock %}
|
||||
|
||||
{% block javascripts %}
|
||||
{{ encore_entry_script_tags('app') }}
|
||||
{% set event = trigger(constant('App\\Event\\ThemeEvent::JAVASCRIPT')) %}
|
||||
{{ event.content|raw }}
|
||||
{% endblock %}
|
||||
@@ -1,23 +0,0 @@
|
||||
{% extends '@AdminLTE/FOSUserBundle/Resetting/request.html.twig' %}
|
||||
|
||||
{% block logo_login %}{% include 'partials/logo_login.html.twig' %}{% endblock %}
|
||||
{% block title %}{{- get_title() -}}{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
{{ parent() }}
|
||||
{% include 'partials/head.html.twig' %}
|
||||
{% set event = trigger(constant('App\\Event\\ThemeEvent::HTML_HEAD')) %}
|
||||
{{ event.content|raw }}
|
||||
{% endblock %}
|
||||
|
||||
{% block stylesheets %}
|
||||
{{ encore_entry_link_tags('app') }}
|
||||
{% set event = trigger(constant('App\\Event\\ThemeEvent::STYLESHEET')) %}
|
||||
{{ event.content|raw }}
|
||||
{% endblock %}
|
||||
|
||||
{% block javascripts %}
|
||||
{{ encore_entry_script_tags('app') }}
|
||||
{% set event = trigger(constant('App\\Event\\ThemeEvent::JAVASCRIPT')) %}
|
||||
{{ event.content|raw }}
|
||||
{% endblock %}
|
||||
@@ -1,40 +0,0 @@
|
||||
{% extends '@AdminLTE/FOSUserBundle/Resetting/request.html.twig' %}
|
||||
|
||||
{% block logo_login %}{% include 'partials/logo_login.html.twig' %}{% endblock %}
|
||||
{% block title %}{{- get_title() -}}{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
{{ parent() }}
|
||||
{% include 'partials/head.html.twig' %}
|
||||
{% set event = trigger(constant('App\\Event\\ThemeEvent::HTML_HEAD')) %}
|
||||
{{ event.content|raw }}
|
||||
{% endblock %}
|
||||
|
||||
{% block stylesheets %}
|
||||
{{ encore_entry_link_tags('app') }}
|
||||
{% set event = trigger(constant('App\\Event\\ThemeEvent::STYLESHEET')) %}
|
||||
{{ event.content|raw }}
|
||||
{% endblock %}
|
||||
|
||||
{% block javascripts %}
|
||||
{{ encore_entry_script_tags('app') }}
|
||||
{% set event = trigger(constant('App\\Event\\ThemeEvent::JAVASCRIPT')) %}
|
||||
{{ event.content|raw }}
|
||||
{% endblock %}
|
||||
|
||||
{% block login_form %}
|
||||
|
||||
{% trans_default_domain 'FOSUserBundle' %}
|
||||
|
||||
{{ form_start(form, { 'action': path('fos_user_resetting_reset', {'token': token}), 'attr': { 'class': 'fos_user_resetting_reset' } }) }}
|
||||
{{ form_widget(form) }}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<button type="submit" class="btn btn-primary btn-block btn-flat">{{ 'resetting.request.submit'|trans }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ form_end(form) }}
|
||||
|
||||
{% endblock %}
|
||||
@@ -1,32 +0,0 @@
|
||||
{% extends '@AdminLTE/FOSUserBundle/Security/login.html.twig' %}
|
||||
|
||||
{% block logo_login %}{% include 'partials/logo_login.html.twig' %}{% endblock %}
|
||||
{% block title %}{{- get_title() -}}{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
{{ parent() }}
|
||||
{% include 'partials/head.html.twig' %}
|
||||
{% set event = trigger(constant('App\\Event\\ThemeEvent::HTML_HEAD')) %}
|
||||
{{ event.content|raw }}
|
||||
{% endblock %}
|
||||
|
||||
{% block stylesheets %}
|
||||
{{ encore_entry_link_tags('app') }}
|
||||
{% set event = trigger(constant('App\\Event\\ThemeEvent::STYLESHEET')) %}
|
||||
{{ event.content|raw }}
|
||||
{% endblock %}
|
||||
|
||||
{% block javascripts %}
|
||||
{{ encore_entry_script_tags('app') }}
|
||||
{% set event = trigger(constant('App\\Event\\ThemeEvent::JAVASCRIPT')) %}
|
||||
{{ event.content|raw }}
|
||||
{% endblock %}
|
||||
|
||||
{% block login_social_auth %}
|
||||
{% if saml.activate %}
|
||||
<a href="{{ path('saml_login') }}" class="btn btn-block btn-google">
|
||||
{{ saml.title|trans }}
|
||||
</a>
|
||||
<br>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
57
templates/security/login.html.twig
Normal file
57
templates/security/login.html.twig
Normal file
@@ -0,0 +1,57 @@
|
||||
{% extends '@AdminLTE/FOSUserBundle/Security/login.html.twig' %}
|
||||
|
||||
{% block logo_login %}{% include 'partials/logo_login.html.twig' %}{% endblock %}
|
||||
{% block title %}{{- get_title() -}}{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
{{ parent() }}
|
||||
{% include 'partials/head.html.twig' %}
|
||||
{% set event = trigger(constant('App\\Event\\ThemeEvent::HTML_HEAD')) %}
|
||||
{{ event.content|raw }}
|
||||
{% endblock %}
|
||||
|
||||
{% block stylesheets %}
|
||||
{{ encore_entry_link_tags('app') }}
|
||||
{% set event = trigger(constant('App\\Event\\ThemeEvent::STYLESHEET')) %}
|
||||
{{ event.content|raw }}
|
||||
{% endblock %}
|
||||
|
||||
{% block javascripts %}
|
||||
{{ encore_entry_script_tags('app') }}
|
||||
{% set event = trigger(constant('App\\Event\\ThemeEvent::JAVASCRIPT')) %}
|
||||
{{ event.content|raw }}
|
||||
{% endblock %}
|
||||
|
||||
{% block login_social_auth %}
|
||||
{% if kimai_config.samlActive %}
|
||||
{% set class = 'btn-primary' %}
|
||||
{% if kimai_config.loginFormActive %}
|
||||
{% set class = 'btn-google' %}
|
||||
<hr>
|
||||
{% endif %}
|
||||
<a href="{{ path('saml_login') }}" class="btn btn-block {{ class }}">
|
||||
{{ kimai_config.samlTitle|trans }}
|
||||
</a>
|
||||
<br>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block login_form %}
|
||||
{% if kimai_config.loginFormActive %}
|
||||
{{ parent() }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block login_actions %}
|
||||
{% if kimai_config.passwordResetActive %}
|
||||
<a href="{{ path('fos_user_resetting_request') }}">
|
||||
{{ 'I forgot my password'|trans({}, 'AdminLTEBundle') }}
|
||||
</a>
|
||||
<br>
|
||||
{% endif %}
|
||||
{% if kimai_config.selfRegistrationActive %}
|
||||
<a href="{{ path('fos_user_registration_register') }}">
|
||||
{{ 'Register a new account'|trans({}, 'AdminLTEBundle') }}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
11
templates/security/password-reset/check_email.html.twig
Normal file
11
templates/security/password-reset/check_email.html.twig
Normal file
@@ -0,0 +1,11 @@
|
||||
{% extends 'security/password-reset/layout.html.twig' %}
|
||||
|
||||
{% block login_form %}
|
||||
|
||||
<p>
|
||||
{{ 'resetting.check_email'|trans({'%tokenLifetime%': tokenLifetime})|nl2br }}
|
||||
</p>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% extends '@AdminLTE/FOSUserBundle/layout.html.twig' %}
|
||||
{% extends '@AdminLTE/FOSUserBundle/Resetting/request.html.twig' %}
|
||||
|
||||
{% block logo_login %}{% include 'partials/logo_login.html.twig' %}{% endblock %}
|
||||
{% block title %}{{- get_title() -}}{% endblock %}
|
||||
@@ -20,4 +20,4 @@
|
||||
{{ encore_entry_script_tags('app') }}
|
||||
{% set event = trigger(constant('App\\Event\\ThemeEvent::JAVASCRIPT')) %}
|
||||
{{ event.content|raw }}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
1
templates/security/password-reset/request.html.twig
Normal file
1
templates/security/password-reset/request.html.twig
Normal file
@@ -0,0 +1 @@
|
||||
{% extends 'security/password-reset/layout.html.twig' %}
|
||||
16
templates/security/password-reset/reset.html.twig
Normal file
16
templates/security/password-reset/reset.html.twig
Normal file
@@ -0,0 +1,16 @@
|
||||
{% extends 'security/password-reset/layout.html.twig' %}
|
||||
|
||||
{% block login_form %}
|
||||
|
||||
{{ form_start(form, { 'action': path('fos_user_resetting_reset', {'token': token}), 'attr': { 'class': 'fos_user_resetting_reset' } }) }}
|
||||
{{ form_widget(form) }}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<button type="submit" class="btn btn-primary btn-block btn-flat">{{ 'Reset your password'|trans({}, 'AdminLTEBundle') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ form_end(form) }}
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,9 @@
|
||||
{% extends 'security/self-registration/layout.html.twig' %}
|
||||
|
||||
{% block login_form %}
|
||||
|
||||
<p>
|
||||
{{ 'registration.check_email'|trans({'%email%': user.email}) }}
|
||||
</p>
|
||||
|
||||
{% endblock %}
|
||||
16
templates/security/self-registration/confirmed.html.twig
Normal file
16
templates/security/self-registration/confirmed.html.twig
Normal file
@@ -0,0 +1,16 @@
|
||||
{% extends 'security/self-registration/layout.html.twig' %}
|
||||
|
||||
{% block login_form %}
|
||||
|
||||
<p>
|
||||
{{ 'registration.confirmed'|trans({'%username%': user.username}, 'AdminLTEBundle') }}
|
||||
</p>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block login_actions %}
|
||||
<br>
|
||||
<a href="{{ path('homepage') }}">
|
||||
{{ 'Show homepage'|trans({}, 'AdminLTEBundle') }}
|
||||
</a>
|
||||
{% endblock %}
|
||||
1
templates/security/self-registration/register.html.twig
Normal file
1
templates/security/self-registration/register.html.twig
Normal file
@@ -0,0 +1 @@
|
||||
{% extends 'security/self-registration/layout.html.twig' %}
|
||||
102
tests/Command/ActivateUserCommandTest.php
Normal file
102
tests/Command/ActivateUserCommandTest.php
Normal file
@@ -0,0 +1,102 @@
|
||||
<?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\Tests\Command;
|
||||
|
||||
use App\Command\ActivateUserCommand;
|
||||
use App\Entity\User;
|
||||
use App\Repository\UserRepository;
|
||||
use App\User\UserService;
|
||||
use Symfony\Bundle\FrameworkBundle\Console\Application;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
use Symfony\Component\Console\Exception\RuntimeException;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
/**
|
||||
* @covers \App\Command\ActivateUserCommand
|
||||
* @group integration
|
||||
*/
|
||||
class ActivateUserCommandTest extends KernelTestCase
|
||||
{
|
||||
/**
|
||||
* @var Application
|
||||
*/
|
||||
private $application;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$kernel = self::bootKernel();
|
||||
$this->application = new Application($kernel);
|
||||
$container = self::$kernel->getContainer();
|
||||
|
||||
$userService = $container->get(UserService::class);
|
||||
|
||||
$this->application->add(new ActivateUserCommand($userService));
|
||||
}
|
||||
|
||||
public function testCommandName()
|
||||
{
|
||||
$application = $this->application;
|
||||
|
||||
$command = $application->find('kimai:user:activate');
|
||||
self::assertInstanceOf(ActivateUserCommand::class, $command);
|
||||
|
||||
// test alias
|
||||
$command = $application->find('fos:user:activate');
|
||||
self::assertInstanceOf(ActivateUserCommand::class, $command);
|
||||
}
|
||||
|
||||
protected function callCommand(?string $username)
|
||||
{
|
||||
$command = $this->application->find('kimai:user:activate');
|
||||
$input = [
|
||||
'command' => $command->getName(),
|
||||
];
|
||||
|
||||
if ($username !== null) {
|
||||
$input['username'] = $username;
|
||||
}
|
||||
|
||||
$commandTester = new CommandTester($command);
|
||||
$commandTester->execute($input);
|
||||
|
||||
return $commandTester;
|
||||
}
|
||||
|
||||
public function testActivate()
|
||||
{
|
||||
$commandTester = $this->callCommand('chris_user');
|
||||
|
||||
$output = $commandTester->getDisplay();
|
||||
$this->assertStringContainsString('[OK] User "chris_user" has been activated.', $output);
|
||||
|
||||
$container = self::$kernel->getContainer();
|
||||
/** @var UserRepository $userRepository */
|
||||
$userRepository = $container->get('doctrine')->getRepository(User::class);
|
||||
$user = $userRepository->loadUserByUsername('chris_user');
|
||||
self::assertInstanceOf(User::class, $user);
|
||||
self::assertTrue($user->isEnabled());
|
||||
}
|
||||
|
||||
public function testActivateOnActiveUser()
|
||||
{
|
||||
$commandTester = $this->callCommand('susan_super');
|
||||
|
||||
$output = $commandTester->getDisplay();
|
||||
$this->assertStringContainsString('[WARNING] User "susan_super" is already active.', $output);
|
||||
}
|
||||
|
||||
public function testWithMissingUsername()
|
||||
{
|
||||
$this->expectException(RuntimeException::class);
|
||||
$this->expectExceptionMessage('Not enough arguments (missing: "username").');
|
||||
|
||||
$this->callCommand(null);
|
||||
}
|
||||
}
|
||||
109
tests/Command/ChangePasswordCommandTest.php
Normal file
109
tests/Command/ChangePasswordCommandTest.php
Normal file
@@ -0,0 +1,109 @@
|
||||
<?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\Tests\Command;
|
||||
|
||||
use App\Command\ChangePasswordCommand;
|
||||
use App\Entity\User;
|
||||
use App\Repository\UserRepository;
|
||||
use App\User\UserService;
|
||||
use Symfony\Bundle\FrameworkBundle\Console\Application;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
use Symfony\Component\Console\Exception\RuntimeException;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
/**
|
||||
* @covers \App\Command\ChangePasswordCommand
|
||||
* @group integration
|
||||
*/
|
||||
class ChangePasswordCommandTest extends KernelTestCase
|
||||
{
|
||||
/**
|
||||
* @var Application
|
||||
*/
|
||||
private $application;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$kernel = self::bootKernel();
|
||||
$this->application = new Application($kernel);
|
||||
$container = self::$kernel->getContainer();
|
||||
|
||||
$userService = $container->get(UserService::class);
|
||||
|
||||
$this->application->add(new ChangePasswordCommand($userService));
|
||||
}
|
||||
|
||||
public function testCommandName()
|
||||
{
|
||||
$application = $this->application;
|
||||
|
||||
$command = $application->find('kimai:user:password');
|
||||
self::assertInstanceOf(ChangePasswordCommand::class, $command);
|
||||
|
||||
// test alias
|
||||
$command = $application->find('fos:user:change-password');
|
||||
self::assertInstanceOf(ChangePasswordCommand::class, $command);
|
||||
}
|
||||
|
||||
protected function callCommand(?string $username, ?string $password)
|
||||
{
|
||||
$command = $this->application->find('kimai:user:password');
|
||||
$input = [
|
||||
'command' => $command->getName(),
|
||||
];
|
||||
|
||||
if ($username !== null) {
|
||||
$input['username'] = $username;
|
||||
}
|
||||
|
||||
if ($password !== null) {
|
||||
$input['password'] = $password;
|
||||
}
|
||||
|
||||
$commandTester = new CommandTester($command);
|
||||
$commandTester->execute($input);
|
||||
|
||||
return $commandTester;
|
||||
}
|
||||
|
||||
public function testChangePassword()
|
||||
{
|
||||
$commandTester = $this->callCommand('john_user', '0987654321');
|
||||
|
||||
$output = $commandTester->getDisplay();
|
||||
$this->assertStringContainsString('[OK] Changed password for user "john_user".', $output);
|
||||
|
||||
$container = self::$kernel->getContainer();
|
||||
/** @var UserRepository $userRepository */
|
||||
$userRepository = $container->get('doctrine')->getRepository(User::class);
|
||||
$user = $userRepository->loadUserByUsername('john_user');
|
||||
self::assertInstanceOf(User::class, $user);
|
||||
|
||||
$container = self::$kernel->getContainer();
|
||||
$encoderService = $container->get('security.password_encoder');
|
||||
self::assertTrue($encoderService->isPasswordValid($user, '0987654321'));
|
||||
}
|
||||
|
||||
public function testWithMissingUsername()
|
||||
{
|
||||
$this->expectException(RuntimeException::class);
|
||||
$this->expectExceptionMessage('Not enough arguments (missing: "username").');
|
||||
|
||||
$this->callCommand(null, '1234567890');
|
||||
}
|
||||
|
||||
public function testWithMissingPassword()
|
||||
{
|
||||
$this->expectException(RuntimeException::class);
|
||||
$this->expectExceptionMessage('Not enough arguments (missing: "password").');
|
||||
|
||||
$this->callCommand('1234567890', null);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user