Problem/Motivation
See #3042745: Remove group @legacy from jsonapi tests and fix deprecation messages and [#32450793], I don't fully understand yet how this all works together but I assume by overwriting the user name *field* with the label/display name can result in overwriting the actual username if that data is saved back.
I'll see if I can create a failing test to show the problem.
Proposed resolution
Not sure, but maybe the display name could be exposed as a separate thing that is explicitly read-only?
Would need to be BC somehow, of course unless we define that the current behavior is simply a bug that must be fixed.
Remaining tasks
User interface changes
API changes
Data model changes
Release notes snippet
In earlier releases, applications that altered user display names programmatically in PHP and also updated user entities via JSON:API were at risk of overwriting user names. JSON:API now serializes a user's display name under a read-only display_name attribute field and the name field instead contains the raw, unaltered user name in place of the altered display name. JSON:API applications that require the display name should be updated to use the display_name attribute field.
| Comment | File | Size | Author |
|---|---|---|---|
| #35 | 3057175-35.patch | 18.54 KB | gabesullice |
| #35 | 3057175-35--tests-only.patch | 11.46 KB | gabesullice |
| #35 | interdiff.txt | 6.47 KB | gabesullice |
| #32 | 3057175-32.patch | 18.09 KB | gabesullice |
| #32 | interdiff-28-32.txt | 15.92 KB | gabesullice |
Comments
Comment #2
berdirYes, behaves just as I expected. This doesn't make sense :)
Had to add a custom test module because user_hooks_test_user_format_name_alter() uses characters that are not allowed and that already fails on validation. This is a bit more explicit in how it fails.
The only failing test if we just remove that label stuff is \Drupal\Tests\jsonapi\Functional\UserTest::testCollectionContainsAnonymousUser and honestly, I think that is the wrong expectation, there is no reason to return 'Anonymous' as the user name there if that's not the stored data.
Comment #4
wim leersThat sounds very bad! I'm not sure yet why this problem doesn't apply to
rest.modulethough, IIRC we just tried to mimic what that exposed injsonapi.module.Comment #5
wim leersMy question in #4 is still relevant as far as I can tell. What does
jsonapi.moduledo different thanrest.module?+1 —
nameanddisplay_namecould be two separate fields — the latter would be a read-only computed field, which would avoid the problem you've demonstrated.Thoughts?
Comment #6
berdir> My question in #4 is still relevant as far as I can tell. What does jsonapi.module do different than rest.module?
I thought I replied to that, maybe in slack, maybe only in my head.
I'm pretty sure that rest.module doesn't do anything in this regard. It doesn't expose the display name, which means it also doesn't have any problems.
> read-only computed field
You know how I think about that :-/. Entities have multiple ways of exposing data, fields/properties is one thing, methods is another. We already have a method for this on the user entity, so from a user.module/entity API perspective, I see zero benefits of exposing this additionally as a field and there is the performance-downside.
We have various issues stuck due to this discussion and we need to find a solution, but I'm not sure what it should be.
I do think that fixing this bug shouldn't be blocked by adding separate support for the display name. This can lead to data loss (overwritten usernames), so one could argue that this is critical. It will anyway be a breaking change for clients that currently rely on this. Most display name implementations are trivial and based on other user fields, so I would assume that combining e.g. the first and last name fields is easy in the client. Sometimes the real value of the username field is then considered private (it's pretty common to e.g. store the e-mail there), that's a bit tricky.
Comment #7
berdiralexpott on slack, on what the things the priority of this should be, in regards to overwriting data without really having a workaround:
Comment #8
berdirJust removing it looks like this.
I feel like the other usage of \Drupal\jsonapi\JsonApiResource\ResourceObject::getLabelFieldName() is also a bit strange: \Drupal\jsonapi\JsonApiResource\LabelOnlyResourceObject::extractFieldsFromEntity().
Assuming that is a read-only representation, why not just use $entity->label(), which would be consistent with how formatters work: \Drupal\Core\Field\Plugin\Field\FieldFormatter\EntityReferenceLabelFormatter. It's also hardcoded for the user entity, and other entity types with custom label implementations like media don't get the same treatment.
Of course I assume that would be a BC break with a much bigger impact as it wouldn't return it in the usual field structure, although I actually don't know exactly how that looks like with jsonapi :)
Per #6, my proposal would be to do this, do a CR and open a follow-up to expose the display name separately as a feature/task. I didn't investigate when/why this was added exactly, maybe someone requested/patched it for a specific use case?
Comment #10
berdirUpdated the anonymous user test accordingly. If that was added specifically for that then we could possibly add a workaround just for that until we have a proper fix, but the anomyous user is very special anyway, not quite sure what exactly the use case there is.
Comment #11
gabesullice@Berdir, the original use case was that the Admin UI needed to show the name of content authors to replicate the content admin listing:
But any decoupled site that wants to show the name of a content author needs access to the user's display name.
I think whatever solution we come up with will need to keep the derived display name under the
nameattribute in the JSON:API response. To do otherwise would be breaking BC for the read-only use case. That's probably 95% of the usages out there.I think we could add a new field, perhaps
user_name, for mutation purposes. In the interest of resolving this quickly, I think we can hardcode this in JSON:API instead of adding any computed fields to the user entity.Comment #12
berdir> I think whatever solution we come up with will need to keep the derived display name under the name attribute in the JSON:API response. To do otherwise would be breaking BC for the read-only use case. That's probably 95% of the usages out there.
I don't see how we can fix the critical data loss bug with that.
It's unfortunate and not my decision to make, but IMHO fixing that is more important than BC/this feature.
Per #8 and without knowing much about how jsonapi works, I would have expected a label-only representation to, well, just call label(), which would then work nicely for the admin/content use case (and all entity types), you can fetch the authors as label and have the display name. And if you get the full user entity, then you likely have the one or multiple fields (e.g. first and last name) that make up the display name and can build it yourself. And the anonymous edge case is IMHO only needed when displaying other things hand having author as a reference, you never need to load and show user 0.
Comment #13
wim leers#8: the impact can be seen in the test failure 😀But you already know that of course, because you made the test pass in #10. That change is problematic though, it is a loss of functionality and a BC break.
Slack
This Slack conversation just happened about this issue:
Proposal
Based on the above, I would propose this:
label: this is whatever$entity->label()returns — this would be read-only, allows the "anonymous" case to still work, and solves the critical data loss bug$label_field, sonameonUsers,titleonNodes: if there is a label field, this allows it to bePATCHed (andPOSTed)The sad consequence is that for most entity types, we'll see two duplicate values. This would be a huge WTF.
A slight alternative implementation would be to add the necessary metadata to entity types to allow other modules to know whether
label()is just returning a field value or whether it's computed. Sort of likelabel_callback, but different. Then JSON:API could continue to expose only$label_fieldfor most entity types, and forUserit could add a read-onlylabel.Comment #14
berdir> the user 0 edge case is an important one actually
By annoying, I didn't mean not important, quite the opposite, as it's the one that all sites see/have, by default, unlike more complex realname-like use cases. That's the annoying part about it, plus that there's actually no other way to get it.
I'd say almost all realname-like use cases with a altered display name are based on one or multiple other fields. And while not pretty, you can build that yourself in a decoupled site/app, e.g. if you document that the display name is "$first_name $last_name", then you can deal with that somehow. Not a generic implementation like admin_ui of course. The anonymous one is trickier, as it is configurable and translatable.
> Proposal
What if $label_field == 'label' :) I think if we'd expose that, then would need to be separate, outside of the field structure, to avoid conflicts similar to the URL), I think.
> A slight alternative implementation would be to add the necessary metadata to entity types to allow other modules to know whether label() is just returning a field value or whether it's computed
Well, we already have the label entity key, although nothing prevents a module to define that and still override label(). Which I think is even how media works, there is a field but it can also dynamically generate a value (most of those cases then persist it on the field in the end).
Comment #15
wim leersI thought about that too. We could put
labelon the same level asid,type,attributes,fieldsandmeta. But that'd be odd. I think it'd be better to put it undermeta. That'd still be outside the field structure, thus avoiding conflicts!Hm … 🤔Great idea! 👍 I think we can fix
Media, because it currently does sort of violate the docs for\Drupal\Core\Entity\EntityTypeInterface::getKeys(), which say:because
\Drupal\media\Entity\Media::label()indeed does:I like @Berdir's proposal. Let's see what @gabesullice thinks!
Comment #16
gabesulliceI'm not 100% sure that I understand the proposal @Wim Leers. The media part is confusing me, are you proposing we fix that too?
As I understand it, the proposal has only 2 parts:
userentity type'snamefield.labelmember to every entity'smetamember and populate the value with thelabelcallback result.Per the Slack thread above (thanks for transcribing that Wim!), I'm fine with part 1.
I don't like part 2 because if the label is part of the
metamember, it can't be removed as part of a sparse fieldset.Since the critical bug only applies to the user entity, let's keep the scope of this issue to only the user entity so we don't create a new feature without fully thinking all aspects of it through. And.. if we're scoping this to only the entity, we don't have to worry about conflicting field names, so we can add a
display_nameattribute that's populated byUser::getDisplayName(). I think that's logical and not WTFy.New Proposal
namefield so it's treated like any other fielddisplay_nameattribute to theuser--userresource type whose value is the result ofUser::getDisplayName()FWIW, I do think it'd be worthwhile to pursue a feature that adds an attribute which shows the entity label on every resource object under a consistent location. This probably isn't the place to do that though.
Comment #17
gabesulliceComment #18
wim leersGood point about sparse fieldsets!
But replacing one piece of special-casing for
Userin JSON:API with another doesn't really get us very far. Other entity types that have alabel()implementation that dynamically computes a name:\Drupal\commerce_log\Entity\Log::label()\Drupal\commerce_payment\Entity\Payment::label()(also uses data of an actual stored field)\Drupal\commerce_payment\Entity\PaymentMethod::label()\Drupal\entity_test\Entity\EntityTestNoLabel::label()\Drupal\group\Entity\GroupContent::label()\Drupal\paragraphs\Entity\Paragraph::label()\Drupal\tmgmt\Entity\Job::label()\Drupal\tmgmt\Entity\JobItem::label()\Drupal\tmgmt_local\Entity\LocalTask::label()\Drupal\tmgmt_local\Entity\LocalTaskItem::label()\Drupal\webform\Entity\WebformSubmission::label()Especially now that Drupal Commerce is actively adopting JSON:API, I think it's important that we tackle that use case here too. Adding
display_nameforuser--userresources may be an easy way out here, but once we come up with a generic solution that will have to stay around forever.Comment #19
wim leersThis sounds like a straight up bug that makes it impossible for modules like JSON:API to do this properly. So, let's fix that. There's only 3 entity types in Drupal core that do this wrong.
This does not fix the reported bug, but I think this probably a blocker in even being able to solve this generically. Thanks to @Berdir for pointing this out!
Comment #20
berdirNope, it's not so simple.
Removing the unecessary implementations is fine, but by removing the key on media, you break autocomplete for example, which relies on that key. There's nothing that prevents you from defining a field and still have a label() implementation to generate a fallback if nothing is set.
Comment #21
berdirPlus, changing entity keys results in storage changes.
Comment #22
wim leersRather than continue to keep talking about it, I figured that because this is a critical bug, I would just implement how I understood @Berdir's guidance (thanks again, @Berdir!).
Whenever an entity type has a
label()callback that does more than just returning thelabelentity key field (detected using reflection that happens only during resource type repository rebuilds, i.e. not on every request), this adds a read-onlylabel_dynamicfield (it is ignored in writes).This passes most JSON:API tests for at least
UserandMedia. (OnlyMediaTest::testCollectionFilterAccess()is failing.)Comment #23
wim leersWhere did I assume it was this simple? I added an all-bold disclaimer to the end of #19 specifically to make it clear that I didn't think that'd fix it all.
Anyway, reverting #19's last hunk because of #21. That simultaneously fixes that last failing test in
MediaTest:)Comment #24
wim leersThis addition is no longer needed.
Comment #25
wim leersWe wouldn't have to change this if the
@todohad been resolved like it should've been (#2821077: PATCHing entities validates the entire entity, also unmodified fields, so unmodified fields can throw validation errors was fixed in 2018!).Opened #3075422: Follow-up for #2821077: address forgotten @todo in UserTest::testPatchDxForSecuritySensitiveBaseFields() for this.
Comment #28
wim leersI can make #22's last test failure pass … but I don't understand why it's failing just yet. Rather than investigating, I'm holding off to first get feedback. I think I've proven that this approach can work.
Comment #30
gabesulliceMy experience building the JSON:API Explorer with @zrpnr taught me the value of the
label()method. It's often very handy to have a human-friendly identifier that is a) at a known location and b) uniform across resource types. This feature of theEntityInterfacemakes it easy to create lists and more friendly UIs. That's a feature that JSON:API (the spec) lacks.If we go with @Wim Leers's proposed patch, we're creating a Drupalism instead of a shareable solution. The next version of the JSON:API spec will have the concept of extensions that would permit us to author a "title" extension for the JSON:API spec. I think that'd be a really valuable contribution. Beyond that, by using an extension, we'd be able to put the label into a "reserved" area of the spec. IOW, we could make the label value a sibling of
typeandid:In fact, that'd be super useful for
field_tagsif the extension applied to resource identifier objects as well. Users wouldn't need to do the clunky?include=field_tags&fields[taxonomy_term--taxonomy_term]=namejust to support the very simple case of a list of tag names on an article.It's this kind of thinking that I want to exhaustively explore before we commit to a fix as far-reaching as adding a
label_dynamicfield to every resource object. I didn't think this critical bug fix was the place to have that conversation though. Maybe seeing the possibilities I'm thinking about will convince you of that @Wim Leers.In #16, I proposed that we use the
display_nameattribute to preserve our freedom to choose another attribute with the term "label" in it without causing too much confusion.The only downside of doing #16 is that, if we decide to put a label attribute on all entity types, the user entity will often have
name,display_nameandlabel_dynamicwith identical values. However, I think a little risk of future duplication is a small price to pay in order to preserve our freedom to choose a more elegant solution for labels later, like the one I hinted at above.Comment #31
wim leersYou've convinced me! 🤩
Let's go with #16 then. If you can roll the patch, I can review. (And ideally @Berdir too.)
Comment #32
gabesulliceGah, what an aggravating set of tests to update :| just toilsome.
Hopefully I got everything.
Comment #33
wim leersYou did! 👏🤓
This is very close :)
I'd like to see these point to a follow-up where we generalize this.
Let's either add "for example" or not tie this to
Nodeentities.Übernit: s/JSON API/JSON:API/. Sorry 😅
👏👏👏
This comment is a copy/paste remnant and should be deleted.
Oops 😁
👍 This is the critical bugfix! Let's also upload a test-only patch; this should fail.
Comment #34
wim leersLooks like this is also coming up in #2336597, see #2336597-141: Convert path aliases to full featured entities and #2336597-144: Convert path aliases to full featured entities.
Comment #35
gabesullice1. Created #3079254: API for JSON:API specific "extra" fields, e.g. for entity labels and updated the comments.
2. Done.
3. Done.
4. :)
5. Fixed.
6. Fixed.
7. Will do.
Comment #36
gabesulliceComment #38
wim leersPerfect! 🥳
Comment #40
catchThanks for the follow-up to try to generalise this. The special-casing is not pretty but I can't think of anything else immediate to close the critical bug. so.. Committed 5fabcff and pushed to 8.8.x. Thanks!
Comment #42
gabesulliceComment #43
gabesulliceWhoa, this didn't have a change record or a release notes snippet! Fixed.
Here's the CR: https://www.drupal.org/node/3085275
Comment #44
xjmThanks @gabesullice! Good catch.
The release note ideally should also describe what it did before so users can understand whether the disruption applies to them. So just a teensy bit more of the CR content. Docs and examples here: https://www.drupal.org/issue-summaries#release-notes
Comment #45
gabesulliceThanks for the pointers, @xjm! Fixed.
Comment #46
gabesulliceComment #47
xjmGreat, thank you!