Secure create-user command #123 (#127)

* ask the user for the password #123
* added integration test #123
This commit is contained in:
Simon Schaufelberger
2018-02-10 19:11:30 +01:00
committed by Kevin Papst
parent 5527f29caf
commit 93e1cd5095
10 changed files with 506 additions and 303 deletions

2
.gitignore vendored
View File

@@ -7,7 +7,7 @@
!bin/console !bin/console
/var/data/kimai.sqlite /var/data/kimai.sqlite
/var/coverage/
/var/cache/* /var/cache/*
!var/cache/.gitkeep !var/cache/.gitkeep

View File

@@ -60,7 +60,7 @@ npm run prod
### Installation (development / demo) ### Installation (development / demo)
Lets boostrap your environment by executing this commands (which is only available in dev environment): Lets boostrap your environment by executing this commands (which is only available in dev environment):
```bash ```bash
bin/console kimai:reset-dev bin/console kimai:reset-dev
``` ```
@@ -95,13 +95,13 @@ Just use the built-in web server for your first tests:
bin/console server:run bin/console server:run
``` ```
This command will start a web server for Kimai. Now you can access the application in your browser at <http://127.0.0.1:8000/>. This command will start a web server for Kimai. Now you can access the application in your browser at <http://127.0.0.1:8000/>.
You can stop the built-in web server by pressing `Ctrl + C` while you're in the terminal. You can stop the built-in web server by pressing `Ctrl + C` while you're in the terminal.
### Installation (live) ### Installation (live)
The database to use is up to you, but we would not recommend using the default SQLite database for production usage. The database to use is up to you, but we would not recommend using the default SQLite database for production usage.
Please create your database and configure the connection string in your environment, e.g. with the `.env` file: Please create your database and configure the connection string in your environment, e.g. with the `.env` file:
``` ```
APP_ENV=prod APP_ENV=prod
@@ -115,10 +115,10 @@ bin/console doctrine:schema:create
bin/console cache:warmup --env=prod bin/console cache:warmup --env=prod
``` ```
Create your first user: Create your first user with the following command. You will be asked to enter a password afterwards.
```bash ```bash
bin/console kimai:create-user username password admin@example.com ROLE_SUPER_ADMIN bin/console kimai:create-user username admin@example.com ROLE_SUPER_ADMIN
``` ```
For available roles, please refer to the [user documentation](var/docs/users.md). For available roles, please refer to the [user documentation](var/docs/users.md).

View File

@@ -8,7 +8,6 @@
"ext-pdo_sqlite": "*", "ext-pdo_sqlite": "*",
"avanzu/admin-theme-bundle": "dev-kevinpapst", "avanzu/admin-theme-bundle": "dev-kevinpapst",
"beberlei/DoctrineExtensions": "^1.0", "beberlei/DoctrineExtensions": "^1.0",
"dama/doctrine-test-bundle": "^4.0",
"erusev/parsedown": "^1.6", "erusev/parsedown": "^1.6",
"sensio/framework-extra-bundle": "^5.1", "sensio/framework-extra-bundle": "^5.1",
"symfony/asset": "^4.0", "symfony/asset": "^4.0",
@@ -34,6 +33,7 @@
"white-october/pagerfanta-bundle": "^1.1" "white-october/pagerfanta-bundle": "^1.1"
}, },
"require-dev": { "require-dev": {
"dama/doctrine-test-bundle": "^5.0",
"doctrine/doctrine-fixtures-bundle": "^3.0", "doctrine/doctrine-fixtures-bundle": "^3.0",
"friendsofphp/php-cs-fixer": "^2.10", "friendsofphp/php-cs-fixer": "^2.10",
"phpunit/phpunit": "^6.5", "phpunit/phpunit": "^6.5",

637
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -17,4 +17,5 @@ return [
WhiteOctober\PagerfantaBundle\WhiteOctoberPagerfantaBundle::class => ['all' => true], WhiteOctober\PagerfantaBundle\WhiteOctoberPagerfantaBundle::class => ['all' => true],
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true, 'test' => true], Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true, 'test' => true],
DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true],
]; ];

View File

@@ -13,6 +13,7 @@
<env name="APP_ENV" value="test"/> <env name="APP_ENV" value="test"/>
<env name="APP_DEBUG" value="1"/> <env name="APP_DEBUG" value="1"/>
<env name="APP_SECRET" value="5a79a1c866efef9ca1800f971d689f3e"/> <env name="APP_SECRET" value="5a79a1c866efef9ca1800f971d689f3e"/>
<env name="DATABASE_PREFIX" value="kimai2_"/>
<!-- define your env variables for the test env here --> <!-- define your env variables for the test env here -->
<!-- ###+ doctrine/doctrine-bundle ### --> <!-- ###+ doctrine/doctrine-bundle ### -->
@@ -25,14 +26,26 @@
</php> </php>
<testsuites> <testsuites>
<testsuite name="Project Test Suite"> <testsuite name="Kimai">
<directory>tests/</directory> <directory>tests/</directory>
</testsuite> </testsuite>
</testsuites> </testsuites>
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">src/</directory>
<directory suffix=".php">templates/</directory>
<exclude>
<directory suffix=".php">vendor/</directory>
<directory suffix=".php">tests/</directory>
</exclude>
</whitelist>
</filter>
<listeners> <listeners>
<!-- it begins a database transaction before every testcase and rolls it back after <!-- it begins a database transaction before every testcase and rolls it back after
the test finished, so tests can manipulate the database without affecting other tests --> the test finished, so tests can manipulate the database without affecting other tests -->
<listener class="\DAMA\DoctrineTestBundle\PHPUnit\PHPUnitListener" /> <listener class="DAMA\DoctrineTestBundle\PHPUnit\PHPUnitListener" />
<listener class="Symfony\Bridge\PhpUnit\SymfonyTestsListener" />
</listeners> </listeners>
</phpunit> </phpunit>

View File

@@ -13,9 +13,11 @@ use App\Entity\User;
use Doctrine\Bundle\DoctrineBundle\Registry; use Doctrine\Bundle\DoctrineBundle\Registry;
use Symfony\Bridge\Doctrine\RegistryInterface; use Symfony\Bridge\Doctrine\RegistryInterface;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\Question;
use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoder; use Symfony\Component\Security\Core\Encoder\UserPasswordEncoder;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface; use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
@@ -65,10 +67,10 @@ class CreateUserCommand extends Command
->setName('kimai:create-user') ->setName('kimai:create-user')
->setDescription('Create a new user') ->setDescription('Create a new user')
->setHelp('This command allows you to create a new user.') ->setHelp('This command allows you to create a new user.')
->addArgument('username', InputArgument::REQUIRED, 'New username (must be unique)') ->addArgument('username', InputArgument::REQUIRED, 'The username of the user to be created (must be unique)')
->addArgument('password', InputArgument::REQUIRED, 'Users password') ->addArgument('email', InputArgument::REQUIRED, 'Email address of the user to be created (must be unique)')
->addArgument('email', InputArgument::REQUIRED, 'Users email address (must be unique)') ->addArgument('role', InputArgument::OPTIONAL, 'A comma separated list of roles to assign. Examples: "ROLE_USER,ROLE_SUPER_ADMIN"', User::DEFAULT_ROLE)
->addArgument('role', InputArgument::OPTIONAL, 'Users role (comma separated list)', User::DEFAULT_ROLE) ->addArgument('password', InputArgument::OPTIONAL, 'Password for the user to be created')
; ;
} }
@@ -81,9 +83,14 @@ class CreateUserCommand extends Command
$username = $input->getArgument('username'); $username = $input->getArgument('username');
$email = $input->getArgument('email'); $email = $input->getArgument('email');
$password = $input->getArgument('password');
$role = $input->getArgument('role'); $role = $input->getArgument('role');
if ($input->getArgument('password') !== null) {
$password = $input->getArgument('password');
} else {
$password = $this->askForPassword($input, $output);
}
$role = $role ?: User::DEFAULT_ROLE; $role = $role ?: User::DEFAULT_ROLE;
$user = new User(); $user = new User();
@@ -103,7 +110,7 @@ class CreateUserCommand extends Command
$value = $error->getInvalidValue(); $value = $error->getInvalidValue();
$io->error( $io->error(
$error->getPropertyPath() $error->getPropertyPath()
. " (" . (is_array($value) ? implode(',', $value) : $value) .")" . ' (' . (is_array($value) ? implode(',', $value) : $value) . ')'
. "\n " . "\n "
. $error->getMessage() . $error->getMessage()
); );
@@ -121,4 +128,31 @@ class CreateUserCommand extends Command
$io->error('Reason: ' . $ex->getMessage()); $io->error('Reason: ' . $ex->getMessage());
} }
} }
/**
* @param InputInterface $input
* @param OutputInterface $output
*
* @return string
*/
protected function askForPassword(InputInterface $input, OutputInterface $output): string
{
/* @var QuestionHelper $helper */
$helper = $this->getHelper('question');
$passwordQuestion = new Question('Please enter the password');
$passwordQuestion->setHidden(true);
$passwordQuestion->setHiddenFallback(false);
$passwordQuestion->setValidator(function (?string $value) {
$password = trim($value);
if (empty($password) || strlen($password) < 6) {
throw new \Exception('The password is too short, must be at least 6 character');
}
return $value;
});
$passwordQuestion->setMaxAttempts(3);
return $helper->ask($input, $output, $passwordQuestion);
}
} }

View File

@@ -416,6 +416,9 @@
"ref": "85834af1496735f28d831489d12ab1921a875e0d" "ref": "85834af1496735f28d831489d12ab1921a875e0d"
} }
}, },
"symfony/security-csrf": {
"version": "v4.0.4"
},
"symfony/stopwatch": { "symfony/stopwatch": {
"version": "v4.0.3" "version": "v4.0.3"
}, },

View File

@@ -0,0 +1,86 @@
<?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\Twig;
use App\Command\CreateUserCommand;
use App\Entity\User;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Console\Tester\CommandTester;
use Symfony\Component\Validator\ConstraintViolationListInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;
/**
* @coversDefaultClass \App\Command\CreateUserCommand
* @group integration
*/
class CreateUserCommandTest extends KernelTestCase
{
/**
* @var Application
*/
protected $application;
protected function setUp()
{
$kernel = self::bootKernel();
$this->application = new Application($kernel);
$container = self::$kernel->getContainer();
$passwordEncoder = $container->get('security.password_encoder');
$validationResult = $this->getMockBuilder(ConstraintViolationListInterface::class)->getMock();
$validationResult->method('count')->willReturn(0);
$validator = $this->getMockBuilder(ValidatorInterface::class)->getMock();
$validator->method('validate')->willReturn($validationResult);
$this->application->add(new CreateUserCommand(
$passwordEncoder,
$container->get('doctrine'),
$validator
));
}
public function testCreateUser()
{
$commandTester = $this->createUser('MyTestUser', 'user@example.com', 'ROLE_USER', 'foobar');
$output = $commandTester->getDisplay();
$this->assertContains('[OK] Success! Created user: MyTestUser', $output);
$container = self::$kernel->getContainer();
$user = $container->get('doctrine')->getRepository(User::class)->loadUserByUsername('MyTestUser');
$this->assertNotNull($user);
}
protected function createUser($username, $email, $role, $password)
{
$command = $this->application->find('kimai:create-user');
$commandTester = new CommandTester($command);
$commandTester->execute([
'command' => $command->getName(),
'username' => $username,
'email' => $email,
'role' => $role,
'password' => $password
]);
return $commandTester;
}
public function testUserAlreadyExisting()
{
$commandTester = $this->createUser('MyTestUser', 'user@example.com', 'ROLE_USER', 'foobar');
$commandTester = $this->createUser('MyTestUser', 'user@example.com', 'ROLE_USER', 'foobar');
$output = $commandTester->getDisplay();
$this->assertContains('[ERROR] Failed to create user: MyTestUser', $output);
}
}

View File

@@ -10,6 +10,7 @@
namespace App\Tests\Controller; namespace App\Tests\Controller;
/** /**
* @coversDefaultClass \App\Controller\HelpController
* @group integration * @group integration
*/ */
class HelpControllerTest extends ControllerBaseTest class HelpControllerTest extends ControllerBaseTest
@@ -26,7 +27,7 @@ class HelpControllerTest extends ControllerBaseTest
$this->request($client, '/help/'); $this->request($client, '/help/');
$this->assertTrue($client->getResponse()->isSuccessful()); $this->assertTrue($client->getResponse()->isSuccessful());
$this->assertContains('<h1>Kimai documentation</h1>', $client->getResponse()->getContent()); $this->assertContains('<h1>Kimai documentation</h1>', $client->getResponse()->getContent());
$this->assertNotContains('<a href="/en/help/README">', $client->getResponse()->getContent()); $this->assertNotContains('<a href="/en/help/">', $client->getResponse()->getContent());
} }
public function testUsersPage() public function testUsersPage()
@@ -34,7 +35,7 @@ class HelpControllerTest extends ControllerBaseTest
$client = $this->getClientForAuthenticatedUser(); $client = $this->getClientForAuthenticatedUser();
$this->request($client, '/help/users'); $this->request($client, '/help/users');
$this->assertTrue($client->getResponse()->isSuccessful()); $this->assertTrue($client->getResponse()->isSuccessful());
$this->assertContains('<a href="/en/help/README">', $client->getResponse()->getContent()); $this->assertContains('<a href="/en/help/">', $client->getResponse()->getContent());
} }
public function testValidateRouteDoesNotAllowSpecialChars() public function testValidateRouteDoesNotAllowSpecialChars()