JSON API does not yet have support for Entity API's view label access operation:
if ($operation === 'view label' && $this->viewLabelOperation == FALSE) {
$operation = 'view';
}
from \Drupal\Core\Entity\EntityAccessControlHandler::access
It was added in #2471154: Anonymous user label can't be viewed and auth user labels are only accessible with 'access user profiles' permission (CR: https://www.drupal.org/node/2661092)
When an entity is not accessible, we should check if its entity type supports the 'view label' operation, and if so, retrieve the label and explicitly mention it in the JSON API error object (http://jsonapi.org/format/#errors), so that at least A) listings of labels are possible, B) sensible error messages are possible
| Comment | File | Size | Author |
|---|---|---|---|
| #63 | 2843922-63.patch | 25.93 KB | wim leers |
| #63 | interdiff.txt | 1.93 KB | wim leers |
| #62 | 2843922-62.patch | 25.71 KB | wim leers |
| #62 | interdiff.txt | 1.2 KB | wim leers |
| #59 | 2843922-59.patch | 25.65 KB | wim leers |
Comments
Comment #2
wim leersComment #3
hampercm commentedThis is definitely a good thing to add, particularly so that the module can respond with Users' names if the rest of the object is inaccessible.
Comment #4
wim leers#3: indeed :)
Comment #5
hampercm commentedI've been running into issues related to the fact that GETing the Anonymous user always returns access denied, so I decided to see what a solution might look like for falling back to "view label" when accessing Users. This approach is not as elegant as I'd like, but it seems to work well.
Making it generic to all Entities might be possible, though there's one piece to that puzzle I haven't figured out yet: how to determine which field is the Entity's "label". It's easy to get the label value itself in a generic way, but then where do you put it in the response without knowing that?
Comment #7
hampercm commentedRetesting...
Comment #9
e0ipsoThanks @hampercm!
All the User special case wrangling is a bit awkward. We should discuss if this principle should apply to all entities.
Also, how is the error handling affected when we can display the label but not the entity?
Damn! I did not know about this
ARRAY_FILTER_USE_BOTHflag.Maybe we should have a LabelOnlyEntityNormalizer class that handles the normalization in that scenario?
Comment #10
hampercm commentedThanks for the review! Responses to #9:
1) Yes, I'd prefer a general solution, I'm just missing one piece of the puzzle, as noted in #5. I guess in some cases the label doesn't even correspond to an actual field, as it can be generated by a callback. Should we just put the label in the attributes section as "label" in that case? If so, would it be a good idea to return the "label" in our responses for all entities, even those the user has full view access to?
2) :-)
3) That approach might be a bit cleaner. We just need to be careful there's no information disclosure along the way.
Comment #11
e0ipsoI'm still on the fence on this. I don't like the
view labelpattern, even if it's already used somewhere else.Quoting @hampercm in #3
I disagree that we should take the liberty to disclose parts of an entity (the label) when the entity says Access Denied for the
viewoperation.If you want an entity to have a public label for everyone, you should make the entity accessible to everyone and the deny access based on permissions for all the fields that are not the label.
I don't feel too strongly about this. I may be convinced the other way, so I'm open for discussion.
Comment #12
hampercm commentedThe label will only be shown for entities that have 'view label' access granted to the user; this is a completely separate access operation. Most entities do not have 'view label' allowed by default, User entities being the main exception, since often times full access to user information is not granted to non-admin users. Following this pattern will be completely consistent with how Drupal core functions.
More specifically, the entity representation of the "Anonymous" user is NEVER accessible by 'view' access, even to user/1; only 'view label' is ever permitted. This makes handling entities that refer to the Anonymous user difficult without the 'view label' functionality.
Comment #13
hampercm commentedWorking on an improved implementation of this...
Comment #14
e0ipsoFYIY I'm still not sold on this idea. I feel a site owner can achieve this without requiring any extra code in the jsonapi module.
Comment #15
wim leersComment #16
wim leersComment #17
gabesullice:O me neither!
Philosophically, I have to say I'm with @e0ipso here. However, I think that ship has sailed. We likely do need to support this. I've personally worked around this issue for client work with custom controllers on several occasions.
A new normalizer for this seems excessive... but I don't have an elegant alternative. I'll try to think more about this.
A few open thoughts and questions:
attributeskey. If so, we need to give it its own key (I assume this will be "label"). Will this appear only when the 'view label' mode is invoked or always?Comment #18
e0ipsoPlease elaborate.
If I provide custom module that removes
viewaccess for all entities of a type, then having a module (core or not) allowing access via aview $propertyNameaccess check seems like as security hole for information disclosure. I don't like navigating these waters unless we absolutely need to. I don't think this is the case.Comment #19
wim leersI understand your hesitation here, and I'm glad you're questioning this :)
However, the whole
view labelthing is a not very well-known Entity API capability. It was added in #2471154: Anonymous user label can't be viewed and auth user labels are only accessible with 'access user profiles' permission (CR: https://www.drupal.org/node/2661092), in February 2016. This operation is only supported on entity types whose access control handlers explicitly opt in, see\Drupal\Core\Entity\EntityAccessControlHandler::$viewLabelOperation:Therefore there is absolutely no security risk here: if you check
view labelaccess, for most entities, it'll fall back toviewaccess, which means that viewing just the label will still be denied. Only for entity types that explicitly have custom logic for this, it may result in the label being exposed.Comment #20
gabesulliceThe reason I say that I think the ship has sailed is because the permissions have already landed in core and they established the precedent for how the operations should be interpreted.
The existence of `view label` implies that `view` should be interpreted as `view all`. We should also remember that Drupal permissions have always been grants, not rules. That is, if a user does not have the `view` permission, we don't say that the viewing the entity is forbidden, we say that the result is neutral. Thus, if another system (node grants, hook_entity_access, etc) does grant permission, it's not an information disclosure because access was never "forbidden" in the first place.
Comment #21
e0ipsoYou both bring solid arguments. Count me convinced.
What should we do next?
Comment #22
wim leersTo clarify: not
view all entities, butview all fields:)Comment #23
wim leers❤️ consensus building like this :)
I'd say: nothing just yet. We have agreement now that we want this feature. But we have more important things to tackle first. This is a nice-to-have for now.
We can keep this issue
Needs worknow instead of marking itClosed (works as designed). Or, if you prefer, we could mark itPostponed.Comment #24
gabesulliceFor next steps, we will need to answer some of these when we come back around to this.
Comment #25
e0ipsoWe totally do.
All entities that have this access type should either have:
If there is a label key, then we're already providing the label under a known field. That answers all your questions.
If there is a label callback, chances are that we're already providing it as well. In fact you can see that
label_callbackis being phased out #2450793: Properly deprecate support for entity type label callbacks. Which means that we can avoid the issue there and ship with special behavior for User, since it's the only core entity that uses it.Comment #26
wim leers#25++
Since this is a new feature, moving to the 2.x branch.
Comment #27
gabesulliceComment #28
wim leers@drpal yesterday indicated in private chat that this is blocking the JS Modernization Initiative. Hence I started working on this.
Comment #29
wim leersThis patch:
LabelOnlyEntityvalue object was added, which contains only a single value: an entity.EntityResource::getIndividual()and::getCollection()now also checkview labelaccess ifviewaccess is forbidden. Ifview labelaccess is allowed, the entity is decorated in aLabelOnlyEntityvalue object.EntityCollectionnow needs to allowLabelOnlyEntityobjects, because some of the entities in the collection may be values of this type.LabelOnlyEntityNormalizeris added, to normalizeLabelOnlyEntityvalue objects. This normalizer:EntityNormalizer, which still retuns aEntityNormalizerValueobjectEntityNormalizerValue, omitting all values in the originalEntityNormalizerValueobject except the label field'sComment #31
wim leersWell that's a bummer:
should have been
Comment #32
wim leersTest coverage!
Comment #35
wim leersOne small mistake with big consequences in #29! Thankfully we have #2953318: Comprehensive JSON API integration test coverage phase 4: collections, filtering and sorting to protect us against this, since about a week! (Which also means this feature could never have landed until a week ago!)
Comment #37
wim leersFrom 53 fails in #32 to 8 in #35.
Most of the remaining failures are for entity types that do support
view labelaccess checking:User,MenuandDateFormat.Unfortunately,
UserAccessControlHandlerandMenuAccessControlHandlergrant ability to view labels blindly, i.e. to all users, even the anonymous user. This is a privacy risk, and potentially information disclosure. That being said, the same data can easily be exposed via the HTML representation of the data, and commonly is. The difference is of course the queryability.This should further reduce the number of failures.
Note: I first came up with something automatic but pretty complex to determine which the appropriate behavior is, by using introspection:
… but this breaks down for the case of
MediaType, which usesview label, but does require a permission.Comment #38
wim leersFix CS violations.
Comment #41
wim leersWhew, this was insanely hard to figure out.
Comment #42
wim leersAnd now the correct interdiff for #41 … 😳
Comment #43
wim leersAnd finally, the updated expectations for
JsonApiDocumentTopLevelNormalizerTest. Now the patch should be green. 🤞Comment #44
wim leersOops.
Comment #46
wim leersNow that we finally have a green patch … let's retry the "add failing test" thing from #32. Although, actually, that's not necessary: #32's test-only patch did trigger a single failure, as predicted!
Fixing CS violations.
Comment #47
wim leersAlright, #46 is green and fixes the original report.
However, in the case of
Userentities, special treatment is necessary, due to incompleteness of the Entity/Field API and User module. Otherwise we'll get"name":""for the anonymous user! Let's first add a failing test to prove this.Otherwise, you still can't see the label!
Comment #48
wim leersAnd fix.
Now this is done. The JS Modernization Initiative should be unblocked now; they can apply this patch until it lands.
Comment #51
wim leers#46 didn't take that Entity/Field API incompleteness into account. Fixed.
Comment #52
gabesulliceMonumental effort! Good job. This is going to make many people very happy :)
Overall, this looks really good too.
One meta question: why is this 2.x?
Ubernit: can we make this an if/else or ternary to avoid reassigning the value?
Is there already an issue for this? If so, can we add a URL?
This comment no longer makes sense.
Let's move this logic to
LabelOnlyEntityand expose it as a method.Same as above.
This is dangerous territory. Looks good though.
This is starting to smell a little, not worth fixing here, but worth putting on the backs of our minds.
I like the explicitness here, over introspection 👍
Why reassign? ... Probably to reuse the code below WRT
$access->getReason()?Why is the status necessary? Can we add a comment?
These two lines assert the whole feature request! :)
Good assertion.
Comment #53
wim leersBecause
That's also why this needs a CR.
Comment #54
wim leers(I'll address the feedback tomorrow btw — thanks for the super fast, very thorough review! 🙏)
Comment #55
GrandmaGlassesRopeMan👏 We're now able to get the anonymous user.
Comment #56
wim leers🎉🍻
Comment #57
wim leersNo longer applied. Rebased.
Comment #58
wim leers#52 still needs to be addressed.
Comment #59
wim leersAddressed everything in #52.
Comment #60
wim leersAll changes in #59 were trivial. The only non-trivial change was the one for #59.10.
As you can see, I removed the
filter[status]=1URL query parameter. I was only able to do this because I updated the other assertions below it. Those assertions were assuming that indata[1], they'd find theuser--userresource for the currently logged in user, which is able to see their ownmailfield. That's still true after this patch, but what does change is that there's an extrauser--userresource showing up in the response: the anonymous user. Hence the assertion needs to be updated todata[2]!To remove all ambiguity there, I also added assertions for the ID of that resource, to ensure we're making the assertions on the intended
Userentity.Comment #62
wim leersClearly, one change was not trivial. Fortunately it was a test-only change:
This was changed to address #52.9. The reassign is necessary to match the logic in
\Drupal\jsonapi\Controller\EntityResource::getEntityAndAccess()and\Drupal\jsonapi\Controller\EntityResource::getIndividual(). The old logic resulted in "allowed", the new logic (which doesn't match the logic in those methods) results in "forbidden". My bad.So, why the reassignment? Because even if
'view'results inAccessResult::forbidden()(which is the case for the anonymousUserentity), we need to allow'view label'to override that. But "forbidden" trumps anything else in regular "or" or "and" joint conditions. This is a special case. Hence the need for a reassignment: to still allow"view label"access.Comment #63
wim leers#62 also made me realize that cacheability of the
view labelaccess control logic was not yet being bubbled correctly. Fixed that now. Tests were already passing because none of theview labelaccess control logic have cacheability other than "vary by permissions" (user.permissions).Also fixed the only CS violation.
Comment #64
wim leersCR created: https://www.drupal.org/node/2983616
IMHO this is ready.
Comment #66
gabesulliceI agree!
Comment #67
wim leersPublished the CR.