From 01e1f27b686709da6a2d7c5bd077f5c3c47bddbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Mon, 18 Mar 2019 19:05:41 +0100 Subject: [PATCH] Added a simple Voter for checking, if a user is allowed, to view/edit/create a part. --- config/permissions.yaml | 40 +++ .../PermissionsConfiguration.php | 65 ++++ src/Controller/PartController.php | 8 +- src/Entity/Group.php | 13 +- src/Entity/PermissionsEmbed.php | 334 ++++++++++++++++++ src/Entity/StructuralDBElement.php | 9 + src/Entity/User.php | 16 +- .../Interfaces/HasPermissionsInterface.php | 40 +++ src/Security/Voter/PartVoter.php | 62 ++++ src/Services/PermissionResolver.php | 167 +++++++++ 10 files changed, 750 insertions(+), 4 deletions(-) create mode 100644 config/permissions.yaml create mode 100644 src/Configuration/PermissionsConfiguration.php create mode 100644 src/Entity/PermissionsEmbed.php create mode 100644 src/Security/Interfaces/HasPermissionsInterface.php create mode 100644 src/Security/Voter/PartVoter.php create mode 100644 src/Services/PermissionResolver.php diff --git a/config/permissions.yaml b/config/permissions.yaml new file mode 100644 index 00000000..38e83a8e --- /dev/null +++ b/config/permissions.yaml @@ -0,0 +1,40 @@ +# In this file the possible permissions are defined. +# This should be compatible with the legacy Part-DB + +perms: # Here comes a list with all Permission names (they have a perm_[name] coloumn in DB) + + parts: # e.g. this maps to perms_parts in User/Group database + # label: "perm.parts" + operations: # Here are all possible operations are listed => the op name is mapped to bit value + read: + bit: 0 + edit: + # label: "perm.part.edit" + bit: 2 + create: + bit: 4 + move: + bit: 6 + delete: + bit: 8 + search: + bit: 10 + all_parts: + bit: 12 + no_price_parts: + bit: 14 + obsolete_parts: + bit: 16 + unknown_instock_parts: + bit: 18 + change_favorite: + bit: 20 + show_favorite_parts: + bit: 24 + show_last_edit_parts: + bit: 26 + show_users: + bit: 28 + show_history: + bit: 30 + diff --git a/src/Configuration/PermissionsConfiguration.php b/src/Configuration/PermissionsConfiguration.php new file mode 100644 index 00000000..11ab52c9 --- /dev/null +++ b/src/Configuration/PermissionsConfiguration.php @@ -0,0 +1,65 @@ +root('permissions'); + + $rootNode->children() + ->arrayNode('perms') + ->arrayPrototype() + ->children() + ->scalarNode('label')->end() + ->arrayNode('operations') + ->arrayPrototype() + ->children() + ->scalarNode('name')->end() + ->scalarNode('label')->end() + ->scalarNode('bit')->end(); + + return $treeBuilder; + } +} \ No newline at end of file diff --git a/src/Controller/PartController.php b/src/Controller/PartController.php index 8eecccb0..64c4cbc3 100644 --- a/src/Controller/PartController.php +++ b/src/Controller/PartController.php @@ -40,6 +40,7 @@ use App\Services\AttachmentFilenameService; use App\Services\EntityURLGenerator; use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityManagerInterface; +use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Annotation\Route; @@ -54,6 +55,8 @@ class PartController extends AbstractController */ public function show(Part $part, AttachmentFilenameService $attachmentFilenameService) { + $this->denyAccessUnlessGranted('read', $part); + $filename = $part->getMasterPictureFilename(true); return $this->render('show_part_info.html.twig', @@ -72,8 +75,9 @@ class PartController extends AbstractController */ public function edit(Part $part, Request $request, EntityManagerInterface $em) { - $form = $this->createForm(PartType::class, $part); + $this->denyAccessUnlessGranted('edit', $part); + $form = $this->createForm(PartType::class, $part); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { @@ -98,6 +102,8 @@ class PartController extends AbstractController { $new_part = new Part(); + $this->denyAccessUnlessGranted('create', $new_part); + $cid = $request->get('cid', 1); $category = $em->find(Category::class, $cid); diff --git a/src/Entity/Group.php b/src/Entity/Group.php index 8d074ff2..076bd648 100644 --- a/src/Entity/Group.php +++ b/src/Entity/Group.php @@ -31,6 +31,7 @@ namespace App\Entity; +use App\Security\Interfaces\HasPermissionsInterface; use Doctrine\ORM\Mapping as ORM; /** @@ -40,7 +41,7 @@ use Doctrine\ORM\Mapping as ORM; * @ORM\Entity() * @ORM\Table("groups") */ -class Group extends StructuralDBElement +class Group extends StructuralDBElement implements HasPermissionsInterface { /** @@ -59,6 +60,11 @@ class Group extends StructuralDBElement */ protected $users; + /** @var PermissionsEmbed + * @ORM\Embedded(class="PermissionsEmbed", columnPrefix="perms_") + */ + protected $permissions; + /** * Returns the ID as an string, defined by the element class. @@ -69,4 +75,9 @@ class Group extends StructuralDBElement { return 'G' . sprintf('%06d', $this->getID()); } + + public function getPermissions(): PermissionsEmbed + { + return $this->permissions; + } } \ No newline at end of file diff --git a/src/Entity/PermissionsEmbed.php b/src/Entity/PermissionsEmbed.php new file mode 100644 index 00000000..6ad0bd70 --- /dev/null +++ b/src/Entity/PermissionsEmbed.php @@ -0,0 +1,334 @@ +$permission_name; + + return static::readBitPair($perm_int, $bit_n); + } + + /** + * Returns the value of the operation for the given permission. + * @param string $permission_name The name of the permission, for which the operation should be returned. + * @param int $bit_n The (lower) bit number of the bit pair for the operation. + * @return bool|null The value of the operation. True, if the given operation is allowed, false if disallowed + * and null if it should inherit from parent. + */ + public function getPermissionValue(string $permission_name, int $bit_n) : ?bool + { + $value = $this->getBitValue($permission_name, $bit_n); + if($value == self::ALLOW) { + return true; + } elseif($value == self::DISALLOW) { + return false; + } else { + return null; + } + } + + /** + * Reads a bit pair from $data. + * @param $data int The data from where the bits should be extracted from. + * @param $n int The number of the lower bit (of the pair) that should be read. Starting from zero. + * @return int The value of the bit pair. + */ + final protected static function readBitPair(int $data, int $n): int + { + Assert::lessThanEq($n,31, '$n must be smaller than 32, because only a 32bit int is used! Got %s.'); + if ($n % 2 !== 0) { + throw new \InvalidArgumentException('$n must be dividable by 2, because we address bit pairs here!'); + } + + $mask = 0b11 << $n; //Create a mask for the data + return ($data & $mask) >> $n; //Apply mask and shift back + } + + /** + * Writes a bit pair in the given $data and returns it. + * @param $data int The data which should be modified. + * @param $n int The number of the lower bit of the pair which should be written. + * @param $new int The new value of the pair. + * @return int The new data with the modified pair. + */ + final protected static function writeBitPair(int $data, int $n, int $new) : int + { + Assert::lessThanEq($n,31, '$n must be smaller than 32, because only a 32bit int is used! Got %s.'); + Assert::lessThanEq($new, 3, '$new must be smaller than 3, because a bit pair is written! Got %s.'); + + if ($n % 2 !== 0) { + throw new \InvalidArgumentException('$n must be dividable by 2, because we address bit pairs here!'); + } + + $mask = 0b11 << $n; //Mask all bits that should be writen + $newval = $new << $n; //The new value. + $data = ($data & ~$mask) | ($newval & $mask); + return $data; + } + +} \ No newline at end of file diff --git a/src/Entity/StructuralDBElement.php b/src/Entity/StructuralDBElement.php index 8e5be02a..c59fa1fc 100644 --- a/src/Entity/StructuralDBElement.php +++ b/src/Entity/StructuralDBElement.php @@ -131,6 +131,15 @@ abstract class StructuralDBElement extends AttachmentContainingDBElement return $this->parent_id ?? self::ID_ROOT_ELEMENT; //Null means root element } + /** + * Get the parent of this element. + * @return StructuralDBElement|null The parent element. Null if this element, does not have a parent. + */ + public function getParent() : ?self + { + return $this->parent; + } + /** * Get the comment of the element. * diff --git a/src/Entity/User.php b/src/Entity/User.php index 724b60f7..11915d6c 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -32,6 +32,8 @@ namespace App\Entity; +use App\Entity\Embeddables\PermissionEntity; +use App\Security\Interfaces\HasPermissionsInterface; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Validator\Constraints as Assert; @@ -43,7 +45,7 @@ use Symfony\Component\Validator\Constraints as Assert; * @ORM\Entity(repositoryClass="App\Repository\UserRepository") * @ORM\Table("users") */ -class User extends NamedDBElement implements UserInterface +class User extends NamedDBElement implements UserInterface, HasPermissionsInterface { /** * @ORM\Id() @@ -115,11 +117,16 @@ class User extends NamedDBElement implements UserInterface /** * @var Group|null The group this user belongs to. - * @ORM\ManyToOne(targetEntity="Group", inversedBy="users") + * @ORM\ManyToOne(targetEntity="Group", inversedBy="users", fetch="EAGER") * @ORM\JoinColumn(name="group_id", referencedColumnName="id") */ protected $group; + /** @var PermissionsEmbed + * @ORM\Embedded(class="PermissionsEmbed", columnPrefix="perms_") + */ + protected $permissions; + /** * A visual identifier that represents this user. @@ -200,6 +207,11 @@ class User extends NamedDBElement implements UserInterface } + public function getPermissions() : PermissionsEmbed + { + return $this->permissions; + } + /************************************************ * Getters ************************************************/ diff --git a/src/Security/Interfaces/HasPermissionsInterface.php b/src/Security/Interfaces/HasPermissionsInterface.php new file mode 100644 index 00000000..32bc61a5 --- /dev/null +++ b/src/Security/Interfaces/HasPermissionsInterface.php @@ -0,0 +1,40 @@ +resolver = $resolver; + } + + protected function supports($attribute, $subject) + { + // replace with your own logic + // https://symfony.com/doc/current/security/voters.html + //return ($subject instanceof Part || in_array($subject, ['PERM_parts', 'PERM_parts_name'])); + + if ($subject instanceof Part) + { + return in_array($attribute, $this->resolver->listOperationsForPermission('parts'), false); + } + + return false; + } + + protected function voteOnAttribute($attribute, $subject, TokenInterface $token) + { + $user = $token->getUser(); + // if the user is anonymous, do not grant access + if (!$user instanceof User) { + return false; + } + + if($subject instanceof Part) { + //Null concealing operator means, that no + return $this->resolver->inherit($user, 'parts', $attribute) ?? false; + } + + //Deny access by default. + return false; + } +} diff --git a/src/Services/PermissionResolver.php b/src/Services/PermissionResolver.php new file mode 100644 index 00000000..d38879f1 --- /dev/null +++ b/src/Services/PermissionResolver.php @@ -0,0 +1,167 @@ +processConfiguration( + $databaseConfiguration, + $configs + ); + + $this->permission_structure = $processedConfiguration; + } + + + /** + * Check if a user/group is allowed to do the specified operation for the permission. + * + * See permissions.yaml for valid permission operation combinations. + * + * @param HasPermissionsInterface $user The user/group for which the operation should be checked. + * @param string $permission The name of the permission for which should be checked. + * @param string $operation The name of the operation for which should be checked. + * @return bool|null True, if the user is allowed to do the operation (ALLOW), false if not (DISALLOW), and null, + * if the value is set to inherit. + */ + public function dontInherit(HasPermissionsInterface $user, string $permission, string $operation) : ?bool + { + //Get the permissions from the user + $perm_list = $user->getPermissions(); + + //Determine bit number using our configuration + $bit = $this->permission_structure['perms'][$permission]['operations'][$operation]['bit']; + + return $perm_list->getPermissionValue($permission, $bit); + } + + + /** + * Checks if a user is allowed to do the specified operation for the permission. + * In contrast to dontInherit() it tries to resolve the inherit values, of the user, by going upwards in the + * hierachy (user -> group -> parent group -> so on). But even in this case it is possible, that the inherit value + * could be resolved, and this function returns null. + * + * In that case the voter should set it manually to false by using ?? false. + * + * @param User $user The user for which the operation should be checked. + * @param string $permission The name of the permission for which should be checked. + * @param string $operation The name of the operation for which should be checked. + * @return bool|null True, if the user is allowed to do the operation (ALLOW), false if not (DISALLOW), and null, + * if the value is set to inherit. + */ + public function inherit(User $user, string $permission, string $operation) : ?bool + { + //Check if we need to inherit + $allowed = $this->dontInherit($user, $permission, $operation); + + if ($allowed !== null) { + //Just return the value of the user. + return $allowed; + } + + $parent = $user->getGroup(); + while($parent != null){ //The top group, has parent == null + //Check if our current element gives a info about disallow/allow + $allowed = $this->dontInherit($parent, $permission, $operation); + if ($allowed !== null) { + return $allowed; + } + //Else go up in the hierachy. + $parent = $parent->getParent(); + } + + return null; //The inherited value is never resolved. Should be treat as false, in Voters. + } + + + /** + * Lists the names of all operations that is supported for the given permission. + * + * If the Permission is not existing at all, a exception is thrown. + * + * This function is useful for the support() function of the voters. + * + * @param string $permission The permission for which the + * @return string[] A list of all operations that are supported by the given + */ + public function listOperationsForPermission(string $permission) : array + { + $operations = $this->permission_structure['perms'][$permission]['operations']; + + return array_keys($operations); + } + + /** + * Checks if the permission with the given name is existing. + * + * @param string $permission The name of the permission which we want to check. + * @return bool True if a perm with that name is existing. False if not. + */ + public function isValidPermission(string $permission) : bool + { + return isset($this->permission_structure['perms'][$permission]); + } + + +} \ No newline at end of file