0
votes

I am trying to implement access control to the owner of an object. I am using the LexikJWTAuthenticationBundle, and access control works when I limit the check to roles, but it throws an exception when checking an object property.

I'm using API platform installed by composer on a Symfony 4.3 project. PHP is 7.2.19.

I can successfully limit the requests to logged in users by checking for roles, but when adding something like "object.owner == user" it fails with "hydra:description": "Cannot access private property App\Entity\Vehicle::$owner"

This is the entity class with the related fields.

/**
 * @ApiResource(
 *     collectionOperations={"get"={"access_control"="is_granted('ROLE_USER')"}, "post"={"access_control"="is_granted('ROLE_USER')"}},
 *     itemOperations={"get"={"access_control"="is_granted('ROLE_USER') and object.owner == user"}, "put"={"access_control"="is_granted('ROLE_USER') and previous_object.owner == user"}},
 *     normalizationContext={"groups"={"vehicle:read"}},
 *     denormalizationContext={"groups"={"vehicle:write"}}
 * )
 * @ORM\Entity(repositoryClass="App\Repository\VehicleRepository")
 * @ApiFilter(SearchFilter::class, properties={"owner": "exact"})
 */
class Vehicle
{
    /**
     * @Assert\NotBlank()
     * @ORM\ManyToOne(targetEntity="App\Entity\User", inversedBy="vehicles")
     * @ORM\JoinColumn(nullable=false)
     * @Groups({"vehicle:read", "vehicle:write"})
     */
    private $owner;

    public function getOwner(): User
    {
        return $this->owner;
    }
}

This should have allowed only the owning user to get or update the vehicle, but it always fail with "hydra:description": "Cannot access private property App\Entity\Vehicle::$owner".

If I removed the "object.owner == user" annotation, but leave the check for ROLE_USER, then the operation is allowed.

1
You can use is_granted() on any string like let's say IS_OWNER and add a security voter that does the owner check - tsadiq
@tsafiq that's an interesting approach. I found this comment with an example of how to do that. Thanks! - ahaaje
You're welcome, that's a pretty common need, been there done that! Don't forget to pass object as second parameter of is_granted() and it should be all good 👌 - tsadiq
I will add a voter too, but my code is very similar to yours and it works. Your getter is not respecting best practices. Even if it cannot be blank, it should be able to return null. Tru to replace the getter declaration by public function getOwner(): ?User - Alexandre Tranchant

1 Answers

8
votes

There are two ways to solve this problem:

  1. change the visibility of the property owner, that is, "public owner" instead of "private owner" (Note: Bad practice, because it breaks the principle of encapsulation);
  2. Instead of object.owner, just write object.getOwner() and previous_object.getOwner(). Because owner property is private, the only way to access it is by it accessor like so:

        /**
         * @ApiResource(
         *     collectionOperations={"get"={"access_control"="is_granted('ROLE_USER')"}, "post"={"access_control"="is_granted('ROLE_USER')"}},
         *     itemOperations={"get"={"access_control"="is_granted('ROLE_USER') and object.getOwner() == user"}, "put"={"access_control"="is_granted('ROLE_USER') and previous_object.getOwner() == user"}},
         *     normalizationContext={"groups"={"vehicle:read"}},
         *     denormalizationContext={"groups"={"vehicle:write"}}
         * )
         * @ORM\Entity(repositoryClass="App\Repository\VehicleRepository")
         * @ApiFilter(SearchFilter::class, properties={"owner": "exact"})
         */
    class Vehicle
    {
        /**
         * @Assert\NotBlank()
         * @ORM\ManyToOne(targetEntity="App\Entity\User", inversedBy="vehicles")
         * @ORM\JoinColumn(nullable=false)
         * @Groups({"vehicle:read", "vehicle:write"})
         */
        private $owner;
    
        public function getOwner(): User
        {
            return $this->owner;
        }
    }