I am designing a per-document ACL for an app using MongoDB. It is trying to achieve the following characteristics:
- Any
find
orupdate
's query can be optionally extended with a specially-constructed ACL query to determine whether tofind
orupdate
is 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
allow
ordeny
.
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.