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!
18.
Access Control & Voters
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
}
}
41 Comments
Is source code that runs with a more current version of Symfony available? Given that this is about security, it'd be great to see code that works with 6.0 / PHP 8, neither of which you can use with this code base.
Also, I can't figure out how to load fixtures -- are they in the repo? I just want to download the code, install it, and see it run.
Hey Tac-Tacelosky !
API Platform 3 should come out sometime this summer - we're waiting on that before we give the tutorials a facelift for newer php and symfony version. I know, it's not ideal until then :).
About the fixtures, there are no fixtures for this course: you just start with an empty database. We DO introduce data fixtures before episode 3, so you'll see some fixtures there (they load with the normal
bin/console doctrine:fixtures:load).Cheers!
I have the start and final working locally with Symfony 6, and am going through the tutorial. A challenge is that this is about security, and security has changed between Symfony 4.3 and 6. OK if I put the source code with Symfony 6 on github?
No problem at all - in fact, that would be super great :).
> A challenge is that this is about security, and security has changed between Symfony 4.3 and 6
That's definitely true. Fortunately, the biggest changes - to custom authenticators - I think isn't something that affects this tutorial... which is lucky!
Cheers!
I've updated all the dependencies and recipes: https://github.com/tacman/a...
BUT tests don't work (likely because of the backporting of apiplatform 2.5 testing client).
Ah, cool - thanks Tac!
About the tests... yes, that's very possible. You could (if you want to) delete that src/ApiPlatform directory entirely, then update a few use statements around the app to point to the REAL API platform classes, since your app nicely uses v2.6.
But anyways, thanks - it's nice to have resources like this. Oh, and soon I can share that we will update our comments system to be "in house" - and one of the features we want to add is the ability to "highlight" extra helpful comments from users, like this one :).
Cheers!
I'm likely using the wrong version of something (PHPUnit?), as I'm getting tripped up on self::$container. Sigh. And $client. What is the correct base class for the tests? I tried use ApiPlatform\Core\Bridge\Symfony\Bundle\Test\ApiTestCase;
Change that self::$container to
static::getContainer()- that's a newer way for newer Symfony versions. I think your base class is good!About $client, it should be, I believe, the normal
$client = static::createClient();.Cheers!
I'm getting pretty close... BUT still not working. Perhaps I'm not setting the client correctly?
1) App\Tests\Functional\CheeseListingResourceTest::testCreateCheeseListing
not passing the correct owner
Failed asserting that the Response status code is 400.
HTTP/1.1 422 Unprocessable Content
Hmm, I think that the test line should just change to look for a 422 status code:
It looks like I WAS testing for a 400 status code previously, but I can't remember why. 422 actually makes more sense: passing the wrong owner fails due to the IsValidOwner constraint, so that's a validation error. The status code changed at some point in API Platform 2.6 - https://symfonycasts.com/screencast/api-platform-security/validator-logic#comment-5574495978
So, just change that line - I think you're good!
Cheers!
Hmmm It was a bit confusing after they added security and security_post_denormalize. We need to use just "object" with security. And to be exactly the same with this video, one can use security_post_denormalize with previous_object.
Hey teh_policer!
Hmm, it sounds like maybe my description of how things should look in API Platform 2.5 was not very clear? Is that correct? I was thinking that the ACL should look like this in 2.5:
Did you find that the 2.5 version code above didn't work? Or was it just confusing? Let me know - I'd like to make this more clear if I can :).
Thanks!
I wonder how to do a UserVoter. For User entity itemOperations, I can do
"put" = { "access_control" = "(is_granted('ROLE_USER') and object == user) or is_granted('ROLE_ADMIN')" }. But if I want to use Voter, I am not sure what to compare$userobject with in switch statement. I think I can compare$user->getId()with$subject->getId(), but I am not sure that's the best option.Thanks!
Hey Sung L. !
Good question. So... comparing the id's is absolutely a safe thing to do - it will check if the currently-authenticated user's id is equal to the id of the user being edited. This is totally safe. A more interesting question is actually this: is comparing the objects safe:
$subject === $user. The answer to that is also "yes" - if the currently-authenticated user is the same as the user being modified, then$subjectand$userwill literally be the same object in memory (e.g. if you changed the email on$subject, then it would also be changed on$userbecause they are actually the same object). This is true simply because this is how Doctrine works: the security system will initially query for the currently-authenticated user. Later, when API Platform queries for that same User, Doctrine returns the same object (instead of making a 2nd query and returning a separate object with the same data).I may have over-answered your question... or missed it entirely. Let me know ;)
Cheers!
Thanks for your detailed explanation to my question. I appreciate it!
Hello, if i have an entity, lets say PrivateCheeseListing, which relates to PrivateCheese, and it outputs PrivateCheeses data when i call PrivateCheeseListing, if i have a PrivateCheese item owned by me and another one PrivateCheese owned by someone else, which i have no access to, it is still possible for me to take the IRI "/api/cheese/123", which i have no access to, create a CheeseListing with it, so the JSON contains "cheese": "/api/private_cheese/123" and API Platform will not check if i have access to the related resource and saves it anyways. Is it possible to easily implement a check so that api platform checks the security i configured before relating and saving the item? Or will i have to write a Persister for each and every entity which checks each and every relation?
Again for clarity
If i call GET /api/private_cheese/123
I will have a 404 returned.
If i POST to /api/private_cheese_listing with:
This will return:
Hi Alex T.!
I think I understand! I believe what you are asking about is covered in this section - https://symfonycasts.com/sc... - and the answer is "do this with a validator". This gets a bit tricky. I should not be able to create a new PirvateCheeseListing (or modify a PrivateCheeseListing) and relate it to a PrivateCheese that someone else owns. That should NOT be allowed, for security reasons. But, the tool to solve this problem is actually the validation system: the user is trying to send "invalid data"... it's just invalid due to security reasons. So, it's a situation where validation & security work together.
Specifically, I would add a custom validator, put it on PrivateCheeseListing, and then it would validate that all of the related PrivateCheese objects ARE owned by the current user.
Let me know if that helps!
Cheers!
Hello, is there a simple way to pass both object and previous_object to the Voter? (to grant by property transition)
To have something like:
<blockquote>
is_granted('ROLE_PUBLISHER') and previous_object.isPusblished() == false and object.isPublished() == true</blockquote>(Please don't mind the example I need this for a complex case).
The way I implemented is by passing the previous_object to the Voter, then getting the object from the database using the EntityManager inside the Voter, which I don't think is the best approach.
(also I wonder if this a validation case and not security)
Thanks!
Hey Ahmed O. !
I've never done this before but, yes, you CAN do this :). But, before I say how, I would usually handle this in a custom validator because it is not a matter of "whether or not the user can perform this action". It is more of a "given this user, this is an invalid value for this field". We do something like that here: https://symfonycasts.com/screencast/api-platform-security/validator-logic. The tricky part is getting the original data so you can see if the field has changed. There is an example of how to fetch the original data from the unit of work here - https://stackoverflow.com/questions/17306635/symfony2-form-validator-comparing-old-and-new-values-before-flush#answers - the example is old, but the important part is still relevant :).
Now, about the voter (if you choose to go this direction). You can pass anything to the 2nd argument of is_granted. For example:
(if I messed up my syntax above, apologies!). In this situation, the
$subjectthat's passed to your voter would be an array, which is a bit rare, but TOTALLY legal. You can then read the keys off of that.Cheers!
I suspected that it's a validation case, thanks a lot!
How can I do filter automatic in GET collectionOperations by owner == user logged (Security) ?
Hey Cesar C. !
Excellent question! You an find info about that a bit later in this tutorial :) https://symfonycasts.com/sc...
Cheers!
Sorry, but why we should taking care about the access using Exception throwing, whether we are not handling the possible typo in the support method?
Hey Anton,
Just a sanity check. Nothing big, as we will deny access for those cases, but it's kind of an edge case, i.e. this kind of things is not implemented and so it may lead to potential bugs in the future. But as I say, it's just a sanity check to notify developer about possible problem "earlier".
Cheers!
Hi!
Is it safe and recommended to get the Request in a Voter? For example, with Ajax I'm sending a small object {user: "user IRI", object: "object IRI"}. For editing or deleting, no problem: a classic voter does the job with $object->getOwner(). But for creating...? How can I be sure that the sent user's IRI is actually the logged in user's IRI?
I read on StackOverflow that I can add in my Voter "private $requestStack" then use "$this->requestStack->getMasterRequest()->get('user')" (to get the user IRI of the sent object), but it doesn't seem to work, and a "dd($this->requestStack->getMasterRequest()" doesn't return anything so I'm not sure what to do.
Edit: ...or should I delete the user part of the sent datas and use a Custom Symfony Controller to create the relationship between the object and the user?
Hey Xav,
You can inject whatever you want in your services, just literally any services. Yeah, Request service is a bit special, and to inject it - you would need to inject RequestStack instead, and then get the actual current request via getMasterRequest() for example. So injecting the RequestStack is the correct way get the request in your services.
To get access to the current user (logged in user) - you can inject another service called Symfony\Component\Security\Core\Security service which has getUser() method that will return the current user if it exist or null.
You will be able to use those objects to do whatever business logic you need.
I hope this helps.
Cheers!
Thank you!
I had a hard time to figure what was wrong with what I was doing. Actually, I didn't need the requestStack, but I needed to add "security_post_denormalize" in my Api Platform Configuration. Not sure I've understood why, though. Anyway, it seems to work now...
Is there a simple way of debugging a voter? I wanted it to return errors at different steps of the voting process, and I couldn't manage to do that. For example, throw an explicit error if there's no $user, or return the $subject, or the "$this->requestStack->getMasterRequest()", or understand why the voter abstains whereas it shouldn't (that was my problem for a long time until I added security_post_denormalize). I tried to dump those vars but nothing appeared in the debugging tools. I must have missed something.
PS: could you please write something about Subresources Voters...?It seems that I have now to understand why "api/users/{id}/agendas" doesn't care at all about my voters....
Hey Xav,
Take a look at Symfony Web Developer Toolbar, it should help with debugging voters as well, but only those what was called, so make sure you call isGranted() in the controller or is_granted() Twig function in a twig template of the page you're on. Otherwise, simple dd(); or dump(); die; in the voter helps as well. Or just inject logger service into the voter and log things to watch them later in logs.
I hope this helps!
Cheers!
In a voter, Why does $subject->getOwner() === $user bring back true when $token->getUser() is an object and $subject->getOwner() is an Iri "/api/users/1"?
Hey Gediminas N.!
Excellent question! All of your assumptions are correct, except for:
So,
$subjectas you know is aCheeseListingobject. This means that$subject->getOwner()is actually callingCheeseListing::getOwner()which returns aUserobject, not an IRI string.The confusing part is probably that if you make a request to
/api/cheeses, you get back JSON like this:This is a key feature of API Platform: when it sees that the CheeseListing.owner property is a User object, and it knows that User object is an API Resource, it converts the User object into an IRI string: /api/users/1.
The same thing happens when you send data. If you, for example, sent a POST request to /api/cheeses with this data:
... API Platform would transform the /api/users/1 into a User object (by querying the database for the User object with id 1) before ultimately setting that object into the owner property of CheeseListing.
Does that help? Or did I confuse thing more? ;)
Cheers!
Perfectly answered question!
Cheers!
My voters use constants for their roles.
const EDIT_POST = 'EDIT_POST';Is there anyway to use these inside the annotation the same way we would do in a controller?
I've been trying a couple of things, but nothing worked so far.
Using Symfony 5.2.1 with PHP 7.4 and API Platform 2.5.9
Hey julien_bonnier!
Hmm. I think in annotations in general you can have things like MyClass::EDIT_POST. The problem is that, when you need to reference the constant, you're not actually writing "annotations" - you're inside a "string" in annotations - specifically the "string security expression". So, I don't think that any normal annotation tricks would work here.
So, I think what you need to do is use an expression language trick - specifically the constant function - https://symfony.com/doc/current/components/expression_language/syntax.html#working-with-functions - but I don't think it will feel very nice, unfortunately - I think you would need something like
constant("My\Full\Class::EDIT_POST")to use it. On the bright side, it would work and if you ever changed the constant (or removed it) the code would continue to function (or, explode with a clear error in the case of removing it).Let me know if that helps!
Cheers!
Hey, not really on topic but i was wondering how you guys "styled" the command line outputs? With the nice red/ green backgorund. I thought i saw it somewhere here on SFCasts but i can't find it anymore. I'm getting annoyed by mine own simple output... aarggh. Please let me know, thanks!
Hey lexhartman
Good question, as I know we do nothing with it, I mean it's default behaviour in osx/linux terminals, there is some difficulties only with windows terminals, but I think it's possible there too
Cheers!
Heyho
Liked your post. I ran into one problem though:
On POST requests the $subject parameter on the supports method will always be null. So there seems to be now way to use this approach for create requests out of the box or did I miss something?
Cheers and looking forward to feedback on this
Phil
Hey @Phil!
Yea, "creates" are always (even outside of an API) a "different animal". That's because the only thing you know is that the user wants to "create a CheeseListing". And so, depending on your needs, you have 2 options:
1) Block access with a role - e.g.
is_granted('ROLE_CHEESE_LISTING_CREATE'). That works for simple cases where it makes sense to assign a role to any user who can create cheese listings.2) Invent a new "attribute" (like we do in this chapter), but with no subject:
is_granted('CREATE_CHEESE_LISTING'). You would then, in a custom voter, have a supports like this:Then, in voteOnAttribute, you can use whatever logic you need to figure out if the current user should be able to create a cheese listing.
Let me know if that's what you were looking for!
Cheers!
Hey,
How to use Voter with paginated collection operation ? On the support method, subject is an instance of ApiPlatform\Core\Bridge\Doctrine\Orm\Paginator and can't do anything to check if subject is an instance of my resource.
And more generally, if $subject is an array of resources, what is the best practice ? Checking if the first array element in an instance of the resource ?
Thanks !
Hey Vincent
In that case I think what you need to is to check if the user has access to such operation, instead of checking on object by object basis
Cheers!
"Houston: no signs of life"
Start the conversation!