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!
25.
Context Builder: Dynamic Fields/Groups
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
}
}
22 Comments
Great job!!!
Thank you for your feedback!
Cheers!
hallo i would like to know wich is the best way to clean input field using api platform, according to this https://github.com/api-plat... has been told to take care of cleaning the values before sending to the client but for me doesn't look like it is the right approach,
what do you think it is the correct approach
Hi!
Thanks for the tutorial, first time I've created a ContextBuilder, nice discovery. :)
I was wondering though... If I'm not mistaking, in the tutorial the PUT method for User has access_control/security property set to
is_granted('ROLE_USER') and object == user, so then when we dynamically add "admin:write" to the denormalization context, and thus allowing the admin user to update their roles... it allows <i>only this user</i> to update <i>their own</i> roles, right? What if I would like to be able in my admin board to update the roles of any user, but without being able to change their e-mail or password? (Because if I change my PUT security attribute tois_granted("ROLE_ADMIN") or object == user, then the admin user can update any field of any user, and that I don't want...) Is this the right situation to use the PATCH method (which I've never used yet)?Thanks!
Hi Chloé!
Excellent, crazy question - I like it! So, this is something that we'll talk about in the next tutorial - https://symfonycasts.com/screencast/api-platform-extending - but it will be a few more days or even a week+ before it's out. So let me give you the preview:
A) validation & security are a blurry line. Is what you want to do security... or validation? It's sort of both :p
B) What I do is use security to deny access entirely to operations: like, you do or don't have access to the PUT method for this user. I use validation for everything else: where a user can/cannot change a certain field to a certain value.
C) So, in the next tutorial, we will create a custom validator, attach it to the ApiResource (attach it above the class if you need access to the entire object, or attach it just to a specific field if you only need to read that one field), inject the Security service into that validator, then go crazy by doing whatever checks that you want. One key ingredient to this is that you will need to know which fields were changed on the request. What I means is, you can't simply look at $user->getRoles() in the validator and say "it looks like the roles changed!" because... maybe they did... or maybe the request didn't touch the roles at all. What I will do in the next tutorial is get the "original data" of my entity (User in your case) from Doctrine and compare it with the new data. For example:
Also:
Correct. admin:write means only an admin can edit that field... and because the only user that has access to a PUT request at all is "myself", it means that only an admin could modify the roles field... and they could only modify their own field :p.
I hope that helps!
Hi @weaverryan!
Thanks a lot for your fast and complete answer, as always it's very interesting and really helps. :D with that in mind I'll think a bit more about what I want to achieve/how I want to achieve it, and wait patiently until the release of the next tutorial.
Cheers!
Hey Chloé
Just to let you know that the ApiPlatform episode 3 course it's been actively released now https://symfonycasts.com/sc...
Cheers!
Hey MolloKhan
Thanks, actually I think the chapter that would interest me most regarding my question is not out yet (I'm guessing it would be the 4th or 6th)... but of course I'll take a look at the first three that are out. :)
This will successfully prevent an unauthorized user from writing to a field in the "admin:write" group, but attempting to do so will still return success in the http response, and it will still write any other fields that are sent that the user is allowed to write to. Shouldn't it throw a 403, and abort the whole PUT/PATCH/POST? Is there a way to do that?
Hey @couteau!
Excellent question! So... it depends on what you want :). In theory, by throwing a 403, you are:
A) GOOD: Giving the user more information about why they can't set this field (that's a good thing! Maybe they forgot to authenticate)
B) BAD: You are potentially exposing that this field exists. That's not the biggest deal ever, but if this IS truly an admin-only field, you're exposing the fact that these fields DO exist - e.g. if I send a non-existent field called "bar", it's simply ignored. But if I send a field that DOES exist, but is protected, then I would suddenly get a 403.
So... it depends on what you want :). How would you throw a 403 instead? Hmm. A few options come to mind:
1) You could create a custom denormalizer that looks at the input array, sees if the field is present, checks if the admin:write group is present, and if the field IS present, but the group is not, throw an AccessDeniedException (the one from the security system). This is pretty "hands on", you are effectively duplicating your work in the context builder. Basically, you now have a context builder to add the admin:write group... but then you again manually check for that field in your custom denormalizer. You could probably also do something similar via an event listener.
2) By default, in general, Symfony's serializer ignores extra fields. However, you can change this behavior and tell it to throw an exception instead - https://symfony.com/doc/current/components/serializer.html#deserializing-an-object - you do this by adding setting an
AbstractNormalizer::ALLOW_EXTRA_ATTRIBUTESkey to false on the$context, which you could do in your context builder. This will cause aExtraAttributesExceptionto be thrown, which I believe will map to a 500 error. However, you could map it to a 403 via the api_platform config: https://api-platform.com/docs/core/errors/So yes, this is possible - but it's not the "normal case" I think, so implementing it will take a bit of work :).
Cheers!
Hey!
I like this way of adding groups depending on the context!
However, I stumbled upon a problem with
admin:*(and in fact, every global groups).Because this method uses the same group name for every resource, it can easily create conflicts with relations.
For instance, I have an
UserwhosepasswordVersionis admin-only.In my
Book, I have a relation to theUser, which I'd like to represent as just an IRI.It's fine, api-platform does that by default... EXCEPT!
Since the
admin:readgroup is present, my beautiful IRI has become a nested object with justpasswordVersion(and jsonld stuff).Note that I may have made a mistake. If so, stop me right here.
The way I fixed it was duplicating already present groups with the
adminprefix:<blockquote>
if (isset($context['groups']) && $isAdmin) {
}
</blockquote>
This way, nested resources will contain admin-specific data only if they were explicitly asked for, not just by "chance".
Plus, if I want the
Userembedded in theBook, I can adduser:readto the books' normalization groups.admin:user:readwill follow.So far it's working well but I'd like to get some feedback from you guys, in case I'm missing something
Thanks in advance!
Hey Philippe B.!
Thanks for sharing! This makes sense to me - I actually like your longer admin groups, instead of the generic
admin:read. When I was writing the tutorial, I gave a lot of thought to these group names - and also talked to Kevin Dunglas about the general idea - because I also like these auto-groups, butI was indeed afraid of conflicts like this. This is one that I honestly didn't think of.So, you have no complains from me! I like it!
Cheers!
Thank you for your great job.
suppose that i have ManyToOne relation inside User entity.
for example one association will have many users and each association is having their own admin
so current logged admin can see only users from the same association as admin
i hope you understand my question
but simply i want to compare current logged user with all other users inside decorator (contextBuilder)
Hey Sardar K.!
I don't think I fully understand your situation, but I think I understand enough to try to answer ;). Let me start with the easy answer:
You are able to access the currently-authenticated user by autowiring a new
Security $securityargument and then using that to get the currently-authenticated user.But... I'm not sure this was really your question. Can you give me a concrete example with real entity names and real API request (e.g. when I login as UserA and make a GET request to /api/products ...)? My guess is that you want to actually filter some collection result: make it so that one user sees a certain sub-set of "items" and another sees a different sub-set. If so, I'm not sure that a context builder will be the answer. But let me know the full situation and then we can decide :).
Cheers!
Thank you for your answer
actually the problem solved with chapter 29 custom normalizer
but i have a new question i will try my best to explain correct way this time
i will add code first the i will explain what really i want.
App\Entity\User
App\Entity\Association
so as you can see one Association is having many users that mean each Association can have their own Admin
so what i want that admin can modify email or any other thing only user from the same Association as the admin himself
just like FACEBOOK GROUP admin of the group can only delete or block user from his group not from the group someone else.
i tried with DeserializerInterface but i couldn't achieve what i wanted
so i hope it is clear this time
if not i'm too bad. :)
Hey Sardar K.!
Let me see if I understand correctly :). Let's suppose that
1) UserA is the admin or AssociationA.
2) UserRyan is a member of AssociationA
3) UserSARDAR is a member of AssociationB
In this situation, UserA should be able allowed to edit the email (or other data) of UserRyan. But UserA should not be able to edit the email or UserSARDAR. Correct?
If I'm right, then you should protect this with a custom voter put operation on the User. Something like this (instead of what you have currently):
Then you will create a class like we do in this chapter - https://symfonycasts.com/screencast/api-platform-security/access-control-voter
This will allow you to do any checks you need:
A) Check to make user the current user is authenticated
B) If the currently-authenticated user === the user that's being edited (someone is editing their own information) allow it
C) If the user that's being edited is related to an association AND the currently-authenticated user === the admin of that association, allow it
There is one small "strange" thing with your data setup. It seems that you are determining if a user is a "member", "president" or "admin" or an association via roles on the User. I would not do it that way, just because there are simpler options. I would do one of the following:
1) Add a "membershipType" string field on User that is set to either "member", "president" or "admin". It doesn't make sense to use roles unless you are ever going to do something like
is_granted("ROLE_PRESIDENT")where you are asking "is this user the president of any association in general?"... which I don't think you will be doing. And so, that string field is a bit simpler than storing the value in the roles array.2) If users could actually belong to multiple associations, then you could create a
AssociationMembershiptable that has a ManyToOne to User and Association (instead of relating User and Association directly to each other). If you had this setup, I would then put thatmembershipTypefield on thisAssociationMembershipentity. That would allow a user to be a president of one association, but only a member of a different one.Cheers!
I think the test on normalization will return true is not false === normalization I think it's a fault
Hey Anas,
Could you explain a bit more, where do you think a fault? Or better, could you link us to specific code block where you think we have an error? I didn't see "false === $normalization" checks in this chapter.
Cheers!
Hey,
Looks like on the code example of Entity/User.php (line 59) there has to be "* @Groups({"admin:write"})" instead of "* @Groups({"user:write"})"
Hey Bohdan,
Agree, in the video we change it to the "admin:write", I updated the code block in https://github.com/SymfonyC...
Thank you for reporting this!
Cheers!
Using context decorators to restrict access to certain entity fields is a great approach I can see myself using a lot. I have just implemented this for my User roles like your example. It really is a shame the documentation created from the API doesn't reflect this like you mentioned though!
One issue I did find with this approach is that if you are allowing group filters without a white-list of allowed groups (@ApiFilter(GroupFilter::class)), you can pass the admin:write group with the POST/PUT/PATCH request (with &groups[]=admin:write) and it will allow you to write to any field restricted with the admin:write group - even if the user is not an admin! This is more of a gotcha then anything else but it would be nice if the GroupFilter logic could have different white-lists for normalization and deserialization.
Restricting the ability to update the roles field is great but only half the battle in terms of roles. If you were to go a step further and also only allow a role to be added if the user has access to give it, say they have it themselves, then how would you go about that? E.G User 1 and User 2 both have access to update the roles field of any User. User 1 has the ROLE_ADMIN role so can give User 2 the ROLE_ADMIN role. User 2 can not give the ROLE_ADMIN role to themselves (until assigned it by User 1).
Thanks!
Hey Jarrod
Probably you will have to add the ROLE_ADMIN manually to the first user, or you can code a Symfony command to update/add roles to a specific user. It depends on your needs but if you don't need anything fancy, I would rather do it manually and save some time.
Cheers!
"Houston: no signs of life"
Start the conversation!