How to implement role / resources ACL in Symfony2 - symfony

I'm a bit disconcerted by the way access control lists are implemented in Symfony2.
In Zend Framework (versions 1 & 2), a list of resources and a list of roles are defined and each role is assigned a subset of resources it's allowed to access. Resources and roles are therefore the main vocabulary of ACL implementation, which is not the case in Symfony2, where only roles rule.
In a legacy app database, I have tables defining a list of roles, a list of resources and a list of allowed resources for each role (many-to-many relationship). Each user is assigned a role (admin, super admin, editor, and such).
I need to make use of this database in a Symfony2 application.
My resources look like this : ARTICLE_EDIT, ARTICLE_WRITE, COMMENT_EDIT, etc.
My User entity in Symfony implements the Symfony\Component\Security\Core\User\UserInterface interface and therefore has a getRoles) method.
I intend to use this method to define the allowed resources, which means I use roles as resources (I mean that what's called resources in Zend Framework is called roles here).
Do you confirm that I should use this method ?
This means I don't care anymore about the role (admin, editor, ...) of each user, but only about its resources.
I would then use $this->get('security.context')->isGranted('ROLE_ARTICLE_WRITE') in my controllers.
Is this the right way to do it and wouldn't it be a circumvented way to use roles in Symfony?

To answer this question years later, it was pretty easy to solve.
The solution is to mix the notions of roles and resources.
Let's assume a role table, a resource table and and role_resource many to many relation are defined.
Users are stored in a user table.
Here are the corresponding Doctrine entities:
User:
use Symfony\Component\Security\Core\User\UserInterface;
class User implements UserInterface
{
/**
* #Id #Column(type="integer")
* #GeneratedValue
*/
private $id;
/**
* #ManyToOne(targetEntity="Role")
* #JoinColumn(name="role_id", referencedColumnName="id")
**/
private $role;
// ...
}
Role:
class Role
{
/**
* #Id #Column(type="integer")
* #GeneratedValue
*/
private $id;
/** #Column(type="string") */
private $name;
/**
* #ManyToMany(targetEntity="Resource")
* #JoinTable(name="role_resource",
* joinColumns={#JoinColumn(name="role_id", referencedColumnName="id")},
* inverseJoinColumns={#JoinColumn(name="resource_id", referencedColumnName="id")}
* )
**/
private $resources;
// ...
}
Resource:
class Resource
{
/**
* #Id #Column(type="integer")
* #GeneratedValue
*/
private $id;
/** #Column(type="string") */
private $name;
// ...
}
So now the solution is to implement the getRoles of UserInterface this way:
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\Role\Role;
class User implements UserInterface
{
// ...
/**
* #var Role[]
**/
private $roles;
/**
* {#inheritDoc}
*/
public function getRoles()
{
if (isset($this->roles)) {
return $this->roles;
}
$this->roles = array();
$userRole = $this->getRole();
$resources = $userRole->getResources();
foreach ($resources as $resource) {
$this->roles[] = new Role('ROLE_' . $resource);
}
return $this->roles;
}
}
This way, resources attributed to the current user can be checked this way (considering there is a resource whose name is ARTICLE_WRITE):
$this->get('security.context')->isGranted('ROLE_ARTICLE_WRITE')

I think this will answer your question.
http://symfony.com/doc/current/cookbook/security/acl.html
http://symfony.com/doc/current/cookbook/security/acl_advanced.html
$builder = new MaskBuilder();
$builder
->add('view')
->add('edit')
->add('delete')
->add('undelete');
$mask = $builder->get(); // int(29)
$identity = new UserSecurityIdentity('johannes', 'Acme\UserBundle\Entity\User');
$acl->insertObjectAce($identity, $mask);

Related

Security voter on relational entity field when not using custom subresource path

I have started doing some more advanced security things in our application, where companies can create their own user roles with customizable CRUD for every module, which means you can create a custom role "Users read only" where you set "read" to "2" and create, update, delete to 0 for the user module. And the same for the teams module.
0 means that he have no access at all.
1 means can access all data under company,
2 means can access only things related to him (if he is owner
of an another user),
Which should result in the behavior that, when user requests a team over a get request, it returns the team with the users that are in the team, BUT, since the user role is configured with $capabilities["users"]["read"] = 2, then team.users should contain only him, without the other team members, because user cannot see users except himself and users that he created.
So far I have managed to limit collection-get operations with a doctrine extension that implements QueryCollectionExtensionInterface and filters out what results to return to the user:
when I query with a role that has $capabilities["teams"]["read"] = 2 then the collection returns only teams that user is part of, or teams that he created.
when I query for users with role that has $capabilities["teams"]["read"] = 1 then it returns all teams inside the company. Which is correct.
The problem comes when I query a single team. For security on item operations I use Voters, which checks the user capabilities before getting/updating/inserting/... a new entity to the DB, which works fine.
So the problem is, that when the team is returned, the user list from the manytomany user<->team relation, contains all the users that are part of the team. I need to somehow filter out this to match my role capabilities. So in this case if the user has $capabilities["users"]["read"] = 2, then the team.users should contain only the user making the request, because he has access to list the teams he is in, but he has no permission to view other users than himself.
So my question is, how can add a security voter on relational fields for item-operations and collection-operations.
A rough visual representation of what I want to achieve
/**
* #ORM\ManyToMany(targetEntity="User", mappedBy="teams")
* #Groups({"team.read","form.read"})
* #Security({itemOperations={
* "get"={
* "access_control"="is_granted('user.view', object)",
* "access_control_message"="Access denied."
* },
* "put"={
* "access_control"="is_granted('user.update', object)",
* "access_control_message"="Access denied."
* },
* "delete"={
* "access_control"="is_granted('user.delete', object)",
* "access_control_message"="Access denied."
* },
* },
* collectionOperations={
* "get"={
* "access_control"="is_granted('user.list', object)",
* "access_control_message"="Access denied."
* },
* "post"={
* "access_control"="is_granted('user.create', object)",
* "access_control_message"="Access denied."
* },
* }})
*/
private $users;
I don't think Normalizer is a good solution from a performance perspective, considering that the DB query was already made.
If I understand well, in the end the only problem is that when you make a request GET /api/teams/{id}, the property $users contains all users belonging to the team, but given user's permissions, you just want to display a subset.
Indeed Doctrine Extensions are not enough because they only limits the number of entities of the targeted entity, i.e Team in your case.
But it seems that Doctrine Filters cover this use case; they allow to add extra SQL clauses to your queries, even when fetching associated entities. But I never used them myself so I can't be 100% sure. Seems to be a very low level tool.
Otherwise, I deal with a similar use case on my project, but yet I'm not sure it fit all your needs:
Adding an extra $members array property without any #ORM annotation,
Excluding the $users association property from serialization, replacing it by $members,
Decorating the data provider of the Team entity,
Making the decorated data provider fill the new property with a restricted set of users.
// src/Entity/Team.php
/**
* #ApiResource(
* ...
* )
* #ORM\Entity(repositoryClass=TeamRepository::class)
*/
class Team
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private $id;
/**
* #var User[]
* #ORM\ManyToMany(targetEntity=User::class) //This property is persisted but not serialized
*/
private $users;
/**
* #var User[] //This property is not persisted but serialized
* #Groups({read:team, ...})
*/
private $members = [];
// src/DataProvider/TeamDataProvider.php
class TeamDataProvider implements CollectionDataProviderInterface, ItemDataProviderInterface, RestrictedDataProviderInterface
{
/** #var ItemDataProvider */
private $itemDataProvider;
/** #var CollectionDataProvider*/
private $collectionDataProvider;
/** #var Security */
private $security;
public function __construct(ItemDataProvider $itemDataProvider,
CollectionDataProvider $collectionDataProvider,
Security $security)
{
$this->itemDataProvider = $itemDataProvider;
$this->collectionDataProvider = $collectionDataProvider;
$this->security = $security;
}
public function supports(string $resourceClass, string $operationName = null, array $context = []): bool
{
return $resourceClass === Team::class;
}
public function getCollection(string $resourceClass, string $operationName = null)
{
/** #var Team[] $manyTeams */
$manyTeams = $this->collectionDataProvider->getCollection($resourceClass, $operationName);
foreach ($manyTeams as $team) {
$this->fillMembersDependingUserPermissions($team);
}
return $manyTeams;
}
public function getItem(string $resourceClass, $id, string $operationName = null, array $context = [])
{
/** #var Team|null $team */
$team = $this->itemDataProvider->getItem($resourceClass, ['id' => $id], $operationName, $context);
if ($team !== null) {
$this->fillMembersDependingUserPermissions($team);
}
return $team;
}
private function fillMembersDependingUserPermissions(Team $team): void
{
$currentUser = $this->security->getUser();
if ($currentUser->getCapabilities()['users']['read'] === 2) {
$team->setMembers([$currentUser]);
} elseif ($currentUser->getCapabilities()['users']['read'] === 1) {
$members = $team->getUsers()->getValues();
$team->setMembers($members); //Current user is already within the collection
}
}
}
EDIT AFTER REPLY
The constructor of the TeamDataProvider use concrete classes instead of interfaces because it is meant to decorate precisely ORM data providers. I just forgot that those services use aliases. You need to configure a bit:
# config/services.yaml
App\DataProvider\TeamDataProvider:
arguments:
$itemDataProvider: '#api_platform.doctrine.orm.default.item_data_provider'
$collectionDataProvider: '#api_platform.doctrine.orm.default.collection_data_provider'
This way you keep advantages of your extensions.

How can I have two User that share the same table?

I have an entity User with lots of feature built for it.
/**
* #ORM\Entity(repositoryClass="App\Repository\UserRepository")
* #UniqueEntity("email", message="Email already in use")
* #ORM\HasLifecycleCallbacks
* #Table(name="users")
*/
class User implements UserInterface
{
/* variables + getter & setter */
}
This entity is good as is for most of my User.
However, a few of them will have a special ROLE, ROLE_TEACHER.
With this role, I need to store a lot of new variables specially for them.
If I create a new entity Teacher, doctrine creates a new table with every User's data + the Teacher's data.
/**
* #ORM\Entity(repositoryClass="App\Repository\TeacherRepository")
* #Table(name="teachers")
*/
class Teacher extends User
{
/**
* #ORM\Column(type="string", length=64, nullable=true)
*/
protected $test;
public function __construct()
{
parent::__construct();
}
}
What I want, is for Teacher & User to share the users table and have the teachers table only store the extra data. How could I achieve that ?
This is more of system design problem than implementation problem. as #Gary suggested you can make use of Inheritance Mapping which can have Performance issues, I'd rather suggest re think your schema and make use of database normalization techniques to break up your data into more manageable entities.
You can have User entity :
/**
* #ORM\Entity(repositoryClass="App\Repository\UserRepository")
* #UniqueEntity("email", message="Email already in use")
* #ORM\HasLifecycleCallbacks
* #Table(name="users")
*/
class User implements UserInterface
{
/* variables + getter & setter */
/**
* One user has many attibute data. This is the inverse side.
* #OneToMany(targetEntity="UserData", mappedBy="data")
*/
private $data;
}
With other UserData Entity with OneToMany relationship :
/**
* #ORM\Entity(repositoryClass="App\Repository\UserDataRepository")
* #Table(name="user_data")
*/
class UserData
{
/* variables + getter & setter */
#ORM\Id()
private $id;
/**
* Many features have one product. This is the owning side.
* #ManyToOne(targetEntity="User", inversedBy="data")
* #JoinColumn(name="user_id", referencedColumnName="id")
*/
private $user;
/**
* #ORM\Column(type="string")
*/
private $attribute;
/*
* #ORM\Column(name="value", type="object")
*/
private $value;
}
Now you can have list of user attributes without requiring specific structure to each role. It's scalable and arbitrary.
You can also define same Relation with TeacherData, StudentData or UserProfile Entities with foreign keys and branch your application logic according to the roles. Key is to break data into their separate domains and keep common data in one table. Load related data by querying related entity, this increases readability and makes it easy to break complex structure into manageable codebase.

Understanding Symfony 5 Doctrine ManyToOne relation concepts with Users and Roles

I'm trying to understand Doctrine ORM relationship concepts, because I spent lot of time to create, according to me, a very simple use case, but, I didn't manage to do it because I've got a slack of knowledges...
Here is my issue :
I've got 2 entities, UserApp and RoleApp.
I want to have the possibility to manage roles from users threw a database, instead of hardcoding role, in array format, directly into users table. So to do that I've got a classic foreign key role_id into user_app table which references the primary key into role_app table.
I need, for each user, to get his role, and to bring it in the right format expected for Symfony Authentification :
class UserApp implements UserInterface, \Serializable{
.....
/**
* #var UserRole
* #ORM\ManyToOne(targetEntity="App\Entity\UserRole")
* #ORM\JoinColumn(nullable=false)
*
*/
private $role;
public function getUserRole(): array{ // I know it's wrong but I don't know what is the right type to get
return $this->role->getUserRole($this);
}
/**
* Returns the roles or permissions granted to the user for security.
*/
public function getRoles(): array{
//$role = ["ADMIN_USER"];
$role = $this->getUserRole();
dump($role);
return array_unique($role);
class UserRole{
/**
* #ORM\Column(type="json")
*/
private $appRole = [];
public function getUserRole(UserApp $user): ?array
{
$userRoleRepository = new UserRoleRepository();
$userRole[] = $userRoleRepository->find(1);
dump($userRole);
return $userRole;
}
}
But this code doesn't provide the right format for user's role need for Symfony authentification :(
Finaly I managed to do what I want; here is my solution, I hope it can help...
class UserApp implements UserInterface, \Serializable
{
....
/**
* #var UserRole
* #ORM\ManyToOne(targetEntity="App\Entity\UserRole")
* #ORM\JoinColumn(nullable=false)
*
*/
private $role;
public function getUserRole(): UserRole
{
return $this->role->getRoleUser($this);
}
/**
* Returns the roles or permissions granted to the user for security.
* it's for authentification compliance
*/
public function getRoles(): array
{
$arrRole = $this->getUserRole()->getAppRole();
return array_unique($arrRole);
}
......
class UserRole{
.....
/**
* #ORM\Column(type="json")
*/
private $appRole = [];//For authentification compliance
.....
public function getRoleUser(UserApp $user): ?self
{
return $this;
}`enter code here`

How to query another entity in entity class in symfony2 using doctrine

I'm trying to query using entity manager in a entity class file but I'm getting this error:
FatalErrorException: Error: Call to undefined method Acme\MasoudBundle\Entity\User::getDoctrine() in /var/www/test/src/Acme/MasoudBundle/Entity/User.php line 192
my entity class is :
namespace Acme\MasoudBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\AdvancedUserInterface;
/**
* User
*
* #ORM\Table(name="user")
* #ORM\Entity
*/
class User implements AdvancedUserInterface, \Serializable
{
/**
* Set email
*
* #param string $email
* #return User
*/
public function setEmail($email)
{
$this->email = $email;
return $this;
}
/**
* Get email
*
* #return string
*/
public function getEmail()
{
return $this->email;
}
/**
* Set isActive
*
* #param boolean $isActive
* #return User
*/
public function setIsActive($isActive)
{
$this->isActive = $isActive;
return $this;
}
/**
* Get isActive
*
* #return boolean
*/
public function getIsActive()
{
return $this->isActive;
}
/**
* #inheritDoc
*/
public function getRoles()
{
$em = $this->getDoctrine()->getManager();
$Permission= $em->getRepository('MasoudBundle:Permission')->find(1);
$this->permissions[]=$Permission->permission;
return $this->permissions;
}
}
I want to have a permission and authentication system like this, can you help me please? there are 5 tables, a user table, a group table, a permission table, and a group_permission and a user_group table. so After user logins, I want to check which user is for which group, and get the groups permission. how can I do that? please help me as much as you have time.
Your entity should not know about other entities and the Entity Manager because of the separation of concerns.
Why don't you simply map your User to the appropriate Role(s) (instances of Permission entity in your case) using Doctrine Entity Relationships/Associations. It will allow you to access the appropriate permissions of a given user from the User instance itself.
In this line:
$em = $this->getDoctrine()->getManager();
$this refers to the current class, the User Entity that does not have a method called getDoctrine(). $this->getDoctrine() works in controllers where you extend the Controller class a subclass of ContainerAware which contains the getDoctrine() method.
In other terms, this method works only on objects of class container or its subclasses, like this: $controller->getDoctrine()->getManager().
Besides, you don't want to have an EntityManager inside your entity classes, that's not a good way of doing things. You would better use listners to do such stuffs
I solved this:
global $kernel;
$em = $kernel->getContainer()->get('doctrine')->getManager();
$role = $em->getRepository('BackendBundle:user_types')->findOneBy(array(
'id' => 10
));

Automatically updating created_by in the model

I wanted to have a created_by field for my model, say Product, that is automatically updated and I am using FOSUserBundle and Doctrine2. What is the recommended way of inputting the User id into Product?
Can I do it in the Product model? I am not sure how to do so and any help would be wonderful. Thanks!
I want to do something like this in the model, but I don't know how to get the user id.
/**
* Set updatedBy
*
* #ORM\PrePersist
* #ORM\PreUpdate
* #param integer $updatedBy
*/
public function setUpdatedBy($updatedBy=null)
{
if (is_null($updatedBy)) {
$updatedBy = $user->id;
}
$this->updatedBy = $updatedBy;
}
To relate the user to the product you want to associate the two entities:
http://symfony.com/doc/current/book/doctrine.html#entity-relationships-associations
/**
* #ORM\ManyToOne(targetEntity="User", inversedBy="products")
* #ORM\JoinColumn(name="user_id", referencedColumnName="id")
* You may need to use the full namespace above instead of just User if the
* User entity is not in the same bundle e.g FOS\UserBundle\Entity\User
* the example is just a guess of the top of my head for the fos namespace though
*/
protected $user;
and for the automatic update field you may be after lifecyclecallbacks:
http://symfony.com/doc/current/book/doctrine.html#lifecycle-callbacks
/**
* #ORM\Entity()
* #ORM\HasLifecycleCallbacks()
*/
class Product
{
/**
* #ORM\PreUpdate
*/
public function setCreatedValue()
{
$this->created = new \DateTime();
}
}
EDIT
This discussion talks about getting the container in the entity in which case you could then get the security.context and find the user id from that if you mean to associate the current user to the product they edited:
https://groups.google.com/forum/?fromgroups#!topic/symfony2/6scSB0Kgds0
//once you have the container you can get the session
$user= $this->container->get('security.context')->getToken()->getUser();
$updated_at = $user->getId();
Maybe that is what you are after, not sure it is a good idea to have the container in the entity though, could you not just set the user on the product in the update action in your product controller:
public function updateAction(){
//....
$user= $this->get('security.context')->getToken()->getUser();
$product->setUser($user)
}

Resources