My roles are stored in the database and I am trying to load them dynamically upon login. What I'm doing is querying for the roles and setting them on the user object in my user provider as seen here:
public function loadUserByUsername($username) {
$q = $this
->createQueryBuilder('u')
->where('u.username = :username')
->setParameter('username', $username)
->getQuery()
;
try {
// The Query::getSingleResult() method throws an exception
// if there is no record matching the criteria.
$user = $q->getSingleResult();
// Permissions
$permissions = $this->_em->getRepository('E:ModulePermission')->getPermissionsForUser($user);
if ($permissions !== null) {
foreach ($permissions as $permission) {
$name = strtoupper(str_replace(" ", "_", $permission['name']));
$role = "ROLE_%s_%s";
if ($permission['view']) {
$user->addRole(sprintf($role, $name, 'VIEW'));
}
if ($permission['add']) {
$user->addRole(sprintf($role, $name, 'ADD'));
}
if ($permission['edit']) {
$user->addRole(sprintf($role, $name, 'EDIT'));
}
if ($permission['delete']) {
$user->addRole(sprintf($role, $name, 'DELETE'));
}
}
}
} catch (NoResultException $e) {
throw new UsernameNotFoundException(sprintf('Unable to find an active admin Entity:User object identified by "%s".', $username), null, 0, $e);
}
return $user;
}
And the user entity:
class User implements AdvancedUserInterface, \Serializable {
....
protected $roles;
....
public function __construct() {
$this->salt = base_convert(sha1(uniqid(mt_rand(), true)), 16, 36);
$this->roles = array();
}
....
public function getRoles() {
$roles = $this->roles;
// Ensure we having something
$roles[] = static::ROLE_DEFAULT;
return array_unique($roles);
}
public function addRole($role) {
$role = strtoupper($role);
$roles = $this->getRoles();
if ($role === static::ROLE_DEFAULT) {
return $this;
}
if (!in_array($role, $roles, true)) {
$this->roles[] = $role;
}
return $this;
}
public function hasRole($role) {
$role = strtoupper($role);
$roles = $this->getRoles();
return in_array($role, $roles, true);
}
}
This works fine and dandy and I see the correct roles when I do:
$this->get('security.context')->getUser()->getRoles()
The problem (I think), is that the token does not know about these roles. Because calling getRoles() on the token is showing only ROLE_USER, which is the default role.
It seems to me that the token is being created before the user is loaded by the UserProvider. I've looked through a lot of the security component but I can't for the life of me find the right part of the process to hook into to set these roles correctly so that the token knows about them.
Update Following the Load roles from database doc works fine, but this does not match my use case as shown here. My schema differs as each role has additional permissions (view/add/edit/delete) and this is why I am attempting the approach here. I don't want to have to alter my schema just to work with Symfony's security. I'd rather understand why these roles are not properly bound (not sure the correct doctrine word here) on my user object at this point.
It looks like you may not be aware of the built in role management that Symfony offers. Read the docs - Managing roles in the database It is actually quite simple to do what you want, all you need to do is implement an interface and define your necessary function. The docs I linked to provide great examples. Take a look.
UPDATE
It looks like the docs don't give you the use statement for the AdvancedUserInterface. Here it is:
// in your user entity
use Symfony\Component\Security\Core\User\AdvancedUserInterface;
then in your role entity:
use Symfony\Component\Security\Core\Role\RoleInterface;
The docs show you how to do the rest.
UPDATE
Take a look at this blog post, which shows how to create roles dynamically:
Dynamically create roles
The problem here stemmed from the fact that I thought I was implementing
Symfony\Component\Security\Core\User\EquatableInterface;
but wasn't (as you can see in the original question, I forgot to add it to my class definition). I'm leaving this here for people if they come across it. All you need is to implement this interface, and add the following method to your user entity.
public function isEqualTo(UserInterface $user) {
if ($user instanceof User) {
// Check that the roles are the same, in any order
$isEqual = count($this->getRoles()) == count($user->getRoles());
if ($isEqual) {
foreach($this->getRoles() as $role) {
$isEqual = $isEqual && in_array($role, $user->getRoles());
}
}
return $isEqual;
}
return false;
}
Related
In Symfony, during the authentication, I want to attribute specific role to my user.
If I specify ->setRoles() in my authenticator, or my "getRoles" function, I come back to the login page, anonymously
Following code in Authenticator doesn't work
public function getUser($credentials, UserProviderInterface $userProvider)
{
$token = new CsrfToken('authenticate', $credentials['csrf_token']);
if (!$this->csrfTokenManager->isTokenValid($token)) {
throw new InvalidCsrfTokenException();
}
$user = $this->entityManager->getRepository(User::class)->findOneBy(['customId' => $credentials['customId']]);
if (!$user) {
// fail authentication with a custom error
throw new CustomUserMessageAuthenticationException('CustomId could not be found.');
}
if($user->getId() == 2) {
$user->setRoles(['ROLE_SUPER_ADMIN']);
}
return $user;
}
This code in my Entity doesn't work
/**
* #see UserInterface
*/
public function getRoles(): array
{
$roles = $this->roles;
// guarantee every user at least has ROLE_USER
$roles[] = 'ROLE_USER';
if($this->getId() == 2) {
$this->setRoles(['ROLE_SUPER_ADMIN']);
}
return array_unique($roles);
}
If you change the user object it will not match the one in the database. Symfony will recognize this as someone messing with the stored data and log you out for safety.
You can change how the comparison of the user is done by implementing the EquatableInterface:
class User implements EquatableInterface
{
public function isEqual(UserInterface $user): bool
{
// Example for what your comparison could look like
return $user->getUsername() === $this->getUsername() && $user->getId() === $this->getId();
}
}
You can find this (in a rather small section) in the docs: https://symfony.com/doc/current/security/user_provider.html#comparing-users-manually-with-equatableinterface
I have a many to many relationship between User and Role models and want to check if a user has any role of a given list.
So I typed on model:
public function hasAnyRoles(array $roles) {
return $this->roles()->whereIn('name', $roles)->exists();
}
And on UserController:
// User has only "manager" role.
$user->hasAnyRoles(['admin', 'manager']) // true
The problem is that in some parts I need to verify if the user has all roles of the given list. For example:
// User has only "manager" role.
$user->hasRoles(['admin', 'manager']) // false
I wrote it in a "lazy" mode, that generates n + 1 queries:
public function hasRolesLazy($roles) {
return array_reduce($roles, function ($initial, $role) {
return $this->roles()->where('name', $role)->exists();
}, false);
}
How hasRoles(array $roles) method must be constructed to execute only one query in database? I'm a newbie in SQL so I can't figure out many solutions for this.
Try this:
public function hasAnyRoles(array $roles) {
return $this->whereHas('roles', function ($query) use ($roles) {
return $query->whereIn('name', $roles);
})->isNotEmpty();
}
My recommendation would be, to load all of the roles for the user once using the relationship. When you load a relationship by calling it like a property, Laravel will cache it under the hood, so it doesn't repeat any queries. You can then simply use the loaded roles to implement your two methods:
public function hasAnyRoles(array $roles)
{
// When you call $this->roles, the relationship is cached. Run through
// each of the user's roles.
foreach ($this->roles as $role) {
// If the user's role we're looking at is in the provided $roles
// array, then the user has at least one of them. Return true.
if (in_array($role->name, $roles) {
return true;
}
}
// If we get here, the user does not have any of the required roles.
// Return false.
return false;
}
and the second method:
public function hasRoles(array $roles)
{
// Run through each of the roles provided, that the user MUST have
foreach ($roles as $role) {
// Call the $this->roles relationship, which will return a collection
// and be cached. Find the first role that has the name of the role
// we're looking at.
$found = $this->roles->first(function ($r) use ($role) {
return $r->name == $role;
});
// If a role could not be found in the user's roles that matches the
// one we're looking, then the user does not have the role, return
// false
if (!$found) {
return false;
}
}
// If we get here, the user has all of the roles provided to the method
return true;
}
There are of course many different ways to implement these methods, particularly methods found in the Collection class will help you, but the point is that using $this->roles results in only a single query.
I would like to know how to implement a check for a field inside voters of an entity.
I have for example my entity Post where I want that a user not admin can't edit title field. Only admin can edit this field.
So I have created my voters but I don't know how to create this check because inside $post there is the old post entity and I don't know how to implement the check for title field
This is my easy voters file
class PostVoter extends Voter
{
const VIEW = 'view';
const EDIT = 'edit';
private $decisionManager;
public function __construct(AccessDecisionManagerInterface $decisionManager)
{
$this->decisionManager = $decisionManager;
}
protected function supports($attribute, $subject)
{
if (!in_array($attribute, array(self::VIEW, self::EDIT))) {
return false;
}
if (!$subject instanceof Post) {
return false;
}
return true;
}
protected function voteOnAttribute(
$attribute,
$subject,
TokenInterface $token
) {
$user = $token->getUser();
if (!$user instanceof User) {
return false;
}
if ($this->decisionManager->decide($token, array('ROLE_SUPER_ADMIN'))) {
return true;
}
/** #var Post $post */
$post = $subject;
switch ($attribute) {
case self::VIEW:
return $this->canView($post, $user);
case self::EDIT:
return $this->canEdit($post, $user);
}
throw new \LogicException('This code should not be reached!');
}
private function canView(Post $post, User $user)
{
if ($this->canEdit($post, $user)) {
return true;
}
return true;
}
private function canEdit(Post $post, User $user)
{
return $user === $post->getUser();
}
}
I would like to implement inside canEdit a check for the title field.
I have tried to print $post but there is only old value not some information for new value.
Couple of possible approaches.
The one I would use is to add a 'edit_title' permission to the voter then adjust my form to make the title read only if the edit_title permission was denied. This not only eliminates the need to check for a changed title but also makes things a bit friendlier for the users. One might imagine them being a bit frustrated with a form that allows them to change the title but then the app rejects the change.
If you really wanted to detect a title change then you could adjust the setTitle method in your post entity. Something like:
class Post {
private $titleWasChanged = false;
public function setTitle($title) {
if ($title !== $this->title) $this->titleWasChanged = true;
$this->title = $title;
And then of course check $titleWasChanged from the voter.
If you really wanted to go all out, the Doctrine entity manager actually has some change checking capability. You could probably access it via the voter but that would probably be overkill. http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/change-tracking-policies.html
In my web application I need to show the type of user in the view in protected/views/layouts/main.php.
But I am getting this error:
"CException" ."Property "CWebUser.type" is not defined."
I am unable to get rid of this error , how to resolve this issue?
I am using this line of code to display the type of user
array('label'=>'Logout ('.Yii::app()->user->type.')', 'url'=>array('/site/logout'),
'visible'=>!Yii::app()->user->isGuest)
I tried by using user->user_type also but not working
My code for the UserIdentity class
class UserIdentity extends CUserIdentity
{
private $_id;
public function authenticate()
{
$user = User::model()->findByAttributes(array(
'email'=>$this->username));
if ($user === null) {
$this->errorCode=self::ERROR_USERNAME_INVALID;
} else if ($user->pass !==
hash_hmac('sha256', $this->password,
Yii::app()->params['encryptionKey']) ) {
$this->errorCode=self::ERROR_PASSWORD_INVALID;
} else {
$this->errorCode=self::ERROR_NONE;
$this->setState('type', $user->user_type);
$this->setState('id', $user->id);
$this->_id = $user->id;
}
return !$this->errorCode;
}
public function getId() {
return $this->_id;
}
}
Also since I am using Role based access control I have changed the code in user.php for assigning roles to users
My code to assign users type.
public function afterSave() {
if (!Yii::app()->authManager->isAssigned(
$this->type,$this->id)) {
Yii::app()->authManager->assign($this->type,
$this->id);
}
return parent::afterSave();
}
And I have used this code in my SiteController for assigning roles to users
$auth->assign($user->type,$user->id);
If I;m right in what's happening, there may be times when you're not logged in that Yii is trying to access the user settings. As you're not logged in you can't access them, hence the error. So in the label, check that user isset()
'label' => (isset(Yii::app()->user->type) ? Yii::app()->user->type : '')
Please, I need to understand how security works and if it can be overriden.
I've read a lot of Symfony Book and Cookbook, and I'd like to implement my own security access check, can that be done? Because it lacks some functionality in roles, like having a constraint of type "if is.author then canedit"
Is it hard to implement? Does FOS UserBundle have this functionality? (Not shown in Docs).
Thanks!
You can implement symfony2 Voters to define access right :
http://symfony.com/doc/2.0/cookbook/security/voters.html
http://kriswallsmith.net/post/15994931191/symfony2-security-voters
Lets create our Voter class :
class PostAuthorVoter implements VoterInterface
{
public function supportsAttribute($attribute)
{
return 'POST_AUTHOR' === $attribute;
}
public function supportsClass($class)
{
return $class instanceof Post;
}
public function vote(TokenInterface $token, $object, array $attributes)
{
// $attributes is an array so we do a foreach loop
foreach ($attributes as $attribute)
{
// if $attribute is POST_AUTHOR and $object is an instance of Post
if ($this->supportsAttribute($attribute) && $this->supportsClass($object))
{
$user = $token->getUser();
// assuming that $posts in an \Doctrine\Common\Collections\ArrayCollection
// we check that user's posts contains the current $object
if ($user->getPosts()->contains($object))
{
return VoterInterface::ACCESS_GRANTED;
}
else
{
return VoterInterface::ACCESS_DENIED;
}
}
}
return VoterInterface::ACCESS_ABSTAIN;
}
}
Then you will be able to call the isGranted method of the security component in your controller like this :
if (!$this->get('security.context')->isGranted('POST_AUTHOR', $post)) {
throw new AccessDeniedException();
}