Short answer—keep your permissions separate from your data.
Long answer—you should use a lightweight permissions table that stores the relationships between groups and users and items. You should not use any permission-related sort key on your items, and unless you have a specific reason to allow items with the same Id (such as storing a versioned history of the items), you should probably not use any sort key at all for your items.
The permissions table has two attributes, which I’ll call entity (the hash key) and relationship (the sort key). (You could use whatever names you want, though.) This table also has a GSI where relationship is the hash key and entity is the sort key. Because the rows of this table are small, a query is very inexpensive. You could potentially read >100 rows and consume only 1 RCU.
The entity attribute is simply a userId or groupId. The relationship attribute is a composite of the relationship type and another ID. (You can use whatever you want for IDs, but I'll be using numbers for all of my examples.)
Here is some sample data:
entity | relationship
===========================================
user-0001 | member-of:group-1000
user-0001 | member-of:group-3000
user-0002 | can-access:item-1111
user-0002 | member-of:group-2000
group-1000 | can-access:item-1111
group-2000 | can-access:item-2222
To find out if a user has permission to access a given item, you need two queries. Query the permissions table for the userId and query the GSI for the “can-access” itemId relationship. Then, compare the two query results to see if there are any groups in common (or if the user has permission to access the item directly).
For example, if you wanted to see if user-0001 can access item-1111, you would query for user-0001 and get back [member-of:group-1000, member-of:group-3000]. Then you would query the GSI for can-access:item-1111 and you would get back [user-0002, group-1000]. Comparing the two results, you would see that user-0001 can access item-1111 by way of group-1000.
An additional benefit of this model is that it's flexible enough to handle other permission use cases. Here is an example of what that data might look like:
entity | relationship
===========================================
user-0001 | admin-of:group-1000
user-0001 | member-of:group-1000
user-0002 | member-of:group-2000
user-0002 | owner-of:item-1111
group-1000 | can-write:item-1111
group-1000 | can-read:item-1111
group-2000 | can-read:item-1111
In this example, we also have users who can administer groups, we've separated out the read and write permissions for items, and we can define a user or group as the owner of an item (which maybe means that they can modify who has permission to read or write the item).
One caveat is that this assumes groups cannot be nested. If groups can be nested, then you will need more queries to traverse the hierarchy of nested groups, and you might be better off using AWS Neptune or another database for storing your permissions.