Support for Drupal 7 is ending on 5 January 2025—it’s time to migrate to Drupal 10! Learn about the many benefits of Drupal 10 and find migration tools in our resource center.
Problem/Motivation
Core's default access handler only supports checking if a user has the administer BLAH permission. It'd be great to have a per-bundle permission system. And one that was for ownership entities.
Proposed resolution
Create EntityApiAccessController with add, update, view, and delete permissions per bundle, and generate them. Then a EntityOwnerApiAccessController with own/any.
I've written this for Profile, I just need to port it over here http://cgit.drupalcode.org/profile/tree/src/ProfileAccessControlHandler.php
Remaining tasks
Port and patch
Document how to implement
Comment | File | Size | Author |
---|---|---|---|
#54 | interdiff.txt | 4.37 KB | bojanz |
#54 | 2801031-51-add-generic-entity-permissions.patch | 32.14 KB | bojanz |
| |||
#48 | interdiff.txt | 36.59 KB | bojanz |
#48 | 2801031-47-add-generic-entity-permissions.patch | 32.57 KB | bojanz |
| |||
#44 | interdiff.txt | 812 bytes | bojanz |
Comments
Comment #2
mglamanPatch with access controller. Need to provide generator that does per bundle CRUD permissions.
Comment #3
mglamanHere is new patch with an abstract permissions callback class. We can't pass parameters to the permission callback, such as "hey, its this entity type." So it has to be abstract with classes simply implementing the
getEntityTypeId
callback.Supports entity_type and bundle
permission_granularity
settings. Needs to handle EntityOwnerInterface implementation on entity class so we have proper any/own.Comment #4
mglamanNeeds test. But generates any/own permissions now. Ready for general review.
Comment #5
mglamanNew patch with tests. Fixes some bugs in permission handler. Verifies permission generation and non-owner entity access, per bundle.
Comment #7
mglamanTests w/ owner entity interface implementer.
Found a good WTF that is minor edge case.
The test was failing in ::testAccessControlHandler for own/any when the entity was missing the UUID. So if an entity implements EntityOwnerInterface and does not have a UUID there could be problems.
Not sure proper action - or if just something to document.
Comment #8
mglamanRemoves the D7ism of "add" and "edit" along with their mppings. It is CRUD and not ARED after all ;)
Comment #9
bojanz CreditAttribution: bojanz at Centarro commentedOur main remaining task is figuring out the interaction between "administer $entity_type", "access $entity_type_overview", "bypass $entity_type access'. The current code only generates the "bypass" one.
The way I understand this, the permissions work like this:
- administer: you can do anything
- access overview: you can view the admin table, but you might not have access to any operations
- bypass: you can do anything
If my understand is correct, then we can generate and use the "administer" and "access overview" ones, leaving out bypass.
Other points:
I generally prefer seeing $entity_type to $definition as a variable name, less generic. We also need to use the plural label where it makes sense (probably everywhere?)
Nitpicks:
We can remove Api from class names, we don't do that in general.
Wrong interface here.
Gets the entity type ID. Gets the permissions.
you can write this as:
Extra newline at the end of this test method:
Comment #10
bojanz CreditAttribution: bojanz at Centarro commentedSharing a historical note: I did a PoC previously for dawehner: https://gist.github.com/bojanz/1b69c9809477312a5efb that used a generic EntityPermissions class defined by Entity, then required Entity Types to use our extended access control handler.
This allowed people to participate in the generic permission behavior just by defining the extended handler.
No longer convinced this is needed, though Matt's solution does require overriding the permissions class for every entity type (to specify the entity type id).
Comment #11
bojanz CreditAttribution: bojanz at Centarro commentedRetitling for clarity.
Comment #12
mglamanFixups for bojanz
Comment #15
mglamanHere's proper interdiff and patch. Tried to get the reviewed work in before running out the door.
Comment #17
mglamanSame interdiff. Fixes access handler in test entity definitions.
Comment #19
mglamanI can't make a proper patch to save my life today.
Comment #20
martin107 CreditAttribution: martin107 commentedpatch did not apply for meComment #21
bojanz CreditAttribution: bojanz at Centarro commentedLooks better.
Sounds like we should figure out a better name for this handler.
Perhaps ExtendedEntityAccessControlHandler like in my gist. Or GenericEntityAccessControlHandler?
I'm afraid this approach makes t() completely useless. We might need to maintain a map of expected strings, ran through t().
We're still not using the plural labels (and $definition still isn't $entity_type).
Incorrect class name.
These two lines are not in sync, let's fix the second one.
Comment #22
mglamanLess magical permission generation for
t()
. Addressed other items.Comment #23
mglamanRemove bypass for administer. Add access overview. Fix label to use plural.
Comment #24
bojanz CreditAttribution: bojanz at Centarro commentedLet's try this. RTBC from my side if green.
Comment #25
Berdirsame as below, I think it would be nice to have a flag to decide if you want to have own/any permissions or not. You might not need that despite having an owner of your entity type.
could be done in a follow-up, but content_translation defines permission_granularity entity type property to define whether permissions are per-bundle or not.
we should add the user cache context here, as this means that it can be different for users, even if they have the same permissions. (another reason to allow not having those per-user permissions.)
AccessResult::allowedIfPermissions($account, [$own_permission, $any_permission, 'OR'] basically does the same. And I see you use that below, so why not here?
I also don't really get the comment :)
Hm, yeah, that is a bit annoying :)
the callback syntax also supports services. If you define it as a service, you can pass in a constructor argument to set the entity type?
Or loop over entity types and look for them using your access controller? Would be tricky to sublcass then, though, in case you want to customize those permissions.
not sure what's the best option.
I hate administer nodes :)
At this point, it doesn't do much more than control access to status/promote/owner/sticky. I don't see why it needs to be restrict access, that's IMHO a leftover from before bypass and access oveview were split off.
Why not define the bypass permission in the same way as node does and leave administer xy out for now?
access content overview isn't a restricted permission, why is this?
oh, you actually do check this here. doesn't seeem to be considered when actually checking the permissions though?
Comment #26
mglamanHow would you flag this? Currently it requires some additional implementation. But do we have a "hasOwnerBasedPermissions" which defaults to whether the entity implements EntityOwnerInterface? This way people in their implementation can set it to false.
But then there is the issue where the access handler doesn't implement permissions, so we'd need to make sure the handler is to extend to say "nope" like the permissions provider being implemented.
Currently you'd only need to implement the permissions provider. (see later comment why we need to.)
+1
If I did that here, the comment is moot. I should follow the pattern later on. Was debugging cruft that I didn't refactor.
I checked, and no :/ but it is container aware. EDIT: I just re-read this. Would that be any easier DX? I suppose. You then just say "mymodule.entity_permissions is entiy access handler class with parameter myentity" Then provide example permissions.yml code. Feels cleaner than what I've suggested.
That's what bojanz initial prototype did. Entity module defined permissions.yml and generated for everyone. However, I'd rather it be an explicit implementation instead of a magical generation which is harder to control.
There was a bypass, but it did pretty much the same thing as administer. And entities have an "administer permission" definition key.
Because I copied administer O:) forgot to remove.
Shoot, you're right. I'm not sure if entity permission granularity is considered.
Comment #27
kristiaanvandeneyndeThought I'd weigh in and explain the bypass permission and why we need it:
The latter has an important distinction and was added to the node module for a reason. It allows people to edit any node's content, without being able to change its published status or author. Vice versa does the administer permission allow someone to edit a node's metadata, as long as they actually have access to that node.
Given the work I intend to do in #777578: Add an entity query access API and deprecate hook_query_ENTITY_TYPE_access_alter() (if I ever get to it), I'd be careful with removing the bypass permission. We may end up needing it in the grant system.
Comment #28
BerdirRight, but bypass is IMHO a better name for "allow everything". Which doesn't actually seem to be implemented yet? As mentioned, node doesn't check administer nodes anywhere for entity-level permissions, just on field level and there it is too generic (you can allow users to see own unpublished content but you can not give them permission to publish and unpublish content without the restricted administer nodes permission).
With the service, I mean that you would add this to services.yml:
mymodule.entity_permissions:
class: \Drupal\entity\EntityPermissions
arguments: ['my_entity_type', '@foo', '@bar']
And then specific it as mymodule.entity_permissions:getPermissions (single colon = service, double colon = class name)
If that's actually easier, I'm not sure. Just mentioning it as a possibility.
About generating permissions, my idea would basically be to support 3 flags on the entity type annotation:
permissions_generate (if not set, default value depends on implementing this access control handler or not. )
permissions_granularity (defined by core, already implemented in the patch for generating permissions but not for checking them)
permissions_owner (if set and entity implements the interface, generate and check owner permissions. not sure about default value)
Then, entity.module provides a permission callback that generates permissions for all entity types that explicitly or implicitly have permissions_generate set to TRUE. that class also has a generatePermissionForEntityType(EntityTypeInterface $entity_type). If you want to customize the permissions, you implement your own class/permission callback, extend from the default, call that method and customize the returned permissions. Or do it completely yourself.
Comment #29
BerdirCrosspost, my comment was not a response to #27.
> administer: You can do anything to the entities you can access
If you mean by that to be able to change fields like owner, status, ... Yes, that is correct. But this patch doesn't do anything with field level permissions, it doesn't use that permission.. so why generate it? IMHO, I would leave that for another issue. You have to specify it manually but then you can also provide a useful description about what it actually allows to do, as I imagine this could vary a lot between entity types (you don't want to allow users to change the owner of a profile entity, if that would even be possible, but for another entity type, that migh be a perfectly valid and non-restricted use case)
Edit: I think we actually agree for the most part. You're suggesting also to keep bypass, not sure if you're for or against generating an administer permission here :)
Comment #30
kristiaanvandeneyndeI was clarifying bojanz's understanding of the permissions in #9.
I don't think a content entity access handler needs to worry about any administer permission, only a bypass permission. However, there is currently a discrepancy in core regarding the permission names:
admin_permission
key, which isadminister content types
So nodes use bypass for the same purpose node types use administer. Yet on the node level, there is a clear distinction between bypass and administer. Looks like a case of confusing naming patterns :)
Comment #31
BerdirYeah, node is the only entity type with a bypass permission in core, the others just have administer. But node is also the only one that same level flexibility as we're proposing here, so I think we should use that as example and not others :)
Comment #32
mglamanWant to clarify one thing on admin and bypass
So ditch generating administer per mission because it needs to have a better description per use case.
Add back bypass checks and permission like earlier patch. Because this has an easily identifiable purpose and scope.
Comment #33
mglamanHere's an updated patch.
::allowedIfHasPermissions
and OR to check entity_type and bundle granularity properlyI still kept "administer x" as a generated permission. That's how Core works now, and that should probably be a fight to make once this goes into a core issue. And even then, I"m sure it'd be a follow up once this handler was moved into Core if it would even be an 8.x change.
Comment #34
mglamanHere's updated patch where I've added
permissions.yml
. This runs the EntityPermission class to generate permissions. It only provides permissions to entities with the access control handler implement and did not definepermissions_generate
or havepermissions_generate
set to TRUE.The only thing missing is the
permissions_owner
flag. Node follows this permission of own and any. I would assume anything that implements an owner needs this kind of control. And, if not, then site builders / implementers can just assign the "any" permission. Is the worry here fluffing up the permissions page? I guess I don't see the overall benefit of adding another flag.EDIT: more thinking on owner permission. You're implementing an interface which says someone owns and controls it. So I'd imagine it should be controlled that way in access control. Otherwise, if its just a user reference, it doesn't need that interface and can have its own base field referencing a user.
Comment #35
bojanz CreditAttribution: bojanz at Centarro commentedI think it's a mistake to use Node module as an absolute example here.
You've said it yourself, Node has permission patterns that are not followed by the rest of core. Or contrib. In neither D7 nor D8.
The only permission that the entity api in D8 forces you to have is the administer permission. Since the early D7 days (outside of Node) it has always meant "you can do anything". Meanwhile, "bypass" is a node specific permission that 10 Drupal developers couldn't define together the same way. I think that terminology is worse and goes against what we're used to.
Furthermore, I am not convinced non-node entity types have a need for the distinction between "I can do anything" and "I can do anything except publish the entity". Especially since we don't even take the "status" key in account when generating permissions, since we can't know the labels (published/unpublished, enabled/disabled, active/inactive). That's something that should be done in the classes that extend these. These classes should provide sane defaults for content and config entity types alike.
So I suggest reverting back to not defining the bypass permission, using the administer one as the "god mode" one.
@berdir
The service approach doesn't seem to be an improvement over just extending the class. It's a level of indirection that doesn't hide the awkwardness of the current approach completely.
Do you find this preferable: https://gist.github.com/bojanz/1b69c9809477312a5efb
It makes the access control responsible for generating the permissions. Is that a pattern we want to introduce?
We'd need to figure out the right name for the interface (does ExtendedEntityAccessControlHandlerInterface make sense)?
I am also not 100% certain that the permissions are generated early enough.
Comment #36
kristiaanvandeneynde@bojanz: Just to be clear: You're advocating for the administer permission to be the 'god mode' switch for being able to do anything to an entity, including getting full access? Because if we go down that road, creating a generic entity grants system could turn out to be quite tricky.
I kind of like the distinction between an all-access pass and a do-what-you-want-within-your-sandbox pass.
Edit: Even though the current naming pattern and lack of documentation is confusing the hell out of everyone :D
Comment #37
bojanz CreditAttribution: bojanz at Centarro commentedCorrect. It's already that for config entities. And it was already that in D7 everywhere, including Commerce 1.x which had query level access filtering. Entity types will be free to provide a less-god permission such as "bypass query access", which would then be clear about what it affects.
My point is that if we want to standardize something, we must do it by looking at what's common, not base it on an outlier.
Comment #38
kristiaanvandeneyndeAll right, I can live with that :)
Guess we'll have to create an exception for 'bypass node access' in the grant system until D9. I can imagine people having written code that checks for that permission. Others could use a more descriptive "bypass ENTITY_TYPE query access" permission as you suggested.
Comment #39
BerdirThis is the main reason for offering to not have owner permissions.
Per-user caching is very problematic and should be avoided if possible. Some things aren't cached anymore at all then and if they are, you're creating possibly hundreds of variations.
Only add cachePerUser() if there are owner permissions, not always.
I can live with only having administer, fine. I think bypass is actually the easy part and administer is weird, at least for nodes. The entity API doesn't require you to have an administer permission anywhere. it just has a feature to have a simple one-permission-for-everything, how you name it is up to you.
I plan to open issues to replace that with more specific permissions and there are multiple bugs around administer nodes usage, for example #2779389: Node bulk delete confirmation from requires 'administer nodes' permission where it is simply incorrectly used. IMHO, administer xy is useful for a simple scenario where you have that to do everything and maybe 1-2 more, like view xy. But again, I can live with doing this ;)
Moving the generation of the permissions to the access control handler sounds nice to me, +1 :)
Comment #40
Berdir#2799209: Incorrect permission check in views node access filter is another one of those bugs, which I reported privately initially but was made public now.
Comment #41
mglamanFair enough.
Also, bojanz missed it, but my most recent patch moves permission generation into Entity API.
Comment #42
bojanz CreditAttribution: bojanz at Centarro commentedHere we go again.
1) After speaking with dawehner, decided to introduce a permission_provider entity handler, mirroring the route provider. Cleanest option so far (VS putting it on the access control handler, or forcing people to extend EntityPermissions).
2) Fixed berdir's cachePerUser() remarks.
3) Removed the bypass permission as agreed on.
4) Fixed the bundle permissions for entity types that don't use config entity type bundles.
Comment #43
bojanz CreditAttribution: bojanz at Centarro commentedImplemented the patch in Commerce, caught bugs in the permission names.
Also switched from plural labels to singular labels for the "any" permissions ("Create any product" instead of "Create any products").
We need to expand our test coverage to cover both entity type & bundle granularity, with and without owners. Currently not all combinations are there.
Comment #44
bojanz CreditAttribution: bojanz at Centarro commentedPermissions weren't sorted properly on the page because TranslatableMarkup objects don't sort, they need to be cast to string. Crazy.
Noticed that for bundle granularity we need a generic view permission, for various views. Perhaps it makes sense not to generate per-bundle view permissions for now?
Comment #45
holist CreditAttribution: holist at Siili Solutions commentedI talked with @dawehner and he told he was about to commit this and asked me to start an issue for porting the feature into core. A start on that is here: #2809177: Introduce entity permission providers
BTW documentation on the workings of this would really be nice.
Comment #46
dawehnerI think this really mostly makes sense for nodes displayed on the page, for which we have the node access system already, with all of its glory and shame.
Comment #47
bojanz CreditAttribution: bojanz at Centarro commentedRerolling.
Comment #48
bojanz CreditAttribution: bojanz at Centarro commentedHere we go.
1) Rewrote the tests. Both the access control handler and the permission provider now have unit tests with (what should be) full coverage. Looking at the EntityPermissionProviderTest also provides a good overview of what permissions are generated.
2) Removed the bundle specific view permissions, as agreed on.
3) Unified the (accidental) "create own" / "create any" permissions into a single "create" permission.
4) Fixed a bug in the create permissions for bundle-less entity types.
5) Stopped generating the "access $entity_type overview" permission for config entity types. A config entity type now gets the following permissions: administer, view, create, update, delete. Is that too much? Do we want to limit it to the administer permission only?
Comment #49
kristiaanvandeneyndeRe 5: What's the use of being able to see node types if you can't manipulate them? :) I'd say for config entities, 'administer' would suffice.
Comment #50
holist CreditAttribution: holist at Siili Solutions commentedHow about adding "view unpublished" permission, as worked on at #273595: Move permission "view any unpublished content" from Content Moderation to Node? Could it be added through here?
Comment #51
dawehnerFor me the default usecase for config entities are indeed really just "administer" and call it a day. I guess we should let people choose by using one handler or another.
Comment #52
BerdirIsn't that what we already do? If a single permission is all you need then core already provides that with the existing admin permission and the default access control handler. And generating a single permission doesn't seem worth it to me, that's more overhead than just defining it?
Comment #53
bojanz CreditAttribution: bojanz at Centarro commentedProblem is, there is no guarantee that a status field will use "unpublished" / "published" as the value labels. No API provides those labels, and it's an assumption Core didn't make previously. That might be changing with the work going on for 8.3.x, but I definitely need more feedback here before introducing it.
Comment #54
bojanz CreditAttribution: bojanz at Centarro commentedOkay, we decided to stop caring about config entity types completely. Removed the config entity from the test and the condition from the permission provider. Did my best with the docs, feel free to provide suggetsions.
Comment #56
dawehnerThank you a lot @mglaman and @bojanz!
Let's see us in the core issue queue.
Comment #58
bojanz CreditAttribution: bojanz at Centarro commentedNote that the commit landed in a phantom 8.x-2.x first, which we decided to remove in favor of the existing 8.x-1.x branch.