Skip to content

Commit

Permalink
feature #736 keeping aliens at bay with maker + new security 5.2 feat…
Browse files Browse the repository at this point in the history
…ures (jrushlow)

This PR was squashed before being merged into the 1.0-dev branch.

Discussion
----------

keeping aliens at bay with maker + new security 5.2 features

Hello Security51! Allows `make:auth` to take advantage of the new security features that were introduced. Starting in Symfony `5.2` when you run `make:auth` MakerBundle will automatically check if you have set:
```
security:
    enable_authenticator_manager: true
```
If so, MakerBundle will generate the required classes to authenticate users leveraging the new Authenticators.

A positive side effect of this PR, all templates can check `$use_typed_properties` to determine if the host is capable of utilizing typed properties that were introduced in PHP 7.4.

e.g. -

```
private <?= $use_typed_properties ? 'UrlGeneratorInterface ' : null ?>$urlGenerator;
```

Internally, we've added the `PhpCompatUtil::canUseTypedProperties()` method that is called anytime a Maker needs to generate a twig template. We then inject `$use_typed_properties` into all templates so the developer can add typed properties to a generated template without having to worry about a bunch of behind the scenes logic.

Commits
-------

820ee25 keeping aliens at bay with maker + new security 5.2 features
  • Loading branch information
weaverryan committed Dec 17, 2020
2 parents dd34e35 + 820ee25 commit fba06d8
Show file tree
Hide file tree
Showing 35 changed files with 828 additions and 53 deletions.
1 change: 1 addition & 0 deletions src/Generator.php
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ private function addOperation(string $targetPath, string $templateName, array $v

$variables['relative_path'] = $this->fileManager->relativizePath($targetPath);
$variables['use_attributes'] = $this->phpCompatUtil->canUseAttributes();
$variables['use_typed_properties'] = $this->phpCompatUtil->canUseTypedProperties();

$templatePath = $templateName;
if (!file_exists($templatePath)) {
Expand Down
50 changes: 37 additions & 13 deletions src/Maker/MakeAuthenticator.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,12 @@
use Symfony\Component\Form\Form;
use Symfony\Component\HttpKernel\Kernel;
use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Yaml\Yaml;

/**
* @author Ryan Weaver <[email protected]>
* @author Ryan Weaver <[email protected]>
* @author Jesse Rushlow <[email protected]>
*
* @internal
*/
Expand All @@ -56,6 +58,8 @@ final class MakeAuthenticator extends AbstractMaker

private $doctrineHelper;

private $useSecurity52 = false;

public function __construct(FileManager $fileManager, SecurityConfigUpdater $configUpdater, Generator $generator, DoctrineHelper $doctrineHelper)
{
$this->fileManager = $fileManager;
Expand Down Expand Up @@ -84,6 +88,15 @@ public function interact(InputInterface $input, ConsoleStyle $io, Command $comma
$manipulator = new YamlSourceManipulator($this->fileManager->getFileContents($path));
$securityData = $manipulator->getData();

// Determine if we should use new security features introduced in Symfony 5.2
if ($securityData['security']['enable_authenticator_manager'] ?? false) {
$this->useSecurity52 = true;
}

if ($this->useSecurity52 && !class_exists(UserBadge::class)) {
throw new RuntimeCommandException('MakerBundle does not support generating authenticators using the new authenticator system before symfony/security-bundle 5.2. Please upgrade to 5.2 and try again.');
}

// authenticator type
$authenticatorTypeValues = [
'Empty authenticator' => self::AUTH_TYPE_EMPTY_AUTHENTICATOR,
Expand Down Expand Up @@ -138,10 +151,13 @@ function ($answer) {
$input->setOption('firewall-name', $firewallName = $interactiveSecurityHelper->guessFirewallName($io, $securityData));

$command->addOption('entry-point', null, InputOption::VALUE_OPTIONAL);
$input->setOption(
'entry-point',
$interactiveSecurityHelper->guessEntryPoint($io, $securityData, $input->getArgument('authenticator-class'), $firewallName)
);

if (!$this->useSecurity52) {
$input->setOption(
'entry-point',
$interactiveSecurityHelper->guessEntryPoint($io, $securityData, $input->getArgument('authenticator-class'), $firewallName)
);
}

if (self::AUTH_TYPE_FORM_LOGIN === $input->getArgument('authenticator-type')) {
$command->addArgument('controller-class', InputArgument::REQUIRED);
Expand Down Expand Up @@ -192,13 +208,21 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen

// update security.yaml with guard config
$securityYamlUpdated = false;

$entryPoint = $input->getOption('entry-point');

if ($this->useSecurity52 && self::AUTH_TYPE_FORM_LOGIN !== $input->getArgument('authenticator-type')) {
$entryPoint = false;
}

try {
$newYaml = $this->configUpdater->updateForAuthenticator(
$this->fileManager->getFileContents($path = 'config/packages/security.yaml'),
$input->getOption('firewall-name'),
$input->getOption('entry-point'),
$entryPoint,
$input->getArgument('authenticator-class'),
$input->hasArgument('logout-setup') ? $input->getArgument('logout-setup') : false
$input->hasArgument('logout-setup') ? $input->getArgument('logout-setup') : false,
$this->useSecurity52
);
$generator->dumpFile($path, $newYaml);
$securityYamlUpdated = true;
Expand Down Expand Up @@ -235,10 +259,8 @@ private function generateAuthenticatorClass(array $securityData, string $authent
if (self::AUTH_TYPE_EMPTY_AUTHENTICATOR === $authenticatorType) {
$this->generator->generateClass(
$authenticatorClass,
'authenticator/EmptyAuthenticator.tpl.php',
[
'provider_key_type_hint' => $this->providerKeyTypeHint(),
]
sprintf('authenticator/%sEmptyAuthenticator.tpl.php', $this->useSecurity52 ? 'Security52' : ''),
['provider_key_type_hint' => $this->providerKeyTypeHint()]
);

return;
Expand All @@ -251,12 +273,13 @@ private function generateAuthenticatorClass(array $securityData, string $authent

$this->generator->generateClass(
$authenticatorClass,
'authenticator/LoginFormAuthenticator.tpl.php',
sprintf('authenticator/%sLoginFormAuthenticator.tpl.php', $this->useSecurity52 ? 'Security52' : ''),
[
'user_fully_qualified_class_name' => trim($userClassNameDetails->getFullName(), '\\'),
'user_class_name' => $userClassNameDetails->getShortName(),
'username_field' => $userNameField,
'username_field_label' => Str::asHumanWords($userNameField),
'username_field_var' => Str::asCamelCase($userNameField),
'user_needs_encoder' => $this->userClassHasEncoder($securityData, $userClass),
'user_is_entity' => $this->doctrineHelper->isClassAMappedEntity($userClass),
'provider_key_type_hint' => $this->providerKeyTypeHint(),
Expand Down Expand Up @@ -322,7 +345,8 @@ private function generateNextMessage(bool $securityYamlUpdated, string $authenti
'main',
null,
$authenticatorClass,
$logoutSetup
$logoutSetup,
$this->useSecurity52
);
$nextTexts[] = '- Your <info>security.yaml</info> could not be updated automatically. You\'ll need to add the following config manually:\n\n'.$yamlExample;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php echo "<?php\n" ?>

namespace <?php echo $namespace ?>;

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\Authenticator\AbstractAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;

class <?php echo $class_name ?> extends AbstractAuthenticator
{
public function supports(Request $request): ?bool
{
// TODO: Implement supports() method.
}

public function authenticate(Request $request): PassportInterface
{
// TODO: Implement authenticate() method.
}

public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
// TODO: Implement onAuthenticationSuccess() method.
}

public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
// TODO: Implement onAuthenticationFailure() method.
}

// public function start(Request $request, AuthenticationException $authException = null): Response
// {
// /*
// * If you would like this class to control what happens when an anonymous user accesses a
// * protected page (e.g. redirect to /login), uncomment this method and make this class
// * implement Symfony\Component\Security\Http\EntryPoint\AuthenticationEntrypointInterface.
// *
// * For more details, see https://symfony.com/doc/current/security/experimental_authenticators.html#configuring-the-authentication-entry-point
// */
// }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?= "<?php\n" ?>

namespace <?= $namespace ?>;

use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Http\Authenticator\AbstractLoginFormAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;
use Symfony\Component\Security\Http\Util\TargetPathTrait;

class <?= $class_name; ?> extends AbstractLoginFormAuthenticator
{
use TargetPathTrait;

public const LOGIN_ROUTE = 'app_login';

private <?= $use_typed_properties ? 'UrlGeneratorInterface ' : null ?>$urlGenerator;

public function __construct(UrlGeneratorInterface $urlGenerator)
{
$this->urlGenerator = $urlGenerator;
}

public function authenticate(Request $request): PassportInterface
{
$<?= $username_field_var ?> = $request->request->get('<?= $username_field ?>', '');

$request->getSession()->set(Security::LAST_USERNAME, $<?= $username_field_var ?>);

return new Passport(
new UserBadge($<?= $username_field_var ?>),
new PasswordCredentials($request->request->get('password', '')),
[
new CsrfTokenBadge('authenticate', $request->get('_csrf_token')),
]
);
}

public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
if ($targetPath = $this->getTargetPath($request->getSession(), $firewallName)) {
return new RedirectResponse($targetPath);
}

// For example:
//return new RedirectResponse($this->urlGenerator->generate('some_route'));
throw new \Exception('TODO: provide a valid redirect inside '.__FILE__);
}

protected function getLoginUrl(Request $request): string
{
return $this->urlGenerator->generate(self::LOGIN_ROUTE);
}
}
73 changes: 60 additions & 13 deletions src/Security/SecurityConfigUpdater.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,39 @@
namespace Symfony\Bundle\MakerBundle\Security;

use Symfony\Bundle\MakerBundle\Util\YamlSourceManipulator;
use Symfony\Component\HttpKernel\Log\Logger;
use Symfony\Component\Security\Core\Encoder\NativePasswordEncoder;

/**
* @author Ryan Weaver <[email protected]>
* @author Jesse Rushlow <[email protected]>
*
* @internal
*/
final class SecurityConfigUpdater
{
/** @var YamlSourceManipulator */
private $manipulator;

/** @var Logger|null */
private $ysmLogger;

public function __construct(Logger $ysmLogger = null)
{
$this->ysmLogger = $ysmLogger;
}

/**
* Updates security.yaml contents based on a new User class.
*/
public function updateForUserClass(string $yamlSource, UserClassConfiguration $userConfig, string $userClass): string
{
$this->manipulator = new YamlSourceManipulator($yamlSource);

if (null !== $this->ysmLogger) {
$this->manipulator->setLogger($this->ysmLogger);
}

$this->normalizeSecurityYamlFile();

$this->updateProviders($userConfig, $userClass);
Expand All @@ -43,10 +59,14 @@ public function updateForUserClass(string $yamlSource, UserClassConfiguration $u
return $contents;
}

public function updateForAuthenticator(string $yamlSource, string $firewallName, $chosenEntryPoint, string $authenticatorClass, bool $logoutSetup): string
public function updateForAuthenticator(string $yamlSource, string $firewallName, $chosenEntryPoint, string $authenticatorClass, bool $logoutSetup, bool $useSecurity52): string
{
$this->manipulator = new YamlSourceManipulator($yamlSource);

if (null !== $this->ysmLogger) {
$this->manipulator->setLogger($this->ysmLogger);
}

$this->normalizeSecurityYamlFile();

$newData = $this->manipulator->getData();
Expand All @@ -56,23 +76,50 @@ public function updateForAuthenticator(string $yamlSource, string $firewallName,
}

if (!isset($newData['security']['firewalls'][$firewallName])) {
$newData['security']['firewalls'][$firewallName] = ['anonymous' => true];
if ($useSecurity52) {
$newData['security']['firewalls'][$firewallName] = ['lazy' => true];
} else {
$newData['security']['firewalls'][$firewallName] = ['anonymous' => 'lazy'];
}
}

$firewall = $newData['security']['firewalls'][$firewallName];

if (!isset($firewall['guard'])) {
$firewall['guard'] = [];
}
if ($useSecurity52) {
if (isset($firewall['custom_authenticator'])) {
if (\is_array($firewall['custom_authenticator'])) {
$firewall['custom_authenticator'][] = $authenticatorClass;
} else {
$stringValue = $firewall['custom_authenticator'];
$firewall['custom_authenticator'] = [];
$firewall['custom_authenticator'][] = $stringValue;
$firewall['custom_authenticator'][] = $authenticatorClass;
}
} else {
$firewall['custom_authenticator'] = $authenticatorClass;
}

if (!isset($firewall['guard']['authenticators'])) {
$firewall['guard']['authenticators'] = [];
}
if (!isset($firewall['entry_point']) && $chosenEntryPoint) {
$firewall['entry_point_empty_line'] = $this->manipulator->createEmptyLine();
$firewall['entry_point_comment'] = $this->manipulator->createCommentLine(
' the entry_point start() method determines what happens when an anonymous user accesses a protected page'
);
$firewall['entry_point'] = $authenticatorClass;
}
} else {
if (!isset($firewall['guard'])) {
$firewall['guard'] = [];
}

$firewall['guard']['authenticators'][] = $authenticatorClass;
if (!isset($firewall['guard']['authenticators'])) {
$firewall['guard']['authenticators'] = [];
}

if (\count($firewall['guard']['authenticators']) > 1) {
$firewall['guard']['entry_point'] = $chosenEntryPoint ?? current($firewall['guard']['authenticators']);
$firewall['guard']['authenticators'][] = $authenticatorClass;

if (\count($firewall['guard']['authenticators']) > 1) {
$firewall['guard']['entry_point'] = $chosenEntryPoint ?? current($firewall['guard']['authenticators']);
}
}

if (!isset($firewall['logout']) && $logoutSetup) {
Expand All @@ -86,10 +133,10 @@ public function updateForAuthenticator(string $yamlSource, string $firewallName,
}

$newData['security']['firewalls'][$firewallName] = $firewall;

$this->manipulator->setData($newData);
$contents = $this->manipulator->getContents();

return $contents;
return $this->manipulator->getContents();
}

private function normalizeSecurityYamlFile()
Expand Down
7 changes: 7 additions & 0 deletions src/Util/PhpCompatUtil.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ public function canUseAttributes(): bool
return version_compare($version, '8alpha', '>=') && Kernel::VERSION_ID >= 50200;
}

public function canUseTypedProperties(): bool
{
$version = $this->getPhpVersion();

return version_compare($version, '7.4', '>=');
}

protected function getPhpVersion(): string
{
$rootDirectory = $this->fileManager->getRootDirectory();
Expand Down
Loading

0 comments on commit fba06d8

Please sign in to comment.