I have a Symfony application with five different entities (what they are doesn't really matter).
For each of these entities, a registered user must either have NONE, READ, EDIT, DELETE permissions. The sticky part for me to grasp is that each user can have different permissions for each entity; User A can edit Entity A, but can only view Entity B, etc.
Now on each user's options page, an admin should be able to see his permissions for each form. Radio buttons should be displayed with the four options for each form. Something like:
Entity A: O NONE O READ X EDIT O DELETE
Entity B: O NONE X READ O EDIT O DELETE
...
I know my choices are basically between creating some type of Voter system or an Access Control List.
At first I just started by listing all of the roles currently in the system within my UserType:
$builder
...
->add('roles', 'choice', array(
'choices' => $this->roles,
'choices_as_values' => true,
'label' => 'Roles',
'expanded' => true,
'multiple' => true,
'mapped' => true,
))
;
but I'm feeling like this isn't going to be very effective in the long run. And either way, this also displays other system roles that have nothing to do with access control to specific entities (such as ROLE_USER, ROLE_ADMIN, etc.)
I'm not looking for a complete solution or anything like that, I'm just having a really hard time getting started and seeing the big picture on how to make this happen. (And yes, I am aware of the Symfony documentation...sometimes that stuff just doesn't make a ton of sense at first).
PROGRESS UPDATE
I decided on Access Control List.
First, when a new entity is created, I use the standard ACL creation strategy as mentioned in the Symfony Documentation:
public function postAvrequestAction(Request $request){
$entity = new AvRequest();
$form = $this->get('form.factory')->createNamed('', new AvRequestType(), $entity);
$form->handleRequest($request);
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($entity);
$em->flush();
$serializer = $this->get('serializer');
$serialized = $serializer->serialize($entity, 'json');
// creating the ACL
$aclProvider = $this->get('security.acl.provider');
$objectIdentity = ObjectIdentity::fromDomainObject($entity);
$acl = $aclProvider->createAcl($objectIdentity);
// retrieving the security identity of the currently logged-in user
$tokenStorage = $this->get('security.token_storage');
$users = $em->getRepository('AppBundle:User')->findAll();
//$tokenStorage->getToken()->getUser();
foreach($users as $user){
$securityIdentity = UserSecurityIdentity::fromAccount($user);
// grant owner access based on owner's overall permissions for this type of entity
$acl->insertObjectAce($securityIdentity, 0);
$aclProvider->updateAcl($acl);
}
return new Response($serialized, 201);
}
return new JsonResponse(array(
'errors' => $this->getFormErrors($form)
));
}
Next, I created a service with all of the necessary dependencies to update a user's permissions for each entity:
#services.yml
services:
user_service:
class: AppBundle\Resources\Services\UserService
arguments: [ #doctrine.orm.entity_manager, #service_container, #security.authorization_checker, #security.acl.provider ]
The service has the function:
/**
* ACLs grant user permission on every instance of each entity.
* In order to edit permissions across all of these entites for each user,
* first iterate over all entities.
* For each entity, update the permission for the specified user.
*
* #param \AppBundle\Entity\User $user The user object whose permissions should be updated
* #param String $entity The entity whose permissions should be updated (e.g. 'AppBundle:AvRequest')
* #param int $permission The bitmask value of the permission level (e.g. MaskBuilder::MASK_VIEW (=4))
*
* #return null
*/
public function editPermission(User $user, $entity, $permission){
$allEntities = $this->em->getRepository($entity)->findAll();
foreach($allEntities as $oneEntity){
// locate the ACL
$objectIdentity = ObjectIdentity::fromDomainObject($oneEntity);
$acl = $this->aclProvider->findAcl($objectIdentity);
// update user access
$objectAces = $acl->getObjectAces();
foreach($objectAces as $i => $ace) {
$acl->updateObjectAce($i, $permission);
}
}
}
This function goes through every instance of the entity and gives it the same permission level for the specified user.
The next step that I haven't quite figured out yet is setting a master permission level for a user on an entity as described up top with my radio buttons. I need to be able to go to the user's profile page, see a radio list of the user's permissions for each entity type, submit the radio button value and then run the editPermission() function on save.
You are looking for Access Control Lists. It is easy to set permission by user or group of users.
Add access level by user:
$builder = new MaskBuilder();
$builder
->add('view')
->add('edit')
->add('delete')
->add('undelete')
;
$mask = $builder->get(); // int(29)
$identity = new UserSecurityIdentity('johannes', 'AppBundle\Entity\User');
$acl->insertObjectAce($identity, $mask);
Specify min access level by entity:
public function addCommentAction(Post $post)
{
$comment = new Comment();
// ... setup $form, and submit data
if ($form->isValid()) {
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($comment);
$entityManager->flush();
// creating the ACL
$aclProvider = $this->get('security.acl.provider');
$objectIdentity = ObjectIdentity::fromDomainObject($comment);
$acl = $aclProvider->createAcl($objectIdentity);
// retrieving the security identity of the currently logged-in user
$tokenStorage = $this->get('security.token_storage');
$user = $tokenStorage->getToken()->getUser();
$securityIdentity = UserSecurityIdentity::fromAccount($user);
// grant owner access
$acl->insertObjectAce($securityIdentity, MaskBuilder::MASK_OWNER);
$aclProvider->updateAcl($acl);
}
}
public function editCommentAction(Comment $comment)
{
$authorizationChecker = $this->get('security.authorization_checker');
// check for edit access
if (false === $authorizationChecker->isGranted('EDIT', $comment)) {
throw new AccessDeniedException();
}
// ... retrieve actual comment object, and do your editing here
}
Related
I would like to pass the authenticated users list of roles to my front end apps, so I can use the same access control structure in the front and back end.
I was looking in the security / authentication classes as that is where the isGranted function are for me to do this
$this->container->get('security.context')->isGranted('ROLE_SUPER_ADMIN')
I can't find anything to get a list of roles though, is this not a supported feature?
nb: I don't want the entire role hierarchy, just the list of roles for the authenticated user
I ended up adding a new repository function and a service method to get this info.
MyProject/UserBundle/Entity/Repository/UserRepository
public function getRoles($userId)
{
$queryBuilder = $this->createQueryBuilder('u');
$queryBuilder
->select('u.id, u.roles AS user_roles, g.roles AS group_roles')
->leftJoin('u.groups', 'g')
->andWhere('u.id = :user_id')
->setParameter('user_id', $userId);
return $queryBuilder->getQuery()->getArrayResult();
}
MyProject/UserBundle/Service/UserService
public function getUserRoles($user)
{
$groupRoles = $this->repository->getRoles($user->getId());
$roles = array('user_roles' => array(), 'group_roles' => array());
foreach ($groupRoles as $groupRole) {
$roles['user_roles'] = array_merge($roles['user_roles'], $groupRole['user_roles']);
$roles['group_roles'] = array_merge($roles['group_roles'], $groupRole['group_roles']);
}
return $roles;
}
This gives me an array like this
"roles":{
"user_roles":[],
"group_roles":["ROLE_ADMIN","ROLE_ONE","ROLE_TWO","ROLE_BEST"]
}
Assuming you're using the Symfony security component, the user interface which your user class implements has this already included:
$user = $this->get('security.token_storage')->getToken()->getUser();
var_dump($user->getRoles());
http://api.symfony.com/3.1/Symfony/Component/Security/Core/User/UserInterface.html#method_getRoles
I am trying to add error to form using FormError. Error must be displayed when user tries to create collection with existing name. But this code doesn't work, and I can't understand why
public function submitInObjectAction(Request $request)
{
$collection = new Collection();
$user = $this->getUser();
$form = $this->createForm(
new CollectionType(),
$collection
);
$form->handleRequest($request);
if ($form->isValid() && $form->isSubmitted()) {
$colname = $form["name"]->getData();
$existing = $this->getDoctrine()->getRepository('CollectionBundle:Collection')
->findBy(['name' => $colname, 'user' => $user]);
if ($existing != NULL) {
$error = new FormError("You already have collection with such name");
$form->get('name')->addError($error);
}
$em = $this->getDoctrine()->getManager();
$collection->setUser($user);
$em->persist($collection);
$em->flush();
return new JsonResponse([
'id' => $collection->getId(),
'name' => $collection->getName()
]);
}
}
I cannot use annotation on name field in Collection entity, because names must be unique only for particular user
I think it is too late in the chain. Form validation happens when you call $form->handleRequest() and by the time $form->isValid() is called your validation should be complete. It is better to add validation constraints further up the chain. See the Symfony guide on form validation and if necessary the validation component, for more info.
I would use annotations to set a unique constraint on the name field of the Collection entity in the CollectionBundle.
This not only validates this user input form, but any other form or component or bundle which uses CollectionBundle - and Doctrine will even prevent storage depending on the constraint leaving your database tidy!
EDIT: Another option for more advanced validation is writing a custom form event listener. Three events are dispatched when Form::handleRequest() or Form::submit() are called: FormEvents::PRE_SUBMIT, FormEvents::SUBMIT, FormEvents::POST_SUBMIT. This example also shows how to access the form itself.
$form = $formFactory->createBuilder()
->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
$user = $event->getData();
$form = $event->getForm();
// .. validation here
})
->getForm();
I have to get the user's plain password for LDAP authentification and then retrieve LDAP user informations in the Active Directory with Symfony2.
/**
* #Route("/infos-profil/{id}", name="infos_profil")
* #Template()
*/
public function infosProfilAction($id)
{
$em = $this->getDoctrine()->getManager();
$user = $em->getRepository('MyUserBundle:LdapUser')->find($id); // Find User Entity
if (!$user) {
throw $this->createNotFoundException('Unable to find LdapUser entity.');
}
$login = $user->getUsername(); // Login
$pass = $user->getPlainPassword(); // Password
$ds = ldap_connect("12.34.56.789"); // Domain connexion
if ($ds) {
$r = ldap_bind($ds, $login, $pass); // LDAP User connexion
if ($r) {
$filter = "(&(objectClass=user)(samaccountname=".$login.")(cn=*))";
$sr=ldap_search($ds, "ou=DOMAIN, ou=Test, ou=Users, dc=ats, dc=lan", $filter);
$info = ldap_get_entries($ds, $sr); // Retrieve user's Active Direcory informations
}
}
return array(
'user' => $user,
'info' => $info,
}
But it doesn't work, $pass is empty. When I put the plain password manually in the ldap_bind() function it works perfectly, I just have to get the plain password ! ...
Is it possible ?
It isn't possible to retrieve plain password from database for obvious security reasons.
For your problem, you should create a custom Authentication Provider, following this tutorial : https://symfony.com/doc/4.4/security/custom_authentication_provider.html
This way, your provider will get the plain password from the login form and you will be able to send it to your LDAP server.
You don't need the user's password to retrieve information about them from active directory. Once they are authenticated simply look them up via LDAP using their username, with either an anonymous connection or failing that, a known privileged account.
Can I set permissions to allow authenticated users to add nodes of a custom type? I need to do that in module i'm trying to create. As I can see hook_permission it's actually just for creating new permissions.
If your module name is say mymodule, then defien a your "create node" permission in hook_permission. Then implement hook_node_access to check and return permission for the content type you have implemented.
SAMPLE CODE.
NB: It won't work out of the box, you got to replace your module name, permission name and the content type name to get it to work. And don't forget to clear your cache, TWICE!.
/**
* Implements hook_permission().
*
*/
function mymodule_permission() {
// define your add permission.
// Naming of "array key" is important. We use that later.
return array(
'YOUR CONTENT NAME: add' => array(
'title' => t('Add Project Management Team'),
),
);
}
/**
* Implements hook_node_access().
*/
function mymodule_node_access($node, $op, $account = NULL) {
$type = is_string($node) ? $node : $node->type;
// make sure you are responding to content type defined by your module only.
if ($type == 'YOUR_CONTENT_TYPE_NAME_HERE') {
// If no account is specified, assume that the check is against the current logged in user
if (is_null($account)) {
global $user;
$account = $user;
}
if ($op == 'create' AND user_access('YOUR CONTENT NAME: add', $account)) {
return NODE_ACCESS_ALLOW;
}
}
return NODE_ACCESS_IGNORE;
}
References:
hook_permission().
hook_node_access().
I'm using Symfony Security/ACL component to check permissions of a group of "random" users for a given domain object.
$article = ...; // domain object
$users = ...; // array of users
$oid = ObjectIdentity::fromDomainObject($article);
$sids = array();
for ($users as $user) {
$sids[] = UserSecurityIdentity::fromAccount($user);
}
$aclProvider = ...; // "security.acl.provider" service
$acl = $aclProvider->findAcl($oid, $sids);
However, I'm having trouble checking whether given permission is granted for a given user. How can I do that?
I think you should use Acl Voter:
http://symfony.com/doc/current/cookbook/security/acl.html#checking-access