merged branch canni/user_comparable_interface2 (PR #2927)

Commits
-------

e23d452 Add info about BC Break to CHANGELOG-2.1
d7ffeb5 Add some more tests, and enforce boolean return value of interface implementations.
9d3a49f When method name is `hasUserChanged` the return boolean should be true (to match question semantics) and false when user has not changed, this commits inverts return statements.
c57b528 Add note about `AdvancedUserInterface`.
3682f62 Refactor `isUserChanged` to `hasUserChanged`
56db4a1 Change names to Equatable
680b108 Suggested fixes ;)
9386583 [BC Break][Security] Moved user comparsion logic out of UserInterface As discussed on IRC meetings and in PR #2669 I came up with implementation. This is option2, I think more elegant.

Discussion
----------

[BC Break][Security][Option2] Moved user comparsion logic out of UserInterface

As discussed on IRC meetings and in PR #2669 I came up with implementation.
This is option2, I think more elegant.

BC break: yes
Feature addition: no/feature move
Symfony2 test pass: yes
Symfony2 test written: yes
Todo: decide about naming

[![Build Status](https://secure.travis-ci.org/canni/symfony.png)](http://travis-ci.org/canni/symfony)

---------------------------------------------------------------------------

by schmittjoh at 2011-12-19T19:33:24Z

This looks much better than the previous PR. Thanks!

One thing, we also discussed this on Doctrine, the name "comparable" is used in most programming languages to perform a real compare operation that is ">", "<", or "=". In this case though, we are specifically interested in equality of two objects (we cannot establish a natural order between these objects). Java has no such interface as all objects naturally have an equals() method, .NET uses "Equatable" which looks a bit odd. Not sure if there are better names.

---------------------------------------------------------------------------

by canni at 2011-12-19T19:34:52Z

I think this is best of "both worlds" we have nice full-featured implementation suitable for most, and if someone needs advanced compare logic just implements interface. @stof @schmittjoh, what do you think?

---------------------------------------------------------------------------

by stof at 2011-12-19T19:36:55Z

@canni I already commented on the code, and I agree with @schmittjoh that the naming can be confusing

---------------------------------------------------------------------------

by jmikola at 2011-12-20T17:33:22Z

I don't mean to bikeshed, but I strongly agree with @schmittjoh about implications of "compare". I'm not concerned with the interface name so much as I am with `compareUser()`. Given that this method returns a boolean, I think it's best to prefix it with `is` (e.g. `isSameUser`, `isUserEqualTo`) or `equals` (e.g. `equalsUser`).

In this PR, the Token class is implementing the interface, so I think having "User" in the method name is a good idea. Naturally, if the interface was intended for User classes, we could do without it.

---------------------------------------------------------------------------

by canni at 2011-12-20T19:00:00Z

@jmikola in this PR Token class does not implement any additional interface, and `compareUser` is `private` and used internally. I don't stand still after this names, I'll update PR as soon as some decision about naming will be done.

---------------------------------------------------------------------------

by jmikola at 2011-12-21T02:29:59Z

@canni: My mistake, I got confused between the Token method and interface method, which you've since renamed in canni/symfony@fcfcd1087b.

---------------------------------------------------------------------------

by mvrhov at 2011-12-21T06:09:45Z

hm. Now I'm going to bike shed. Wouldn't the proper function name be hasUserChanged?

---------------------------------------------------------------------------

by stof at 2011-12-21T10:58:38Z

it would probably be bettter. The meaning of ``true`` and ``false`` would then be the opposite of the current ones but this is not an issue IMO as it is a different method

---------------------------------------------------------------------------

by jstout24 at 2011-12-27T18:08:49Z

@canni nice job

---------------------------------------------------------------------------

by fabpot at 2011-12-30T14:59:11Z

The method `isUserChanged()` must be rename. What about `hasUserChanged()` as @mvrhov suggested or `isUserDifferent()`?

---------------------------------------------------------------------------

by canni at 2012-01-02T11:44:05Z

@fabpot done.

---------------------------------------------------------------------------

by fabpot at 2012-01-02T18:13:40Z

The only missing thing I can think of is adding some unit tests.

---------------------------------------------------------------------------

by canni at 2012-01-10T20:16:25Z

@fabpot is there anything more you think that should done in this PR?

---------------------------------------------------------------------------

by stof at 2012-01-10T20:38:46Z

@canni can you rebase your branch ? it conflicts with the current master according to github

---------------------------------------------------------------------------

by canni at 2012-01-10T20:56:55Z

@stof done.

---------------------------------------------------------------------------

by fabpot at 2012-01-12T18:06:00Z

@canni: Can you just add some information in the CHANGELOG and in the UPGRADE file? That's all I need to merge this PR now. Thanks a lot.

---------------------------------------------------------------------------

by canni at 2012-01-12T18:16:32Z

@fabpot done, and no problem :)
This commit is contained in:
Fabien Potencier 2012-01-12 19:26:40 +01:00
commit 741859dc47
7 changed files with 108 additions and 85 deletions

View File

@ -79,6 +79,10 @@ To get the diff between two versions, go to https://github.com/symfony/symfony/c
toto: { password: foobar, roles: [ROLE_USER] }
foo: { password: bar, roles: [ROLE_USER, ROLE_ADMIN] }
* [BC BREAK] Method `equals` was removed from `UserInterface` to its own new
`EquatableInterface`, now user class can implement this interface to override
the default implementation of users equality test.
* added a validator for the user password
* added 'erase_credentials' as a configuration key (true by default)
* added new events: `security.authentication.success` and `security.authentication.failure`

View File

@ -14,6 +14,8 @@ namespace Symfony\Component\Security\Core\Authentication\Token;
use Symfony\Component\Security\Core\Role\RoleInterface;
use Symfony\Component\Security\Core\Role\Role;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\AdvancedUserInterface;
use Symfony\Component\Security\Core\User\EquatableInterface;
/**
* Base class for Token instances.
@ -87,7 +89,7 @@ abstract class AbstractToken implements TokenInterface
if (!$user instanceof UserInterface) {
$changed = true;
} else {
$changed = !$this->user->equals($user);
$changed = $this->hasUserChanged($user);
}
} elseif ($user instanceof UserInterface) {
$changed = true;
@ -220,4 +222,49 @@ abstract class AbstractToken implements TokenInterface
return sprintf('%s(user="%s", authenticated=%s, roles="%s")', $class, $this->getUsername(), json_encode($this->authenticated), implode(', ', $roles));
}
private function hasUserChanged(UserInterface $user)
{
if (!($this->user instanceof UserInterface)) {
throw new \BadMethodCallException('Method "hasUserChanged" should be called when current user class is instance of "UserInterface".');
}
if ($this->user instanceof EquatableInterface) {
return ! (Boolean) $this->user->isEqualTo($user);
}
if ($this->user->getPassword() !== $user->getPassword()) {
return true;
}
if ($this->user->getSalt() !== $user->getSalt()) {
return true;
}
if ($this->user->getUsername() !== $user->getUsername()) {
return true;
}
if ($this->user instanceof AdvancedUserInterface && $user instanceof AdvancedUserInterface) {
if ($this->user->isAccountNonExpired() !== $user->isAccountNonExpired()) {
return true;
}
if ($this->user->isAccountNonLocked() !== $user->isAccountNonLocked()) {
return true;
}
if ($this->user->isCredentialsNonExpired() !== $user->isCredentialsNonExpired()) {
return true;
}
if ($this->user->isEnabled() !== $user->isEnabled()) {
return true;
}
} elseif ($this->user instanceof AdvancedUserInterface xor $user instanceof AdvancedUserInterface) {
return true;
}
return false;
}
}

View File

@ -0,0 +1,37 @@
<?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\User;
/**
* EquatableInterface used to test if two objects are equal in security
* and re-authentication context.
*
* @author Dariusz Górecki <darek.krk@gmail.com>
*/
interface EquatableInterface
{
/**
* The equality comparison should neither be done by referential equality
* nor by comparing identities (i.e. getId() === getId()).
*
* However, you do not need to compare every attribute, but only those that
* are relevant for assessing whether re-authentication is required.
*
* Also implementation should consider that $user instance may implement
* the extended user interface `AdvancedUserInterface`.
*
* @param UserInterface $user
*
* @return Boolean
*/
function isEqualTo(UserInterface $user);
}

View File

@ -112,44 +112,4 @@ final class User implements AdvancedUserInterface
public function eraseCredentials()
{
}
/**
* {@inheritDoc}
*/
public function equals(UserInterface $user)
{
if (!$user instanceof User) {
return false;
}
if ($this->password !== $user->getPassword()) {
return false;
}
if ($this->getSalt() !== $user->getSalt()) {
return false;
}
if ($this->username !== $user->getUsername()) {
return false;
}
if ($this->accountNonExpired !== $user->isAccountNonExpired()) {
return false;
}
if ($this->accountNonLocked !== $user->isAccountNonLocked()) {
return false;
}
if ($this->credentialsNonExpired !== $user->isCredentialsNonExpired()) {
return false;
}
if ($this->enabled !== $user->isEnabled()) {
return false;
}
return true;
}
}

View File

@ -84,19 +84,4 @@ interface UserInterface
* @return void
*/
function eraseCredentials();
/**
* Returns whether or not the given user is equivalent to *this* user.
*
* The equality comparison should neither be done by referential equality
* nor by comparing identities (i.e. getId() === getId()).
*
* However, you do not need to compare every attribute, but only those that
* are relevant for assessing whether re-authentication is required.
*
* @param UserInterface $user
*
* @return Boolean
*/
function equals(UserInterface $user);
}

View File

@ -144,13 +144,10 @@ class AbstractTokenTest extends \PHPUnit_Framework_TestCase
public function getUsers()
{
$user = $this->getMock('Symfony\Component\Security\Core\User\UserInterface');
$user
->expects($this->any())
->method('equals')
->will($this->returnValue(true))
;
$advancedUser = $this->getMock('Symfony\Component\Security\Core\User\AdvancedUserInterface');
return array(
array($advancedUser),
array($user),
array(new TestUser('foo')),
array('foo'),
@ -176,11 +173,7 @@ class AbstractTokenTest extends \PHPUnit_Framework_TestCase
public function getUserChanges()
{
$user = $this->getMock('Symfony\Component\Security\Core\User\UserInterface');
$user
->expects($this->any())
->method('equals')
->will($this->returnValue(false))
;
$advancedUser = $this->getMock('Symfony\Component\Security\Core\User\AdvancedUserInterface');
return array(
array(
@ -193,14 +186,20 @@ class AbstractTokenTest extends \PHPUnit_Framework_TestCase
'foo', $user,
),
array(
$user, $user,
'foo', $advancedUser
),
array(
$user, 'foo'
),
array(
$advancedUser, 'foo'
),
array(
$user, new TestUser('foo'),
),
array(
$advancedUser, new TestUser('foo'),
),
array(
new TestUser('foo'), new TestUser('bar'),
),
@ -210,6 +209,15 @@ class AbstractTokenTest extends \PHPUnit_Framework_TestCase
array(
new TestUser('foo'), $user,
),
array(
new TestUser('foo'), $advancedUser,
),
array(
$user, $advancedUser
),
array(
$advancedUser, $user
),
);
}

View File

@ -123,22 +123,4 @@ class UserTest extends \PHPUnit_Framework_TestCase
$user->eraseCredentials();
$this->assertEquals('superpass', $user->getPassword());
}
public function testUsersAreEqual()
{
$user1 = new User('fabien', 'superpass', array('ROLE_USER'));
$user2 = clone $user1;
$this->assertTrue($user1->equals($user2));
$this->assertTrue($user2->equals($user1));
}
public function testUsersAreNotEqual()
{
$user1 = new User('fabien', 'superpass', array('ROLE_USER'));
$user2 = new User('fabien', 'superpass', array('ROLE_USER'), false);
$this->assertFalse($user1->equals($user2));
$this->assertFalse($user2->equals($user1));
}
}