feature #24337 Adding a shortcuts for the main security functionality (weaverryan, javiereguiluz)

This PR was squashed before being merged into the 3.4 branch (closes #24337).

Discussion
----------

Adding a shortcuts for the main security functionality

| Q             | A
| ------------- | ---
| Branch?       | 3.4
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | none
| License       | MIT
| Doc PR        | Big ol' TODO

I'd like one class that I can inject (especially with autowiring) to get access to the User and `isGranted()` methods. This is *really* important... because to get the User currently, you need to type-hint `TokenStorageInterface`... and there are *two*! That's really bad DX!

Questions:

A) I hi-jacked the existing `Security` class... I wanted a simple class called Security
B) I called the service `security.helper`... for lack of a better id.
C) I did not make `Security` implement the 2 other interfaces (`TokenStorageInterface`, `AuthorizationCheckerInterface`... but I suppose we could?)

Cheers!

Commits
-------

0851189 Adding a shortcuts for the main security functionality
This commit is contained in:
Robin Chalas 2017-09-28 17:13:14 +02:00
commit 3b5742e6b5
10 changed files with 240 additions and 3 deletions

View File

@ -4,6 +4,8 @@ CHANGELOG
3.4.0
-----
* Added new `security.helper` service that is an instance of `Symfony\Component\Security\Core\Security`
and provides shortcuts for common security tasks.
* Tagging voters with the `security.voter` tag without implementing the
`VoterInterface` on the class is now deprecated and will be removed in 4.0.
* [BC BREAK] `FirewallContext::getListeners()` now returns `\Traversable|array`

View File

@ -26,6 +26,19 @@
</service>
<service id="Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface" alias="security.token_storage" />
<service id="security.helper" class="Symfony\Component\Security\Core\Security">
<argument type="service">
<service class="Symfony\Component\DependencyInjection\ServiceLocator">
<tag name="container.service_locator" />
<argument type="collection">
<argument key="security.token_storage" type="service" id="security.token_storage" />
<argument key="security.authorization_checker" type="service" id="security.authorization_checker" />
</argument>
</service>
</argument>
</service>
<service id="Symfony\Component\Security\Core\Security" alias="security.helper" />
<service id="security.user_value_resolver" class="Symfony\Bundle\SecurityBundle\SecurityUserValueResolver">
<argument type="service" id="security.token_storage" />
<tag name="controller.argument_value_resolver" priority="40" />

View File

@ -0,0 +1,34 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bundle\SecurityBundle\Tests\Functional;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\User\User;
class SecurityTest extends WebTestCase
{
public function testServiceIsFunctional()
{
$kernel = self::createKernel(array('test_case' => 'SecurityHelper', 'root_config' => 'config.yml'));
$kernel->boot();
$container = $kernel->getContainer();
// put a token into the storage so the final calls can function
$user = new User('foo', 'pass');
$token = new UsernamePasswordToken($user, '', 'provider', array('ROLE_USER'));
$container->get('security.token_storage')->setToken($token);
$security = $container->get('functional_test.security.helper');
$this->assertTrue($security->isGranted('ROLE_USER'));
$this->assertSame($token, $security->getToken());
}
}

View File

@ -0,0 +1,20 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
use Symfony\Bundle\TwigBundle\TwigBundle;
use Symfony\Bundle\SecurityBundle\SecurityBundle;
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
return array(
new FrameworkBundle(),
new SecurityBundle(),
new TwigBundle(),
);

View File

@ -0,0 +1,18 @@
imports:
- { resource: ./../config/default.yml }
services:
# alias the service so we can access it in the tests
functional_test.security.helper:
alias: security.helper
public: true
security:
providers:
in_memory:
memory:
users: []
firewalls:
default:
anonymous: ~

View File

@ -4,6 +4,7 @@ CHANGELOG
3.4.0
-----
* Added `getUser`, `getToken` and `isGranted` methods to `Security`.
* added a `setToken()` method to the `SwitchUserEvent` class to allow to replace the created token while switching users
when custom token generation is required by application.
* Using voters that do not implement the `VoterInterface`is now deprecated in

View File

@ -11,10 +11,12 @@
namespace Symfony\Component\Security\Core;
use Psr\Container\ContainerInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* This class holds security information.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
* Helper class for commonly-needed security tasks.
*/
final class Security
{
@ -22,4 +24,50 @@ final class Security
const AUTHENTICATION_ERROR = '_security.last_error';
const LAST_USERNAME = '_security.last_username';
const MAX_USERNAME_LENGTH = 4096;
private $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
/**
* @return UserInterface|null
*/
public function getUser()
{
if (!$token = $this->getToken()) {
return null;
}
$user = $token->getUser();
if (!is_object($user)) {
return null;
}
return $user;
}
/**
* Checks if the attributes are granted against the current authentication token and optionally supplied subject.
*
* @param mixed $attributes
* @param mixed $subject
*
* @return bool
*/
public function isGranted($attributes, $subject = null)
{
return $this->container->get('security.authorization_checker')
->isGranted($attributes, $subject);
}
/**
* @return TokenInterface|null
*/
public function getToken()
{
return $this->container->get('security.token_storage')->getToken();
}
}

View File

@ -0,0 +1,97 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Core\Tests;
use PHPUnit\Framework\TestCase;
use Psr\Container\ContainerInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\User\User;
class SecurityTest extends TestCase
{
public function testGetToken()
{
$token = new UsernamePasswordToken('foo', 'bar', 'provider');
$tokenStorage = $this->getMockBuilder(TokenStorageInterface::class)->getMock();
$tokenStorage->expects($this->once())
->method('getToken')
->will($this->returnValue($token));
$container = $this->createContainer('security.token_storage', $tokenStorage);
$security = new Security($container);
$this->assertSame($token, $security->getToken());
}
/**
* @dataProvider getUserTests
*/
public function testGetUser($userInToken, $expectedUser)
{
$token = $this->getMockBuilder(TokenInterface::class)->getMock();
$token->expects($this->any())
->method('getUser')
->will($this->returnValue($userInToken));
$tokenStorage = $this->getMockBuilder(TokenStorageInterface::class)->getMock();
$tokenStorage->expects($this->once())
->method('getToken')
->will($this->returnValue($token));
$container = $this->createContainer('security.token_storage', $tokenStorage);
$security = new Security($container);
$this->assertSame($expectedUser, $security->getUser());
}
public function getUserTests()
{
yield array(null, null);
yield array('string_username', null);
$user = new User('nice_user', 'foo');
yield array($user, $user);
}
public function testIsGranted()
{
$authorizationChecker = $this->getMockBuilder(AuthorizationCheckerInterface::class)->getMock();
$authorizationChecker->expects($this->once())
->method('isGranted')
->with('SOME_ATTRIBUTE', 'SOME_SUBJECT')
->will($this->returnValue(true));
$container = $this->createContainer('security.authorization_checker', $authorizationChecker);
$security = new Security($container);
$this->assertTrue($security->isGranted('SOME_ATTRIBUTE', 'SOME_SUBJECT'));
}
private function createContainer($serviceId, $serviceObject)
{
$container = $this->getMockBuilder(ContainerInterface::class)->getMock();
$container->expects($this->atLeastOnce())
->method('get')
->with($serviceId)
->will($this->returnValue($serviceObject));
return $container;
}
}

View File

@ -20,6 +20,7 @@
"symfony/polyfill-php56": "~1.0"
},
"require-dev": {
"psr/container": "^1.0",
"symfony/event-dispatcher": "~2.8|~3.0|~4.0",
"symfony/expression-language": "~2.8|~3.0|~4.0",
"symfony/http-foundation": "~2.8|~3.0|~4.0",
@ -28,6 +29,7 @@
"psr/log": "~1.0"
},
"suggest": {
"psr/container": "To instantiate the Security class",
"symfony/event-dispatcher": "",
"symfony/http-foundation": "",
"symfony/validator": "For using the user password constraint",

View File

@ -32,6 +32,7 @@
"symfony/security-http": "self.version"
},
"require-dev": {
"psr/container": "^1.0",
"symfony/finder": "~2.8|~3.0|~4.0",
"symfony/polyfill-intl-icu": "~1.0",
"symfony/routing": "~2.8|~3.0|~4.0",
@ -41,6 +42,7 @@
"psr/log": "~1.0"
},
"suggest": {
"psr/container": "To instantiate the Security class",
"symfony/form": "",
"symfony/validator": "For using the user password constraint",
"symfony/routing": "For using the HttpUtils class to create sub-requests, redirect the user, and match URLs",