1

I am designing a per-document ACL for an app using MongoDB. It is trying to achieve the following characteristics:

  1. Any find or update's query can be optionally extended with a specially-constructed ACL query to determine whether to find or update 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.
  2. Documents support permissions for users who have no log in (unauthorized), users who are logged in (authorized), and specific users.
  3. Permissions can be specified as allow or deny.

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.

Community
  • 1
  • 1
DoctorPangloss
  • 2,994
  • 1
  • 18
  • 22

1 Answers1

0

Here is a basic solution, though it has some bad performance characteristics.

var generateAclQuery = function (userId, permission) {
    // If userId is specified, check the whole permissions chain
    return userId ? {
        $or: [
            // In the permission chain, we evaluate from the broadest permission down to the finest.
            {
                "security.unauthorized.allow": permission,
                "security.unauthorized.deny": {$ne: permission},

                // We have to check if at a finer level, no deny occurs
                "security.authorized.deny": {$ne: permission},
                $or: [
                    {"security.users._id": {$ne: userId}},
                    {"security.users._id": userId, "security.users.deny": {$ne: permission}}
                ]
            },
            {
                "security.authorized.allow": permission,
                "security.authorized.deny": {$ne: permission},

                // Again, check if at a finer level, no deny occurs
                $or: [
                    {"security.users._id": {$ne: userId}},
                    {"security.users._id": userId, "security.users.deny": {$ne: permission}}
                ]
            },
            {
                "security.users._id": userId,
                "security.users.allow": permission,
                "security.users.deny": {$ne: permission}
            }
        ]
    } : {
        // Otherwise, if userId is null or undefined, only check the unauthorized permission
        "security.unauthorized.allow": permission,
        "security.unauthorized.deny": {$ne: permission}
    }
};
DoctorPangloss
  • 2,994
  • 1
  • 18
  • 22