diff --git a/config/services.yaml b/config/services.yaml index 449519b8..dd2da97f 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -127,10 +127,6 @@ services: # Security #################################################################################################################### - App\Security\EntityListeners\ElementPermissionListener: - tags: - - { name: "doctrine.orm.entity_listener" } - #################################################################################################################### # Cache #################################################################################################################### diff --git a/src/Entity/Base/AbstractDBElement.php b/src/Entity/Base/AbstractDBElement.php index b3cadb25..000e912d 100644 --- a/src/Entity/Base/AbstractDBElement.php +++ b/src/Entity/Base/AbstractDBElement.php @@ -37,8 +37,6 @@ use Symfony\Component\Serializer\Annotation\Groups; * * @ORM\MappedSuperclass(repositoryClass="App\Repository\DBElementRepository") * - * @ORM\EntityListeners({"App\Security\EntityListeners\ElementPermissionListener"}) - * * @DiscriminatorMap(typeProperty="type", mapping={ * "attachment_type" = "App\Entity\AttachmentType", * "attachment" = "App\Entity\Attachment", diff --git a/src/Entity/Base/AbstractStructuralDBElement.php b/src/Entity/Base/AbstractStructuralDBElement.php index 504d730c..7eb4e82e 100644 --- a/src/Entity/Base/AbstractStructuralDBElement.php +++ b/src/Entity/Base/AbstractStructuralDBElement.php @@ -45,7 +45,7 @@ use Symfony\Component\Serializer\Annotation\Groups; * * @ORM\MappedSuperclass(repositoryClass="App\Repository\StructuralDBElementRepository") * - * @ORM\EntityListeners({"App\Security\EntityListeners\ElementPermissionListener", "App\EntityListeners\TreeCacheInvalidationListener"}) + * @ORM\EntityListeners({"App\EntityListeners\TreeCacheInvalidationListener"}) * * @UniqueEntity(fields={"name", "parent"}, ignoreNull=false, message="structural.entity.unique_name") */ diff --git a/src/Entity/Parts/Part.php b/src/Entity/Parts/Part.php index bfd30ee7..a48ca206 100644 --- a/src/Entity/Parts/Part.php +++ b/src/Entity/Parts/Part.php @@ -61,7 +61,6 @@ use App\Entity\Parts\PartTraits\BasicPropertyTrait; use App\Entity\Parts\PartTraits\InstockTrait; use App\Entity\Parts\PartTraits\ManufacturerTrait; use App\Entity\Parts\PartTraits\OrderTrait; -use App\Security\Annotations\ColumnSecurity; use DateTime; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; @@ -100,7 +99,6 @@ class Part extends AttachmentContainingDBElement protected $parameters; /** - * @ColumnSecurity(type="datetime") * @ORM\Column(type="datetime", name="datetime_added", options={"default"="CURRENT_TIMESTAMP"}) */ protected ?DateTime $addedDate = null; @@ -113,14 +111,12 @@ class Part extends AttachmentContainingDBElement /** * @var string The name of this part * @ORM\Column(type="string") - * @ColumnSecurity(prefix="name") */ protected string $name = ''; /** * @var Collection * @ORM\OneToMany(targetEntity="App\Entity\Attachments\PartAttachment", mappedBy="element", cascade={"persist", "remove"}, orphanRemoval=true) - * @ColumnSecurity(type="collection", prefix="attachments") * @ORM\OrderBy({"name" = "ASC"}) * @Assert\Valid() */ @@ -128,7 +124,6 @@ class Part extends AttachmentContainingDBElement /** * @var DateTime the date when this element was modified the last time - * @ColumnSecurity(type="datetime") * @ORM\Column(type="datetime", name="last_modified", options={"default"="CURRENT_TIMESTAMP"}) */ protected ?DateTime $lastModified = null; diff --git a/src/Entity/Parts/PartTraits/AdvancedPropertyTrait.php b/src/Entity/Parts/PartTraits/AdvancedPropertyTrait.php index 2413490c..5113225e 100644 --- a/src/Entity/Parts/PartTraits/AdvancedPropertyTrait.php +++ b/src/Entity/Parts/PartTraits/AdvancedPropertyTrait.php @@ -43,7 +43,6 @@ declare(strict_types=1); namespace App\Entity\Parts\PartTraits; use App\Entity\Parts\Part; -use App\Security\Annotations\ColumnSecurity; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Validator\Constraints as Assert; @@ -55,21 +54,18 @@ trait AdvancedPropertyTrait /** * @var bool Determines if this part entry needs review (for example, because it is work in progress) * @ORM\Column(type="boolean") - * @ColumnSecurity(type="boolean") */ protected bool $needs_review = false; /** * @var string a comma separated list of tags, associated with the part * @ORM\Column(type="text") - * @ColumnSecurity(type="string", prefix="tags", placeholder="") */ protected string $tags = ''; /** * @var float|null how much a single part unit weighs in grams * @ORM\Column(type="float", nullable=true) - * @ColumnSecurity(type="float", placeholder=null) * @Assert\PositiveOrZero() */ protected ?float $mass = null; diff --git a/src/Entity/Parts/PartTraits/BasicPropertyTrait.php b/src/Entity/Parts/PartTraits/BasicPropertyTrait.php index f43b6f7a..0e2f1f08 100644 --- a/src/Entity/Parts/PartTraits/BasicPropertyTrait.php +++ b/src/Entity/Parts/PartTraits/BasicPropertyTrait.php @@ -44,7 +44,6 @@ namespace App\Entity\Parts\PartTraits; use App\Entity\Parts\Category; use App\Entity\Parts\Footprint; -use App\Security\Annotations\ColumnSecurity; use App\Validator\Constraints\Selectable; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Validator\Constraints as Assert; @@ -54,14 +53,12 @@ trait BasicPropertyTrait /** * @var string A text describing what this part does * @ORM\Column(type="text") - * @ColumnSecurity(prefix="description") */ protected string $description = ''; /** * @var string A comment/note related to this part * @ORM\Column(type="text") - * @ColumnSecurity(prefix="comment") */ protected string $comment = ''; @@ -74,7 +71,6 @@ trait BasicPropertyTrait /** * @var bool true, if the part is marked as favorite * @ORM\Column(type="boolean") - * @ColumnSecurity(type="boolean") */ protected bool $favorite = false; @@ -83,7 +79,6 @@ trait BasicPropertyTrait * Every part must have a category. * @ORM\ManyToOne(targetEntity="Category") * @ORM\JoinColumn(name="id_category", referencedColumnName="id", nullable=false) - * @ColumnSecurity(prefix="category", type="App\Entity\Parts\Category") * @Selectable() * @Assert\NotNull(message="validator.select_valid_category") */ @@ -93,7 +88,6 @@ trait BasicPropertyTrait * @var Footprint|null The footprint of this part (e.g. DIP8) * @ORM\ManyToOne(targetEntity="Footprint") * @ORM\JoinColumn(name="id_footprint", referencedColumnName="id") - * @ColumnSecurity(prefix="footprint", type="App\Entity\Parts\Footprint") * @Selectable() */ protected ?Footprint $footprint = null; diff --git a/src/Entity/Parts/PartTraits/InstockTrait.php b/src/Entity/Parts/PartTraits/InstockTrait.php index deca8424..f2d80559 100644 --- a/src/Entity/Parts/PartTraits/InstockTrait.php +++ b/src/Entity/Parts/PartTraits/InstockTrait.php @@ -44,7 +44,6 @@ namespace App\Entity\Parts\PartTraits; use App\Entity\Parts\MeasurementUnit; use App\Entity\Parts\PartLot; -use App\Security\Annotations\ColumnSecurity; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Validator\Constraints as Assert; @@ -58,7 +57,6 @@ trait InstockTrait * @var Collection|PartLot[] A list of part lots where this part is stored * @ORM\OneToMany(targetEntity="PartLot", mappedBy="part", cascade={"persist", "remove"}, orphanRemoval=true) * @Assert\Valid() - * @ColumnSecurity(type="collection", prefix="lots") * @ORM\OrderBy({"amount" = "DESC"}) */ protected $partLots; @@ -68,7 +66,6 @@ trait InstockTrait * Given in the partUnit. * @ORM\Column(type="float") * @Assert\PositiveOrZero() - * @ColumnSecurity(prefix="minamount", type="integer") */ protected float $minamount = 0; @@ -76,7 +73,6 @@ trait InstockTrait * @var ?MeasurementUnit the unit in which the part's amount is measured * @ORM\ManyToOne(targetEntity="MeasurementUnit") * @ORM\JoinColumn(name="id_part_unit", referencedColumnName="id", nullable=true) - * @ColumnSecurity(type="object", prefix="unit") */ protected ?MeasurementUnit $partUnit = null; diff --git a/src/Entity/Parts/PartTraits/ManufacturerTrait.php b/src/Entity/Parts/PartTraits/ManufacturerTrait.php index 669867a3..e00826b1 100644 --- a/src/Entity/Parts/PartTraits/ManufacturerTrait.php +++ b/src/Entity/Parts/PartTraits/ManufacturerTrait.php @@ -44,7 +44,6 @@ namespace App\Entity\Parts\PartTraits; use App\Entity\Parts\Manufacturer; use App\Entity\Parts\Part; -use App\Security\Annotations\ColumnSecurity; use App\Validator\Constraints\Selectable; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Validator\Constraints as Assert; @@ -58,7 +57,6 @@ trait ManufacturerTrait * @var Manufacturer|null The manufacturer of this part * @ORM\ManyToOne(targetEntity="Manufacturer") * @ORM\JoinColumn(name="id_manufacturer", referencedColumnName="id") - * @ColumnSecurity(prefix="manufacturer", type="App\Entity\Parts\Manufacturer") * @Selectable() */ protected ?Manufacturer $manufacturer = null; @@ -67,14 +65,12 @@ trait ManufacturerTrait * @var string the url to the part on the manufacturer's homepage * @ORM\Column(type="string") * @Assert\Url() - * @ColumnSecurity(prefix="mpn", type="string", placeholder="") */ protected string $manufacturer_product_url = ''; /** * @var string The product number used by the manufacturer. If this is set to "", the name field is used. * @ORM\Column(type="string") - * @ColumnSecurity(prefix="mpn", type="string", placeholder="") */ protected string $manufacturer_product_number = ''; @@ -82,7 +78,6 @@ trait ManufacturerTrait * @var string The production status of this part. Can be one of the specified ones. * @ORM\Column(type="string", length=255, nullable=true) * @Assert\Choice({"announced", "active", "nrfnd", "eol", "discontinued", ""}) - * @ColumnSecurity(type="string", prefix="status", placeholder="") */ protected string $manufacturing_status = ''; diff --git a/src/Entity/Parts/PartTraits/OrderTrait.php b/src/Entity/Parts/PartTraits/OrderTrait.php index e0ee8744..c1d24cbc 100644 --- a/src/Entity/Parts/PartTraits/OrderTrait.php +++ b/src/Entity/Parts/PartTraits/OrderTrait.php @@ -43,7 +43,6 @@ declare(strict_types=1); namespace App\Entity\Parts\PartTraits; use App\Entity\PriceInformations\Orderdetail; -use App\Security\Annotations\ColumnSecurity; use Doctrine\Common\Collections\ArrayCollection; use Symfony\Component\Validator\Constraints as Assert; use function count; @@ -59,7 +58,6 @@ trait OrderTrait * @var Orderdetail[]|Collection the details about how and where you can order this part * @ORM\OneToMany(targetEntity="App\Entity\PriceInformations\Orderdetail", mappedBy="part", cascade={"persist", "remove"}, orphanRemoval=true) * @Assert\Valid() - * @ColumnSecurity(prefix="orderdetails", type="collection") * @ORM\OrderBy({"supplierpartnr" = "ASC"}) */ protected $orderdetails; @@ -67,14 +65,12 @@ trait OrderTrait /** * @var int * @ORM\Column(type="integer") - * @ColumnSecurity(prefix="order", type="integer") */ protected int $order_quantity = 0; /** * @var bool * @ORM\Column(type="boolean") - * @ColumnSecurity(prefix="order", type="boolean") */ protected bool $manual_order = false; @@ -82,8 +78,6 @@ trait OrderTrait * @var Orderdetail * @ORM\OneToOne(targetEntity="App\Entity\PriceInformations\Orderdetail") * @ORM\JoinColumn(name="order_orderdetails_id", referencedColumnName="id") - * - * @ColumnSecurity(prefix="order", type="object") */ protected Orderdetail $order_orderdetail; diff --git a/src/Security/Annotations/ColumnSecurity.php b/src/Security/Annotations/ColumnSecurity.php deleted file mode 100644 index dcc8881b..00000000 --- a/src/Security/Annotations/ColumnSecurity.php +++ /dev/null @@ -1,148 +0,0 @@ -. - */ - -declare(strict_types=1); - -/** - * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony). - * - * Copyright (C) 2019 Jan Böhmer (https://github.com/jbtronics) - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 2 - * of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA - */ - -namespace App\Security\Annotations; - -use App\Entity\Base\AbstractNamedDBElement; -use DateTime; -use Doctrine\Common\Annotations\Annotation; -use Doctrine\Common\Collections\ArrayCollection; -use InvalidArgumentException; -use function is_string; - -/** - * @Annotation - * - * @Annotation\Target("PROPERTY") - * - * With these annotation you can restrict the access to certain coloumns in entities. - * The entity which should use this class has to use ElementListener as EntityListener. - */ -class ColumnSecurity -{ - /** - * @var string The name of the edit permission - */ - public string $edit = 'edit'; - /** - * @var string The name of the read permission - */ - public string $read = 'read'; - - /** - * @var string A prefix for all permission names (e.g..edit, useful for Parts) - */ - public string $prefix = ''; - - /** - * @var mixed the placeholder that should be used, when the access to the property is denied - */ - public $placeholder = null; - - public $subject = null; - - /** - * @var string The name of the property. This is used to determine the default placeholder. - * @Annotation\Enum({"integer", "string", "object", "boolean", "datetime", "collection"}) - */ - public $type = 'string'; - - public function getReadOperationName(): string - { - if ('' !== $this->prefix) { - return $this->prefix.'.'.$this->read; - } - - return $this->read; - } - - public function getEditOperationName(): string - { - if ('' !== $this->prefix) { - return $this->prefix.'.'.$this->edit; - } - - return $this->edit; - } - - public function getPlaceholder() - { - //Check if a class name was specified - if (class_exists($this->type)) { - $object = new $this->type(); - if ($object instanceof AbstractNamedDBElement) { - if (is_string($this->placeholder) && '' !== $this->placeholder) { - $object->setName($this->placeholder); - } else { - $object->setName('???'); - } - } - - return $object; - } - - if (null === $this->placeholder) { - switch ($this->type) { - case 'integer': - case 'int': - return 0; - case 'float': - return 0.0; - case 'string': - return '???'; - case 'object': - return null; - case 'collection': - return new ArrayCollection(); - case 'boolean': - case 'bool': - return false; - case 'datetime': - return (new DateTime())->setTimestamp(0); - default: - throw new InvalidArgumentException('Unknown type! You have to specify a placeholder!'); - } - } - - return $this->placeholder; - } -} diff --git a/src/Security/EntityListeners/ElementPermissionListener.php b/src/Security/EntityListeners/ElementPermissionListener.php deleted file mode 100644 index d9a8d90d..00000000 --- a/src/Security/EntityListeners/ElementPermissionListener.php +++ /dev/null @@ -1,223 +0,0 @@ -. - */ - -declare(strict_types=1); - -/** - * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony). - * - * Copyright (C) 2019 Jan Böhmer (https://github.com/jbtronics) - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 2 - * of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA - */ - -namespace App\Security\EntityListeners; - -use App\Entity\Base\AbstractDBElement; -use App\Entity\UserSystem\User; -use App\Security\Annotations\ColumnSecurity; -use function count; -use Doctrine\Common\Annotations\Reader; -use Doctrine\ORM\Event\LifecycleEventArgs; -use Doctrine\ORM\EntityManagerInterface; -use Doctrine\ORM\Event\PreFlushEventArgs; -use Doctrine\ORM\Mapping as ORM; -use Doctrine\ORM\Mapping\PostLoad; -use function get_class; -use InvalidArgumentException; -use ReflectionClass; -use Symfony\Component\Security\Core\Security; - -/** - * The purpose of this class is to hook into the doctrine entity lifecycle and restrict access to entity informations - * configured by ColoumnSecurity Annotation. - * If the current programm is running from CLI (like a CLI command), the security checks are disabled. - * (Commands should be able to do everything they like). - * - * If a user does not have access to an coloumn, it will be filled, with a placeholder, after doctrine loading is finished. - * The edit process is also catched, so that these placeholders, does not get saved to database. - */ -class ElementPermissionListener -{ - protected Security $security; - protected Reader $reader; - protected EntityManagerInterface $em; - protected bool $disabled; - - protected array $perm_cache; - - public function __construct(Security $security, Reader $reader, EntityManagerInterface $em) - { - $this->security = $security; - $this->reader = $reader; - $this->em = $em; - //Disable security when the current program is running from CLI - $this->disabled = $this->isRunningFromCLI(); - $this->perm_cache = []; - } - - /** - * @PostLoad - * @ORM\PostUpdate() - * This function is called after doctrine filled, the entity properties with db values. - * We use this, to check if the user is allowed to access these properties, and if not, we write a placeholder - * into the element properties, so that a user only gets non sensitve data. - * - * This function is also called after an entity was updated, so we dont show the original data to user, - * after an update. - */ - public function postLoadHandler(AbstractDBElement $element, LifecycleEventArgs $event): void - { - //Do nothing if security is disabled - if ($this->disabled) { - return; - } - - //Read Annotations and properties. - $reflectionClass = new ReflectionClass($element); - $properties = $reflectionClass->getProperties(); - - foreach ($properties as $property) { - /** @var ColumnSecurity */ - $annotation = $this->reader->getPropertyAnnotation( - $property, - ColumnSecurity::class - ); - - //Check if user is allowed to read info, otherwise apply placeholder - if ((null !== $annotation) && !$this->isGranted('read', $annotation, $element)) { - $property->setAccessible(true); - $value = $annotation->getPlaceholder(); - - //Detach placeholder entities, so we dont get cascade errors - if ($value instanceof AbstractDBElement) { - $this->em->detach($value); - } - - $property->setValue($element, $value); - } - } - } - - /** - * @ORM\PreFlush() - * This function is called before flushing. We use it, to remove all placeholders. - * We do it here and not in preupdate, because this is called before calculating the changeset, - * and so we dont get problems with orphan removal. - */ - public function preFlushHandler(AbstractDBElement $element, PreFlushEventArgs $eventArgs): void - { - //Do nothing if security is disabled - if ($this->disabled) { - return; - } - - $unitOfWork = $eventArgs->getEntityManager()->getUnitOfWork(); - - $reflectionClass = new ReflectionClass($element); - $properties = $reflectionClass->getProperties(); - - $old_data = $unitOfWork->getOriginalEntityData($element); - - foreach ($properties as $property) { - $annotation = $this->reader->getPropertyAnnotation( - $property, - ColumnSecurity::class - ); - - $changed = false; - - //Only set the field if it has an annotation - if (null !== $annotation) { - $property->setAccessible(true); - - //If the user is not allowed to edit or read this property, reset all values. - //Set value to old value, so that there a no change to this property - if ((!$this->isGranted('read', $annotation, $element) - || !$this->isGranted('edit', $annotation, $element)) && isset( - $old_data[$property->getName()] - )) { - $property->setValue($element, $old_data[$property->getName()]); - $changed = true; - } - - if ($changed) { - //Schedule for update, so the post update method will be called - $unitOfWork->scheduleForUpdate($element); - } - } - } - } - - /** - * This function checks if the current script is run from web or from a terminal. - * - * @return bool Returns true if the current programm is running from CLI (terminal) - */ - protected function isRunningFromCLI(): bool - { - return empty($_SERVER['REMOTE_ADDR']) && !isset($_SERVER['HTTP_USER_AGENT']) && count($_SERVER['argv']) > 0; - } - - /** - * Checks if access to the property of the given element is granted. - * This function adds an additional cache layer, where the voters are called only once (to improve performance). - * - * @param string $mode What operation should be checked. Must be 'read' or 'edit' - * @param ColumnSecurity $annotation The annotation of the property that should be checked - * @param AbstractDBElement $element The element that should for which should be checked - * - * @return bool True if the user is allowed to read that property - */ - protected function isGranted(string $mode, ColumnSecurity $annotation, AbstractDBElement $element): bool - { - if ('read' === $mode) { - $operation = $annotation->getReadOperationName(); - } elseif ('edit' === $mode) { - $operation = $annotation->getEditOperationName(); - } else { - throw new InvalidArgumentException('$mode must be either "read" or "edit"!'); - } - - //Users must always be checked, because its return value can differ if it is the user itself or something else - if ($element instanceof User) { - return $this->security->isGranted($operation, $element); - } - - //Check if we have already have saved the permission, otherwise save it to cache - if (!isset($this->perm_cache[$mode][get_class($element)][$operation])) { - $this->perm_cache[$mode][get_class($element)][$operation] = $this->security->isGranted($operation, $element); - } - - return $this->perm_cache[$mode][get_class($element)][$operation]; - } -}