Problem/Motivation
From #2983179-16: [META] Implement stricter access checking for the media library:
Secondly, as @marcoscano points out, a permission might be too blunt of an instrument for controlling access to the media library, simply because it can potentially be used in a bunch of different places and in different ways. With a permission, a site builder would need to grant at least two permissions to authors in order to allow them to use the media library at all -- they'd need the "view media" permission, and permission to access the media reference field at all. That's a big fat DrupalWTF that we should do our best to avoid. Instead, we will implement some sort of system, probably based on the event system, to ask the opener (i.e., the field or the WYSIWYG editor in question) to allow or deny access to the media library, based on the parameters in the state object. Since the state object is central to computing access this way, we'll need the tamper-proofing in place first.
Proposed resolution
Implement some sort of system, probably based on the event system, to ask the opener (i.e., the field or the WYSIWYG editor in question) to allow or deny access to the media library, based on the parameters in the state object.
Remaining tasks
Write patch
Review
Commit
User interface changes
None
API changes
None
Data model changes
None
Release notes snippet
| Comment | File | Size | Author |
|---|---|---|---|
| #84 | interdiff-3038254-82-84.txt | 16.03 KB | phenaproxima |
| #84 | 3038254-84.patch | 25.91 KB | phenaproxima |
| #82 | interdiff-3038254-80-82.txt | 1.26 KB | phenaproxima |
| #82 | 3038254-82.patch | 23.39 KB | phenaproxima |
| #80 | interdiff-3038254-76-80.txt | 1.63 KB | yogeshmpawar |
Comments
Comment #2
seanbComment #3
seanbHere is a first patch showing how this could work. Would love to get some feedback on it! Let's see if all tests pass.
Comment #5
wim leersI was concerned how this would impact a future alternative media library UI that is decoupled. How would this work in that case?
In the end, all of this infrastructure boils down to making it possible to ensure that the underlying media field is allowed to be
edited.How could a decoupled implementation achieve the same?
Can't there be different operations on the media library?
Opening?
Selecting?
Something else?
🤔 Can we change this to a more useful (less abstract) reason string?
🤔 It's not merging: we require access to be granted for both.
✔ Woah, that's a lot of "special snowflake string stuff". Not a problem, but surely tricky. This seems to be purely UI implementation details, so doesn't matter.
Comment #6
wim leersOn point 5: it'd be helpful if both the event docs listed concrete use cases (@phenaproxima told me in chat: "fields AND CKEditor" are two concrete use cases.
Also: documentation in the
media_library.api.php.Comment #7
seanbReroll now #3038241: Implement a tamper-proof hash for the media library state has landed.
About #5
1. Not sure, the event is dispatched when you check access for the media library UI, so that is probably done on the route level when creating/validating the state?
2. Currently not. I think opening/selecting is the same thing. It would be very weird if you could open the media library but not be able to select anything. The purpose of the library is to select media.
3. Fixed.
4. Fixed. Changed the comment.
5. Fixed. Added some documentation and moved variables to hopefully make it more logical.
#6
Added docs to the event classes. Not sure about
media_library.api.php, I couldn't find an example where events are documented in anapi.phpfile? Is there an example of this I am missing?Comment #9
seanbThis should fix the tests.
Comment #10
wim leers#7: thanks, that addresses all my concerns. Removed .
Also marking this as a task instead of a feature request, since it blocks CKEditor integration in Drupal 8.8.
Another round of review, getting very close to final review! Leaving marked as to encourage others to review it too.
🤓 Übernit: the quotes are unnecessary!
This is the default access result…
… and this allows overwriting it.
s/has access to the/is allowed to edit the
We shouldn't forbid access by default. We should use
AccessResult::neutral()by default.This then allows us to get rid of the overwriting, and instead use
->orIf(…).That's also how access checking for e.g. Entity/Field API works: default to "neutral" (= no access, but something else can choose to grant access"), rather than "forbidden" (= "no access, ever, because this is explicitly always forbidden").
🤓 Übernit: third person singular.
Inaccurate name.
Comment #11
wim leersComment #12
phenaproximaI think my overarching architectural concern right now is that MediaLibraryUiBuilder has too many responsibilities. I think we should probably abstract out the access checking into its own dedicated route-level access checker class. This will give us more flexibility in the future to determine context-specific access to the media library.
So, in my ideal world, what we'd do is something like this in media_library.routing.yml:
The _permission access check verifies user permissions, and then _media_library_ui is responsible for verifying the media library state parameters, then delegating the access check to the event subscribers. MediaLibraryUiBuilder is then free to focus on doing one thing: namely, building the renderable UI.
The other benefit here is that, in #3038350: Deny access to all media library View Displays if there is no valid state object, we'll be able to more easily modify the routes for the media library widget views; all we need to do is add a
_media_library_uirequirement to them, and access checking is dealt with by the routing system.I'm willing to implement this, but would like a second opinion before I proceed.
Comment #13
phenaproximaAfter tinkering more with this, I'm thinking that the whole "move the access check into a service" might be better done in #3038350: Deny access to all media library View Displays if there is no valid state object. It's not really in scope here, and that issue will benefit much more from such refactoring, since it means we will be able to add a simple
_media_library_state: 'TRUE'requirement to Views routes, rather than having to do bizarre jiu-jitsu around the routing system.So for now, I guess we can just get this delegation feature done, and then tweak the architecture in the next issue.
Comment #14
phenaproximaOK, I addressed part of Wim's feedback in #10. Namely: the access event now sets AccessResult::neutral() by default, and expects subscribers to call
andIf()andorIf()on it, thensetAccessResult().Comment #16
seanb#10
1. Fixed.
2/3/5. Fixed in #14.
4. Fixed.
6. Fixed.
7. Fixed.
Also did some small refactoring in
MediaLibraryAccessTestsince we were starting to repeat ourselves a bit too much.Comment #18
wim leersFrom Slack:
#12 + #13: interesting connection to #3038350: Deny access to all media library View Displays if there is no valid state object, I was completely unaware that those would need the same access checking. That is the case right? To properly check access to those, it needs the exact same inputs to decide whether access should be allowed or not? I know that you wrote in #13 that you think it's better left to #3038350, but it's still valuable to know whether I correctly understand the next steps you describe. :)
✅ Based on our chat in Slack, I think this can be refined. Done.
👍 Per my explanation, this would also determine access in case of using CKEditor on a body field: we'd then check
editaccess on thebodyfield of the given entity.🤔 The 4th parameter here is
NULL. That isFieldItemListInterface $items = NULL, for which\Drupal\Core\Entity\EntityAccessControlHandlerInterface::fieldAccess()'s docs say:So this is not wrong. But it does mean that some field access checks will deny access. For example,
\Drupal\user\UserAccessControlHandler::checkFieldAccess()allows editing certain fields if it's the owner making the change. Due to thisNULL, it's impossible to determine which entity's field is being edited. So it's feasible to run into a scenario where I'm the owner and on this community site I want am able to pick one of the fifty existing forum post banners (this used to be a thing 🤓) from the media library. The code concludes that it's not the owner editing this field, and denies access. Even though I am the owner.I am fine with marking this out of scope and delegating it to a follow-up, because supporting this also requires modifying the opener ID.
🤔 Is this a pure refactor or is this changing the opener ID?
Comment #19
wim leersAs the patch now clearly documents, this blocks #2801307: [META] Support WYSIWYG embedding of media entities.
Comment #21
seanb#18
1. Thanks.
2. Yep.
3. Fixed. Nice catch, I guess we need to provide the items as well then.
4. We are actually changing the opener ID and adding all the information we need to do the access checking.
5. Thanks!
Comment #22
wim leersGood rename 👍
👍
🤔 In this case,Oh wait, if there's no entity yet, it must be a new one, so it can't have a current value. Oops! :D$entity->get($field_name)won't be set to the particular value that it currently has, which the field access check logic might use.👎 We can't assume numerical IDs.
👍 Woah! That's a big change. AFAICT this is not essential, although it results a better UX: rather than having a "Add media" button that doesn't do anything, this ensures the "Add media" button just doesn't appear.
Ideally, we'd have explicit test coverage for this, but I don't think that's a blocker for this; this is a pure UX improvement.
If it weren't for point 3, this would be RTBC!
Comment #23
phenaproximaI'm wondering if this should be CHECK_ACCESS, or just ACCESS. The latter is less verbose, but probably just as clear from a DX standpoint.
I think we should rename this class to MediaLibraryAccessSubscriber, and have it only deal with access-related things (field widget and otherwise). "WidgetEventSubscriber" is both too long and too vague for my taste.
Also, I'm not sure we need to put this in an EventSubscriber sub-namespace. Do other core modules do that? If so, we can leave it; otherwise, let's follow their lead.
Can this be renamed to 'checkAccess'? 'onMediaLibrary' seems too verbose.
I'm not sure we should add a new public static method to MediaLibraryWidget just for this event subscriber to use. For now, let's make it a protected method of the event subscriber.
In fact, let's move all of this to a protected method of the event subscriber. Something like getFieldItems() or similar.
Why did this change?
I'm hoping we will eventually move this stuff into its own access checker class, which means these will be moved out of here. However, that won't break BC, because we can _remove_ parameters from a constructor with no ill effects. No need to change anything, just pointing it out.
This really should be moved to the route requirements, i.e.,
_permission: 'view media'. However, that's out of scope here and there's no need to change it in this patch.I was going to suggest that this be a constant, but then I saw that we have a similar thing already in this class. I don't like that either, but eh...at least it's protected.
We really need a @see here to show how the entity ID is added to the opener ID.
This could be
list(, $entity_id), since we never actually use $field_id.I think the inline comment could use an update to account for what this regex is now doing. It should mention what kind of things expects to find after the 'field:' prefix.
$reason is a string.
$cache_tags and $cache_contexts should be type hinted as arrays.
Comment #24
seanbAs discussed in slack removed some of the string concatenation weirdness and added methods to the media library state for passing an optional entity the library is opened for. Since the media library is going to be used on form fields, and form fields could be tied to entities in a lot of cases, having support for entities in the media library state seems to make sense.
#22.3 In this case we don't have an entity, so we pass 0 to detect that. Changed the approach in the new patch though :)
#23
1. Fixed.
2. Fixed. Renamed to MediaLibraryWidgetSubscriber as discussed on Slack. We are going to add a MediaLibraryCKEditorSubscriber later for the WYSIWYG editor integration. It seems most modules event subscribers are in the EventSubscriber namespace, so kept that.
3. Renamed the event to
MediaLibraryAccessEvent. Since these are event listeners, the most logical name would beonMediaLibraryAccess.4. Fixed. This is not longer in the patch.
5. Fixed.
6. The form parents might not be set (depending on whether or not the form has parents), so this prevents notices.
7. :)
8. Agreed.
9. I think the reason it's not a constant was exactly so we can make it protected.
10. Fixed. Added that to the event subscriber since that is where we fetch the entity data.
11. This code has been removed now.
12. Fixed.
13. Fixed.
14. Fixed.
Comment #26
seanbWhoops, forgot to add a file.
Comment #27
phenaproximaPartial review. This is shaping up really clearly. I'd like a framework manager to look at a particular point, but I'm running out of things to complain about.
I think we should rephrase the description of this parameter, since it's not certain that the state of the media library will be derived from the request. Perhaps "The current state of the media library, usually derived from the current request."
This method doesn't throw an exception, so it should not have a
@throwsannotation.I think we should rename this to getFieldNameFromOpenerId(). That is more verbose, but it's also a lot more clear as to what it does. Given that the whole idea of an "opener ID" is somewhat obscure anyway, anything we can do to make this clearer is a good thing.
I know we discussed this, but I'm still wondering if it's necessary to have entity-specific stuff on MediaLibraryState. It feels like the wrong place for it. Could we maybe put that on the event class? Perhaps we could subclass MediaLibraryAccessEvent with some entity-specific methods (EntityAwareMediaLibraryAccessEvent or something), and use those?
Although, that would require additional query string hashing and validation, which is inconvenient and verbose (and potentially impossible...although we could potentially confine it to this class, which might be the most appropriate course of action).
Sorry for thinking out loud there...I think I need to defer to a framework manager on this one.
This is the wrong type check. The entity type would be an instance of ContentEntityTypeInterface, not ContentEntityInterface. The fact that tests did not catch this means that we probably need a unit or kernel test of it. :)
I think core has a "sample entity generator" mechanism now, which we should probably use instead. :)
Likewise, I think we should rename this getFieldWidgetIdFromOpenerId(), so that it's clearer. We should probably also add a @see so that one can easily look up what a "widget ID" is in this context.
Why was this moved? If it needs to come before the validation, we should probably expand the comment to explain why.
I don't think we can default the entity ID to 0. For all we know, that could be a valid ID for that entity type. This is also assuming that the entity has a numeric ID. We should default it to an empty string, I think, since that is what NULL would become if parsed from a query string.
Coming from a parsed query string, the return value is always going to be a string.
We don't really need the
?: NULLhere.Rather than do this here, let's implement __clone() on MediaLibraryState, so that these parameters are always removed. :) (And let's add a unit test to confirm that this actually happens as expected.)
Comment #28
phenaproximaWe might need a change record for these deprecations.
Also, let's make the $ui_builder the final argument. I say this because in the future we might want to refactor the access-checking to work another way, rather than directly calling methods of the UI builder -- in which case we'll likely remove the argument. If that comes to pass, having it as the final argument will make the deprecation easier.
Can we use AccessResult::allowedIfHasPermission() here, for readability?
Why is this method in the test class? Doesn't seem to be used anywhere...?
Comment #29
seanbThanks for the review! I had to reroll 26, so the interdiff is based on the reroll, not on the actual patch in 26.
#27
1. Fixed.
2. Fixed.
3. Fixed.
4. Yes, since media fields are probably attached to entities most of the times, allowing this to be an optional part of the state is not that weird. Since they are not required, I personally think this makes it easier to work with for developers.
5. Good point. The test was checking the media library button in the entity form, but the form is also checking field access in its own way. Changed the test to call the media library URL directly since this only uses the actual code we want to test.
6. This is only used by layout builder at the moment. Is there any documentation on how this should work? I wanted to mimic what the actual entity form does, and the entity forms do not seem to use
SampleEntityGeneratorInterfacefor new entities? I'm also not sure what the difference is between a sample entity and other entities and if what we are doing here qualifies as a "sample"?7. Fixed.
8. Added some documentation. The entity ID, entity type and bundle parameters are also optionally part of the hash but not required/used while creating a new state.
9. Fixed.
10. Fixed.
11. Fixed.
12. Fixed.
#28
1. Fixed.
2. Fixed.
3. We are using it in the test:
$this->container->get('event_dispatcher')->addListener(MediaLibraryEvents::ACCESS, [$this, 'onMediaLibraryAccess']);Comment #31
seanbHad to revert the change for 28.2 since we have to explicitly deny access.
Comment #32
wim leersDid a thorough review. Every single time I thought something looked questionable, I searched the issue for it and found @phenaproxima asking the exact question I wanted to ask! (And a few times I did so for things I already asked about earlier and there was a good reason to do it the current way.)
Thanks to all prior reviews — those by me, but especially those by @phenaproxima — this review is at least
threefive times shorter than it otherwise would've been :DThese are the only two setters, the only two mutators on this value object. I was tempted it to
withSelectedTypeId(…)and have it return a newMediaLibraryState. But then I realized thatMediaLibraryState extends ParameterBag, which means all data is overridable anyway. So that's pointless.Keeping this exactly the same, but moved
setSelectedTypeId()aftergetSelectedTypeId()and fixed a comment nit.This comment is explaining implementation details of the method; renaming the method instead for clarity.
Also fixed a bunch of übernits.
Evidently very little to complain about. I think this is ready :) Great work!
Comment #33
larowlanshould this handle things like forward revisions and translations?
we have an api for that since #2942907: Entity system does not provide an API for retrieving an entity variant that is safe for editing
should we use
\Drupal\Core\Entity\ContentEntityStorageInterface::createWithSampleValueshere?are we sure we want to mutate state on the object here - i.e should this return a new instance with the new type and the new hash?
these kind of mutations can cause weird edge cases and can be hard to track down
we could just use inheritdoc here?
So because this doesn't use the normal entity access api, there is no other extension point (such as hook_entity_access{_alter}) ?
Comment #34
wim leersI just noticed the class-level docblock hasn't been updated yet to document these.
Comment #35
seanbCopied a comment from phenaproxima in #3044649-9: Delegate media library selection handling to the "thing" that opened the library:
We can remove the entity specific method from the state and have the widget add the parameters it needs after the state is created. The reason we added this was purely for DX reasons. Every field widget using the media library would need to implement similar code to add the entity parameters and re-hash everything to use this in the access check. So even though I think it's useful to provide better DX for developers creating new widgets, we can definitely remove the methods for now and fix this in the library widget.
Comment #36
effulgentsia commentedIt's not just the methods though. It's also the entity-related state parameters, and adding them to the hash validation.
What I think might be cleaner is adding a
MediaLibraryOpenerclass, and having that be the only new thing added toMediaLibraryState. Then, we can have different subclasses of MediaLibraryOpener, so for example, we could have aEntityFieldWidgetMediaLibraryOpener, and that could have methods on it related to the widget's host entity and field.It might take more time to flesh this out though, and there's DrupalDevDays coming up next week, so if there are other things being worked on that are currently blocked on some of what's in this patch that's unrelated to any of the MediaLibraryState changes, it would be great to unblock that while we figure out the MediaLibraryState changes in parallel. For example, it doesn't look to me like #3044649: Delegate media library selection handling to the "thing" that opened the library needs any changes to MediaLibraryState. Is that correct? If so, then could the other parts of this patch that are needed by that one be moved into that one?
Comment #37
phenaproximaHere's a partial, no-tests-yet rewrite of this patch on top of the new architecture we're intending to land in #3044649: Delegate media library selection handling to the "thing" that opened the library.
We found a way to get this to work which involves no nasty code smells or entity-specific changes to MediaLibraryState. Thanks to this extremely pleasant turn of events, this is far slimmer and does not specifically need framework manager review. So I'm removing the tag.
This new architecture doesn't involve events (or even tagged services!), but it's quite different from our previous work in here and therefore necessitates rethinking the tests. I'll get on that next.
This patch combines the access stuff with the work we're doing in #3044649: Delegate media library selection handling to the "thing" that opened the library, just to make sure we're not introducing any problems once we land that one.
Comment #38
phenaproximaThis one changes MediaLibraryUiBuilder so that it actually delegates the access check to the opener service. Let's see how testbot likes it.
Comment #41
phenaproximaAnother pass, after some changes in the other issue. Including the combined patch and the "real" patch for comparison.
Comment #42
phenaproximaForgot an (important) thing in the previous patch. :)
Comment #44
phenaproximaDang. Killed the previous test, because I forgot to update the media_library.ui_builder service definition...let's see what this one does.
Comment #46
phenaproximaOK. Now that #3044649: Delegate media library selection handling to the "thing" that opened the library is in, here's a new patch which fixes most of the tests on my local machine.
Comment #47
phenaproximaTagging for backport to 8.7.x. Access hardening is, in general, a Good Thing, and since #3044649: Delegate media library selection handling to the "thing" that opened the library was backported as well, it makes sense to have this in 8.7.x too, since it makes use of those new internals.
Comment #49
phenaproximaFixed the test.
Comment #50
wim leersThis error message doesn't really make sense, since on lines 52–57 we already are requiring
field_nameanyway.$view_accessis confusing in this context, because there's theview mediapermission that is being checked, but there's also$viewwhich is a "View".I suggest
$can_view_media.Nit: flipping the order would make this slightly easier to read:
Ugh, nothing we can do about this, but so annoying to once again have the DB layer be loading ints as strings.
Why isn'tentity_idalso expected?Ah … because
entity_idisn't required: it is only used when editing an entity whose media field widget is triggering the media library.👍
Comment #51
wim leersWRT #50.1:
Okay so actually there is a convoluted way to reach this point: it’d require somebody constructing a
MediaLibraryStatefor a non-fieldable entity type intentionally.But that's plainly wrong code, not forbidden access.
So rather than this edge case doing
return AccessResult, it shouldthrow new HttpException(500, 'Internal Server Error');Comment #52
phenaproximaAddressed Wim's feedback and added an explicit test of the access delegation, per @effulgentsia's request.
Comment #53
wim leersRe-reviewed the entire patch. No remaining concerns.
👏
Comment #54
alexpottThe $account argument is always supplied here. There's no point in it being optional - see \Drupal\Core\Access\CustomAccessCheck::access. And therefore there's not point in injecting the current user service as far as I can see. See also \Drupal\migrate_drupal_ui\MigrateAccessCheck::checkAccess
I think the shortcut for checking $can_view_media makes sense but that means I think we need a comment saying we are doing
->andIf($can_view_media)do add the cacheability data in. Or perhaps even better we want to useinheritCacheability()instead ofandIfas this makes it clearer.Comment #55
phenaproximaFixed #54.1, but I after closer examination of #54.2, I decided to leave it as-is.
Why? Because inheritCacheability() is implemented on AccessResult, but not defined on AccessResultInterface -- and MediaLibraryOpenerInterface::checkAccess() returns AccessResultInterface. So, calling inheritCacheability() on the return value of checkAccess() would be assuming implementation details of the opener service in question, which strikes me as a bad idea since MediaLibraryOpenerInterface is clearly an API. andIf() is safer from that standpoint, and the default implementation will inherit cacheability anyway.
Comment #56
phenaproximaOn second thought, I decided to go ahead with the suggestion in #54.2, wrapping it in an instanceof check. I figured that AccessResultInterface::andIf() does not guarantee the merging of cache metadata (its interface documentation makes no mention of that); the cache metadata inheritance is a beneficial detail of AccessResult::andIf()'s implementation. So why not take advantage of it? :)
Comment #58
effulgentsia commentedThis if statement is never entered, because allowedIfHasPermission() returns either allowed or neutral, never forbidden. If the intention is to prevent further unnecessary access checking, then I think
if (!$can_view_media->isAllowed())is what's intended. That would be fine from a cacheability standpoint.I disagree with that. I think
->andIf()is clearer, is by far the more common/idiomatic pattern in the rest of core's codebase, and also has the advantage of being more optimized. For example, if the left side is neutral, then it does not need to (and doesn't) merge the cacheability of the argument.Yeah, but if the implementor of andIf() returns an object that doesn't implement
CacheableDependencyInterface, then we don't cache the result, so that's fine. And if the implementor returns an object that does implementCacheableDependencyInterface, then it's responsible for returning one that does so accurately.Comment #59
effulgentsia commentedIf we're validating that $field_name corresponds to a field that exists, should we also validate that $entity_type_id corresponds to an entity type that exists prior to invoking a function (entityClassImplements) on it?
Don't we also need to check edit access on the entity itself as well? If not, why not?
This is mocking a situation where a media reference field is placed on a media bundle. Which, while a possible scenario, is a confusing one, because you have to mentally keep straight what's the host and what's the target. I think it would make future reading of this easier if we use one of our test entity types (e.g.,
entity_testor whichever one most easily supports fields) as the host instead.Same here.
Comment #60
effulgentsia commentedSince everything after here depends on $parameters, we should probably add
url.query_args(or at a minimumurl.query_args:media_library_opener_parametersandurl.query_args:hash) to the cache context of every returned access result. Maybe it makes sense to encapsulate that into makingMediaLibraryStateimplementCacheableDependencyInterface, which we could punt to a separate issue if desired.The lack of this cache context isn't currently surfacing into a bug, because when the media library is opened via
/admin/content/media-widget, that's an admin route which isn't cached by dynamic_page_cache. And when it's opened via/media-library, then I think the View that's rendered is adding theurl.query_argscache context, and something is also setting max-age to 0, but it's fragile to rely on those incidental things.Comment #61
effulgentsia commentedPerhaps we should also check that the field is a media field? Even further, do we want to check that it's configured to use the media library widget? Though that would require us knowing which form display to check for that.
Comment #62
wim leers#54.2 + #56: I don't like using
inheritCacheability(). We're AND'ing two access results together. It doesn't matter that there's an early return in some case. We useandIf()just about everywhere in core. Finally, the early return could also be considered a premature optimization that should be removed.EDIT: looks like @effulgentsia essentially states the same in #58.
#58:
So great that you spotted this! 👏 So bad I did not notice 😩😨 It looked like @phenaproxima had copied what I proposed in #50.3, but he did not.
#59.2: great question! I thought about this too while reviewing. I concluded the current code is correct. My thought process:
view mediapermission:MediaAccessControlHandler::access()is called for everyMediaentity listed by the view in the media library selection dialog. Hence there is already no risk of access bypass or information disclosure. If I applyand try to access my media library (which contains 2 images), I see this:

No media to be seen.
(Although there's markup there that shouldn't be there, but that's out of scope here. Created #3063216: Inaccessible Media entities still have rows generated for them in the Media Library view, containing form wrapper markup *and* the media label for that.)
$can_view_mediaaccess result being talked about in #58 and in other comments is solely for usability, not for security: we don't want users to be able to access the MLSD (Media Library Selection Dialog) if they're not going to see anything in there. Just like we don't render menu links that we know result in a 403 for the current user.IOW: to prevent anonymous users from browsing the entire media library, despite them having the
view mediapermission. #3038241: Implement a tamper-proof hash for the media library state already achieves much of this, but the tamper-proof hash that it uses is based on contents, not on session/user. In other words: if an anonymous user manages to snatch a URL used by an authenticated user, then that URL would contain a hash that also works for the anonymous user — that hash works for any user.Which finally brings me to answering your concrete question: whether to also check entity access and not only field access.
fieldAccess()receives an$entity, so it can choose to provide different behavior for new vs existing entities. (\Drupal\Core\Entity\EntityAccessControlHandler::fieldAccess()does this for example.) And to allow field access logic to on entity newness, the current patch does:To be honest, this is the code I was least certain about. Because
\Drupal\Core\Entity\EntityAccessControlHandlerInterface::fieldAccess()is so quite ill-defined. We struggled with this in the past too, for example for Quick Edit, but also REST (to this day it is unclear whether the$itemsparameter should contain the existing value or the new value, for example).However, in this case, I felt it didn't matter as much since the access checking is not related to editing the referencing entity but to browsing the media library if the current user is able to edit this data model field in general (and not about being able to edit this data model field on this particular entity instance).
I could be convinced either way, precisely because the API is ambiguous. But the current implementation seems correct for my statement, since
\Drupal\Core\Entity\EntityAccessControlHandlerInterface::fieldAccess()'s docs say:#59.3: +1, that'd be clearer.
Comment #63
wim leersAlso, having written #62, I think it's clear that the current issue summary is inadequately explaining what problem this is aiming to solve, and how big of a problem that really is in the first place.
Comment #64
wim leersCreated #3063216: Inaccessible Media entities still have rows generated for them in the Media Library view, containing form wrapper markup *and* the media label for #62 but forgot to add it to the related issues here.
Comment #65
alexpott+1 to removing the premature optimisation. For me it helps but there is less complexity to review in the access method which always seems a good thing.
This is a little unfortunate. The arguments being the other way around... we changed them on the \Drupal\media_library\MediaLibraryOpenerInterface so that checkAccess matched getSelectionResponse - at my request :(
Not sure the best way out of this other than to change the order of both methods on MediaLibraryOpenerInterface since there's nothing that impacts the order of getSelectionResponse in the same way. I think this is acceptable because the interface is so new.
Comment #66
phenaproximaOK, this patch addresses most of the feedback since #56. It should also pass the tests.
Let's go comment-by-comment:
#58: I removed the early return and went back to
andIf().#59:
#60: I added the
url.query_argscache context to all returned access results. +1 on adding CacheableDependencyInterface to MediaLibraryState, and I will open that issue.#61: I added checks to ensure that the field is an entity reference field, and that it references media items. Checking the entity form display feels like overkill to me, especially since it is a straight-up error condition if MediaLibraryFieldWidgetOpener::checkAccess() is ever run with a field that isn't a media-targeted entity reference field. (Which is why it throws \LogicException in those cases, rather than deny access. It should not be possible, under normal use, to cause this error.)
#62: Thanks for this very well-written and expertly thought-out comment, Wim! I had to read it three times before I got it, but I agree with your reasoning.
#65: I'd like a second opinion before I change the interface. I say this because I'd ultimately like to move MediaLibraryUiBuilder::checkAccess() into its own dedicated route-level access checker, and that would probably obviate the refactoring you propose. So, for now, I left it as it is.
Comment #67
phenaproximaOpened #3063343: Make MediaLibraryState implement CacheableDependencyInterface to remove the need for hardcoding a cache context to implement CacheableDependencyInterface on MediaLibraryState.
Comment #68
effulgentsia commentedWhat do we define as the "data model field in general"? Suppose the only entity bundle that contains a media field is Article nodes. And suppose that the anonymous user does not have permission to edit any article nodes. By default, without a module that implements hook_entity_field_access(),
->fieldAccess()returns allowed for all fields. It's only an entity access check that would determine that the user isn't allowed to edit articles at all.Comment #69
effulgentsia commentedI think it's rare for us to have access checking methods that have more than one parameter and where $account is the first one. So, from a consistency standpoint, I think it would be best to change
MediaLibraryUiBuilder::checkAccess()to put$statefirst. If we want to wait until we move the entire method out to a separate class, I think that's fine.Comment #70
phenaproximaI discussed the "should we check entity access" question with @effulgentsia and we decided that, because core's out of the box field permissions are so permissive, it makes sense to check entity access as well (or create access if the entity does not exist). Kicking back for that, plus explicit test coverage.
Comment #71
wim leersI thought this was the the argument that would be made :) That's why I tried to explain in #62 why that is okay. In a nutshell: if a user is able to edit a field that can reference media, then the user can also access the media library.
Now fast forward to #2994699: Create a CKEditor plugin to select and embed a media item from the Media Library, where a second consumer of the API being added by this issue will be added. A prototype of that can be found at #2998005-28: [PP-1] Support Drupal core's Media Library (which was built on top of an earlier iteration of this patch). Let me quote the relevant code:
This checks only the text format's permission! Because in the context of embedding something from the media library in CKEditor, that is happening without any knowledge of a host entity. In fact, it may be something completely else than an entity form that is using
\Drupal\filter\Element\TextFormat.In a nutshell: if a user is able to use a text format, the user can also access the media library.
Both are about
In the former, we could also check entity access. In the latter, we don't get that choice. Are we okay with that difference?
If the answer is "yes": okay, then let's continue this way, although I still am concerned this is making things very confusing. If the answer is "no": please elaborate :)
Comment #72
effulgentsia commentedI think the text format case and the field case are different. In the case of text format, it's clear what permission to a text format means. But in the case of a field, what does "if a user is able to edit a field that can reference media" mean? Fields can only exist on entity types / bundles. And if a user does not have create/edit permission to any entity types / bundles on which a field exists, then that user does not in fact have permission to edit the field, regardless of what
->fieldAccess()says. That's why the docs for fieldAccess() says that the caller is also responsible for checking entity-level access.Comment #73
phenaproximaFor whatever it's worth, I agree with @effulgentsia on that. If the docs for fieldAccess() explicitly state that the calling code is responsible for checking entity-level access, then that's what we should do. There's no harm in it.
Comment #74
phenaproximaRerolled, so no interdiff this time.
I added entity-level access checking and expanded the tests. MediaLibraryAccessTest now distinctly tests that entity creation permissions, entity edit permissions, and field-level permissions are respected, in addition to the stuff related to the media_library view.
Comment #76
phenaproximaThis should fix it.
Comment #78
alexpottI think we want to merge in $entity_access here. Because we want all the cacheability stuff merge. So this should be
$process_result($entity_access->addIf($field_access))Comment #79
yogeshmpawarComment #80
yogeshmpawarAddressed comments in #78 & resolved some coding standard issues.
Comment #82
phenaproximaThanks, @yogeshmpawar! This should bring things back to green...
Comment #83
seanbI have mostly nits/questions regarding mostly the documentation. Besides that the overall state of the patch looks good to me!
This seems to be a little misleading, I think it's more that this specific opener implementation only handles ER fields right?
Why are we using entity_test with a new bundle and field? We already have the node module installed with a basic_page bundle having entity reference fields to media. We should be able to use that and just change some permissions right?
This tests that the field widget opener respects entity create permissions right? I think we should try to avoid confusion between the media library in general and the opener specific access. The same could be said for the documentation of the other methods in the test.
We talked about this before, but it might be nice to have a helper method to assert all this? I believe this was in one of the earlier patches.
Should we also assert that access is allowed when the user has the right permissions?
Same here, should we check for allowed access as well?
Should we document that the media_library_test module prevents access?
The field level access should work for a saved AND unsaved entity right? Could we assert both?
Comment #84
phenaproximaAddressing #83:
Comment #85
wim leers#72:
This is what convinced me 👍Thanks for your patience! 😊
#83.7++ — I'd like to see explicit test coverage for this edge case too.
This addressed #83.2, but I'm not sure we really want this. Generally speaking we always use the
entity_testentity type in our tests, except when we are testing entity type-specific behavior. This is to ensure we don't couple tonode-specific assumptions.Then again,
nodeassumptions were a problem in the past, but not anymore today.So I think this is actually okay now. Especially because this is just the functional JS test, the kernel test is still testing with
entity_test.I'm missing test coverage for the inverse: access granted even without an entity to work with (in case the user has the necessary permissions of course).
Comment #86
phenaproximaI think we have coverage for this in
testFieldWidgetEntityCreateAccess(). If no entity exists, it uses creation permissions.Comment #87
seanbSeems to me like all feedback is addressed. Thanks!
I think this is ready.
Comment #88
alexpottIt looks like one part of #85 still needs to be addressed.
Putting back to needs review just to check that this is outstanding.
Comment #89
wim leersHeh, thanks for your vigilance :)
I was agreeing with @seanB's request for explicit tests in #83.7, and @phenaproxima already addressed that in #84.
#86: Ah, yes, I missed that. The structure of the tests makes it hard to see what is and what isn't tested. But you are right, that edge case indeed does have test coverage.
(Ideally there'd be a single test method testing all permutations in a data provider — that's 2^3 permutations: entity access allowed/forbidden + field access allowed/forbidden + entity new/existing. If core committers think this is sufficiently clear, I won't hold this up.)
Comment #90
wim leersRE-RTBC'ing because #88 has been confirmed to be addressed.
Comment #91
alexpottCommitted and pushed 94361f3394 to 8.8.x and 7b11e5f44a to 8.7.x. Thanks!
Backported to 8.7.x as the media library is experimental.
I've committed this pre the release as I believe this is advantageous but it would be great if someone can do the issue summary update as requested by @Wim Leers in #63.
Comment #94
wim leersYay! Updated the #2994699 core patch to use this newly added API: #2994699-22: Create a CKEditor plugin to select and embed a media item from the Media Library.
Comment #96
luke.leberJust as a follow-up to this, I've noticed that there's something new happening with the arguments passed to the access checkers for non-bundleable entities on the new check.
Quoted below from \Drupal\Core\Entity\EntityAccessControlHandler:
The $entity_bundle argument seems like it's being passed the entity type id instead of NULL for entities like Users.
To reproduce:
1) Install Drupal 8.7.7
2) Enable the media library module
3) Create an image media type
4) Add an image.
5) Create an entity reference field on the user entity to a media item
6) Go to the user add form and open the media library widget and select a media item, but do not insert it yet.
7) Set a breakpoint at line 284 in EntityAccessControlHandler.php
8) Click insert to pause at the breakpoint in the XHR request.
9) Observe that $context['entity_type_id'] is 'user' and that $entity_bundle is also 'user'.
Going by the documentation on the checkCreateAccess method, I would have expected that NULL would be passed in $entity_bundle for something like a User.
If this is agreeable, then I'd be more than willing to make a new issue and submit a patch to fix it.
Thanks