I am designing a per-document ACL for an app using MongoDB. It is trying to achieve the following characteristics:
- Any
findorupdate's query can be optionally extended with a specially-constructed ACL query to determine whether tofindorupdateis permitted. Hence a permission check doesn't increase the number of database queries, and for this particular app, does not generally reduce performance because of index intersection. - Documents support permissions for users who have no log in (
unauthorized), users who are logged in (authorized), and specific users. - Permissions can be specified as
allowordeny.
I am having trouble implementing deny permissions in a performant way.
How can I structure a MongoDB query to evaluate a permission chain with allow and deny specs?
An example document:
var PERMISSION = 1;
{
someData: "test",
// The ACL data structure
security: {
unauthorized: {
allow: [PERMISSION]
},
authorized: {
allow: [PERMISSION]
},
users: [
{_id: "userId", allow:[], deny:[PERMISSION]}
]
}
}
The intent here is that to all users who are unauthorized (have no logins) and users who are authorized (have logins), they can do PERMISSION to the document. But a particular user with the ID "userId" cannot do PERMISSION.
What MongoDB query would correctly evaluate this permission chain?
Here's the current query I have:
{"$or": [
// Top of the permission chain
{
"security.unauthorized.allow": PERMISSION,
"security.unauthorized.deny": {"$ne": PERMISSION}
},
// Middle of the permission chain
{
"security.authorized.allow": PERMISSION,
"security.authorized.deny": {"$ne": PERMISSION}
},
// Bottom of the permission chain
{
"security.users._id": "userId",
"security.users.allow": PERMISSION,
"security.users.deny": {"$ne": PERMISSION}
}
]}
When evaluating the chain from broadest-to-finest control, all allow evaluations work. But as soon as there is a deny permission deeper in the chain, this query doesn't work, like with the example above.
The thoughts for ACL design here are pretty interesting: Database schema for ACL. But this mostly discusses the mapping from something conceptual, like a role, to something concretely architectural, a permission. That problem for me is solved. I'm having trouble solving this deny permission problem without introducing another query with the chain inverted.
In particular, if there were a way that I could combine the array element match, "security.users._id": "userId" with the deny check "security.users.deny": {$ne: permission}, I would solve the deny problem—I could just check the finest-grain deny at the top of the chain and the finest-grain allow at the bottom.
If I had an $xor operator (http://en.wikipedia.org/wiki/XOR), I could do this:
{"$or": [
{
"security.unauthorized.allow": PERMISSION,
"security.unauthorized.deny": {"$ne": PERMISSION},
"security.authorized.deny": {"$ne": PERMISSION},
"$xor": [
{"security.users._id": "userId"},
{"security.users.deny": PERMISSION}
]
},
{
"security.authorized.allow": PERMISSION,
"security.authorized.deny": {"$ne": PERMISSION}
},
{
"security.users._id": "userId",
"security.users.allow": PERMISSION,
"security.users.deny": {"$ne": PERMISSION}
}
]}
I can build a $xor, but my suspicion is the performance characteristics will be very poor.
I'd accept a totally different schema and structure too.