Chapters
39 Chapters
|
4:45:13
|
Login to bookmark this video
-
Course Code
Subscribe to download the code!Compatible PHP versions: ^7.1.3, <8.0
Subscribe to download the code!Compatible PHP versions: ^7.1.3, <8.0
-
This Video
Subscribe to download the video!
Subscribe to download the video!
-
Subtitles
Subscribe to download the subtitles!
Subscribe to download the subtitles!
-
Course Script
Subscribe to download the script!
Subscribe to download the script!
35.
Security Logic in the Validator
Scroll down to the script below, click on any sentence (including terminal blocks) to jump to that spot in the video!
Subscribe to jump to this part in the video!
Keep on Learning!
If you liked what you've learned so far, dive in! Subscribe to get access to this tutorial plus video, code and script downloads.
This tutorial works great for Symfony 5 and API Platform 2.5/2.6.
What PHP libraries does this tutorial use?
// composer.json
{
"require": {
"php": "^7.1.3, <8.0",
"ext-ctype": "*",
"ext-iconv": "*",
"api-platform/core": "^2.1", // v2.4.5
"composer/package-versions-deprecated": "^1.11", // 1.11.99
"doctrine/annotations": "^1.0", // 1.13.2
"doctrine/doctrine-bundle": "^1.6", // 1.11.2
"doctrine/doctrine-migrations-bundle": "^2.0", // v2.0.0
"doctrine/orm": "^2.4.5", // v2.7.2
"nelmio/cors-bundle": "^1.5", // 1.5.6
"nesbot/carbon": "^2.17", // 2.21.3
"phpdocumentor/reflection-docblock": "^3.0 || ^4.0", // 4.3.1
"symfony/asset": "4.3.*", // v4.3.2
"symfony/console": "4.3.*", // v4.3.2
"symfony/dotenv": "4.3.*", // v4.3.2
"symfony/expression-language": "4.3.*", // v4.3.2
"symfony/flex": "^1.1", // v1.21.6
"symfony/framework-bundle": "4.3.*", // v4.3.2
"symfony/http-client": "4.3.*", // v4.3.3
"symfony/monolog-bundle": "^3.4", // v3.4.0
"symfony/security-bundle": "4.3.*", // v4.3.2
"symfony/twig-bundle": "4.3.*", // v4.3.2
"symfony/validator": "4.3.*", // v4.3.2
"symfony/webpack-encore-bundle": "^1.6", // v1.6.2
"symfony/yaml": "4.3.*" // v4.3.2
},
"require-dev": {
"hautelook/alice-bundle": "^2.5", // 2.7.3
"symfony/browser-kit": "4.3.*", // v4.3.3
"symfony/css-selector": "4.3.*", // v4.3.3
"symfony/maker-bundle": "^1.11", // v1.12.0
"symfony/phpunit-bridge": "^4.3", // v4.3.3
"symfony/stopwatch": "4.3.*", // v4.3.2
"symfony/web-profiler-bundle": "4.3.*" // v4.3.2
}
}
17 Comments
If you want to use attributes with PHP 8, you have to modify class IsValidOwner like that :
Into CheeseListing class :
I noted with ApiPlatform 2.6, we have to use $this->assertResponseStatusCodeSame(422, 'not passing the correct owner'); instead of $this->assertResponseStatusCodeSame(400, 'not passing the correct owner');
Hey Stephane,
Thank you for sharing the code that uses PHP 8 attributes! And thanks for the tip about return status code, I think we will add a note about it
Cheers!
Hi there!
Tell me if I am wrong but even if the validator triggers a violation (and thus a 400 error), the request will still be written into the database.
For example, if a user (not admin) wants to set another user of his choice as author to an existing post, his put request will return a 400 error, with the customized error message, but in the database, the owner will effectively be changed...
In your test, please do not use only $this->assertResponseStatusCodeSame(400), but compare also the previous data to the data in the database after the request has been made.
For example, an assertion like this one:
$user = $em->getRepository(UserObject::class)->findOneBy(['username' => 'XXXX']);
$this->assertEquals($prevUuid, $user->getId());
For my part, I had to add this kind of code into the validator to be sure that the query hasn't changed my data in the database even after a validator violation:
$previousStatus = $this->em->getUnitOfWork()->getOriginalEntityData($value)['status'];
$value->setStatus($previousStatus)
So maybe I did something wrong somewhere, so let me know please :)
Hey Simon L. !
Hmm. So, this definitely should not be happening. It totally may be happening... but it should - so we need to figure out what's going wrong! :)
First, the problem could just be in the test. I don't know what your full test looks like, but it's possible that your test is using "out of date" data. That's because this is what happens behind the scenes:
A) In the test, when you create the Client object, that boots Symfony's kernel & container
B) When you make a request in the test, that request is made through that kernel. This means that if you have, for example, a CheeseListing object in your test (maybe you created one and are actually testing to see if you can update to a different owner), that is literally the same object that will be "modified" in your request.
C) And so, even though the modified CheeseListing was never saved to the database, the CheeseListing in your test was modified and so your assertion makes it appear that it was modified (but really, the data is coming from "memory" and not from the database).
This may or may not be the issue, but I wanted to mention it first. It's an annoying little thing in tests. To be sure, "refresh" any objects before you run an assertion on them - e.g.
$em->refresh($cheeseListing). Even re-querying (e.g.$em->getRepository(UserObject::class)->findOneBy(['username' => 'XXXX']);) may not be enough because Doctrine is smart and tries to re-use things in memory whenever possible to avoid a fresh query.Anyways, if this is NOT it, then you're right: something is saving this. But, as I mentioned, it "shouldn't" happen. It is however possible: if anything else in your system called $em->flush() after the object was modified (I can't think of why something might do this, but it's possible you have some custom listener or something somewhere) then it would cause any modified objects to be flushed. This is a known "quirk" of how Symfony's validator system works: the fact that you need to modify an object before you validate it means that if something accidentally calls flush later during the same request, then it will flush that object. In practice, I've never been bit by this - it would likely only be possible if you had an event listener on Symfony's ResponseEvent or ExceptionEvent that flushed something to the database.
Anyways, let me know what you find out!
Cheers!
Hi weaverryan !
I don't know what to say... You are such an expert... You were totally right about the test behaviour. If I refresh the object, it works well...
Again, thank you so much for your in-depth help and your great quality courses.
Have a great day :)
Hey Simon L.!
Sweet! Glad we got it worked out! That test behavior is *tricky*, unfortunately!
Cheers!
Hi there !
Validators are great. But just to check if I understood everything well, could we achieve the same result using Voters and "security_post_denormalize" as described below:
In the Entity:
`
/**
`
(In my example, I do not want all users to be able to create a cheese_listing, only the one with CHEESE_CREATE role, but it could be that every user can have this role, it doesn't really change the logic I want to point out).
In the voter:
Some logic to check if (object->getOwner() === $user) OR ($user is admin)
By using Voters we can sort users with a specific role (eg. CHEESE_CREATE), but we could do that in Validators too (by checking $user->getRoles()).
Am I wrong ?
Hey Simon L.
Validating data and authorizing access are two different things. Validators are meant to, well, validate that your data is in a healthy/expected state, and authorizing is for doing things like you want
It's not recommended to grant access to some resource by checking a user's roles via the
getRoles()method, you have to do it via theis_granted()functionI hope it makes sense to you. Cheers!
Hi MolloKhan !
Thanks for your reply :)
Hi there,
Just wondering, why isn't the throw statement on top of the validator's validate method?
Hey julien_bonnier
Did you mean docblock above function definition?
Cheers!
Lets say i have a User with ROLE_EDITOR who is not the owner.
If this user is trying to edit the cheeselisting, without trying to set the owner field, there will be a violation.
Shouldnt there only be a validation check for $value->getId() !== $user->getId()) if the owner field is being set or updated?
Hey Hannah R.
I think you will need to create a custom Voter for implementing such logic. In this chapter you can watch (or read) how to do it https://symfonycasts.com/sc...
Cheers!
Hey MolloKhan ,
just to make it clear: i have a voter that allows users with ROLE_EDITOR to edit a cheeseListing (as in the chapter you linked). But if any user with ROLE_EDITOR is trying to edit the cheeseListing, this Validator is preventing it. Because it checks for $value->getId() !== $user->getId()) even if the owner field is not being updated.
If ROLE_EDITOR tries to update the title only for example, it fails with "Cannot set owner to a different user". The user with ROLE_EDITOR didn't try to set the owner ... Its not even writebale with a put or patch because the owner field has this in the annotation:
@Groups({"cheeselisting:read", "cheeselisting:collection:post"})Hey Hannah R.
Got it now. The problem comes from the
IsValidOwnerValidator, you need make it smarter. If the logged in User has the roleROLE_EDITOR, then, you'll have to check that theownerproperty didn't change. To do that, you'll need to get the initial version of your CheeseListing object. It's a bit tricky because of how Doctrine + Symfony Forms works but here I leave you a good article I found that explains how to do so https://medium.com/@ger86/symfony-validate-an-object-based-on-its-previous-version-4b6ca7a85dc6 - scroll down to theDefining a “Validator” classsectionCheers!
Tengo muchas ganas de ver este tambien
English Only :)
"Houston: no signs of life"
Start the conversation!