Horizon: Coffeemug's permission system proposal RFC

Created on 9 Mar 2016  路  4Comments  路  Source: rethinkdb/horizon

I'd like propose a permission system spec as an alternative to #4. This is largely an object/role/ACL-based permission system.

Users, groups, and permissions

The system will have a notion of permissions that give users certain rights with respect to the specified collections or documents. Permissions can be coarse or granular. For example, a 'write' permission allows the user to run most modification operations. An insert permission only allows the user to insert a document. A coarse permission is essentially a porcelain over a set of more granular permissions to improve UX.

  • write -- includes permissions to insert, update, upsert, replace.
  • read -- a permission to read from a resource
  • grant -- a permission to grant permissions to other users; a given user may only grant permissions that she already has and cannot grant permissions that she doesn't have

The system will also have a notion of groups, which are objects that can contain users and other groups. Permissions can be granted to either users or groups. This allows easily updating permissions without having to change them on every relevant document.

Users and groups are special namespaces in horizon that act exactly like any other collection:

// Get all the users, assuming you have permissions to do it
hz.users.fetch();

// Get all the groups, assuming you have permissions to do it
hz.groups.fetch();

Collection-level permissions

The simplest way to grant permissions is to do it on the entire collection:

// Run this on the client to allow `user` to insert documents into the `wall` collection 
hz('wall').grant(user, { insert: true });

Permissions may be granted via a grant command that takes a user or group, and a document with specific permissions granted to that user or group wrt the collection. The specific user running the grant commands needs to have permissions to grant permissions.

Document-level permissions

Permissions may also be granted wrt specific documents instead of entire collections:

// Run this on the client to allow `user` to write to document
// with the `id == 1` on collection `wall`
hz('wall').find(1).grant(user, { insert: true });

Just like in the previous example, the user running the grant command needs permissions to actually grant permissions to another user.

Document-level permissions take precedence over collection-level permissions. To be able to run an operation, the user either must be allowed to do it on the collection level and not be forbidden on the document level, or allowed to do it on the document level.

Manipulating users and groups

By default we'll have a user admin that belongs to the group admin which is allowed to do everything.

Admins can allow other users to manipulate user and group namespaces:

// Allow `user_xyz` to create groups
hz.groups.grant(user_xyz, { insert: true });

TODO: spec out the structure of user and group documents. What do you have to write to the group document to add a user to the group?

Limiting default permissions

When a user inserts a document into a collection (assuming they have permissions to do that), by default they're granted all permissions they can possibly get on that document (including reading, writing, and granting permissions for that document to other users). Effectively this makes a user that inserts a document its owner, though there is no special notion of ownership in the system (they merely get all possible permissions by default).

However, under some circumstances an app might want to limit the notion of ownership (for example, if we want to allow users to add documents, but not delete them). To limit ownership we'll let horizon users specify permissions assigned to the "owner" of the document on insert in a config file (on a collection basis):

// permissions.config file
// Only let users insert documents but not edit them in a `wall` collection
wall = { insert: true }

Links

TODO: work this out.

Suppose we're creating a Trello-like app and have two collections: organizations and cards. A user creates an organization, and invites other users to it. To create/modify cards, users manipulate thecardscollection. For example, to add a card to an organization a user might insert a document{ task: 'foo', organization_id: org_id }document into thecards` collection.

Now we have a problem: how do we prevent rogue users from guessing random organization UUIDs and inserting tasks into other people's organizations? In this case we want to grant a permission to insert a card with a specific { organization_id: org_id }, but the permission system outlined here doesn't let users do it.

One option is to use cryptographically secure UUIDs that users would have to share with each other. Another option is to have a notion of links between collections and permissions on those links. Suggestions?

Validation

TODO: spec out a validation system

Most helpful comment

I like the proposal's simplicity, but I don't think it actually works for what we need.

Ownership-based authorization works extremely well for files, because it maps onto the domain well. I think it works much less well for apps, because you want to be authenticating operations instead.

I have a bunch of small objections, but the largest objection is that ownership-based authorization assumes that the creator of an object should be able to control what other people can do with it. For files, this is almost always true, so it works fine. For arbitrary objects in an application, it doesn't work at all.

For example, imagine you want your app to have an email-like messaging system. Sending a message would involve inserting an object into the messages collection, granting read and write permissions to another user, and then removing your own write permissions (since you shouldn't be able to delete messages from another user's inbox). But what if the client cheats? Or, ignore cheating, what if it crashes in-between creating the object and making some or all of the grant calls? What if it never calls grant to give the receiver of the message write permissions, for example? Would the message just hang around in the receiver's inbox forever because they don't have permission to delete it?

You could try and resolve this particular case by having the application logic check that the permissions on the document are correct before displaying it, but that's an enormous pain in the ass to do every time you read a document, and I'm pretty sure it doesn't work in the general case.

What you want is an authorization scheme that authorizes particular _queries_, because that sidesteps the entire issue. For example, with templating, you can have rules "It's legal to insert a message into the messages collection if the sender field is set to your user ID." and "It's legal to read or delete a message in the messages collection if the receiver field is set to your user ID.". There's no possibility of cheating or shitty edge conditions if a client crashes before calling grant, you just explicitly say what users are allowed to do and then they're allowed to do those things.


Anyway, here are my smaller thoughts on the proposal:

  • Why is the update permission different from replace? And why isn't upsert just insert AND update?
  • Letting groups contain other groups seems a little scary.

    • We have to recursively query to see who owns something.

    • We have to handle cycles.

  • I think that the Links session is actually reaching the same conclusion I did, which is that ownership semantics aren't powerful enough for some tasks.

    • Using cryptographically secure UUIDs probably isn't good enough, because you might want certain groups of objects to be world-readable without letting anyone who wants to add to them.

    • Granting permissions to insert a card with a particular organization_id is basically the templating proposal. (Or, at least, I think it will turn into the templating proposal when you generalize it. You'll want a way to grant permissions to insert things of a certain form, update things of a certain form, read things of a certain form, etc., and eventually you'll be back at query templates.)

All 4 comments

This sounds really good to me.

A few details that I didn't see in the proposal which we'll need to figure out:

  • When doing a query that fetches multiple documents, and the user doesn't have read permissions on one of them, do we filter it our error the full query?
  • If we error (which we probably should), are there ways in which an attacker could sabotage an application by revoking permissions on a document and thereby breaking such queries for other users? It sounds like there are such ways, as long as the user has permissions to change permissions on their documents (which they would have by default right?).
    A possible fix for this is to change the rule that per-document permissions overrule per-collection permissions. We could change this so that a user has permission to access a document, if they have permissions either on the collection, _or_ on the specific document (i.e. we use the more permissive of the two). This has the nice side-effect that we no longer need to check permissions on individual documents as soon as we have established permissions on the collection, which has some performance benefits.
  • We have to think a bit about how we implement checking for these permissions with different queries. It sounds like it's going to be pretty straight-forward, but there are some details we'll need to fill in.

I like the proposal's simplicity, but I don't think it actually works for what we need.

Ownership-based authorization works extremely well for files, because it maps onto the domain well. I think it works much less well for apps, because you want to be authenticating operations instead.

I have a bunch of small objections, but the largest objection is that ownership-based authorization assumes that the creator of an object should be able to control what other people can do with it. For files, this is almost always true, so it works fine. For arbitrary objects in an application, it doesn't work at all.

For example, imagine you want your app to have an email-like messaging system. Sending a message would involve inserting an object into the messages collection, granting read and write permissions to another user, and then removing your own write permissions (since you shouldn't be able to delete messages from another user's inbox). But what if the client cheats? Or, ignore cheating, what if it crashes in-between creating the object and making some or all of the grant calls? What if it never calls grant to give the receiver of the message write permissions, for example? Would the message just hang around in the receiver's inbox forever because they don't have permission to delete it?

You could try and resolve this particular case by having the application logic check that the permissions on the document are correct before displaying it, but that's an enormous pain in the ass to do every time you read a document, and I'm pretty sure it doesn't work in the general case.

What you want is an authorization scheme that authorizes particular _queries_, because that sidesteps the entire issue. For example, with templating, you can have rules "It's legal to insert a message into the messages collection if the sender field is set to your user ID." and "It's legal to read or delete a message in the messages collection if the receiver field is set to your user ID.". There's no possibility of cheating or shitty edge conditions if a client crashes before calling grant, you just explicitly say what users are allowed to do and then they're allowed to do those things.


Anyway, here are my smaller thoughts on the proposal:

  • Why is the update permission different from replace? And why isn't upsert just insert AND update?
  • Letting groups contain other groups seems a little scary.

    • We have to recursively query to see who owns something.

    • We have to handle cycles.

  • I think that the Links session is actually reaching the same conclusion I did, which is that ownership semantics aren't powerful enough for some tasks.

    • Using cryptographically secure UUIDs probably isn't good enough, because you might want certain groups of objects to be world-readable without letting anyone who wants to add to them.

    • Granting permissions to insert a card with a particular organization_id is basically the templating proposal. (Or, at least, I think it will turn into the templating proposal when you generalize it. You'll want a way to grant permissions to insert things of a certain form, update things of a certain form, read things of a certain form, etc., and eventually you'll be back at query templates.)

Now we have a problem: how do we prevent rogue users from guessing random organization UUIDs and inserting tasks into other people's organizations? In this case we want to grant a permission to insert a card with a specific { organization_id: org_id }, but the permission system outlined here doesn't let users do it.

@mlucy mentions this at the end of his comment, but I thought I'd tell you our three big requirements in our current Firebase-implemented app. The three most important things we use are:

  1. a notion of append-only with a monotonic key,
  2. switching permissions based on the shape of the data, and
  3. ensuring inserts and updates match a pattern (this is really only so that 1 and 2 can keep working)

Append-only is really important for log-based collaboration features. I can't let users insert things out of order.

The shape of the data is also critical:

  • Users can leave comments but only if comments are enabled
  • Users can schedule tasks for a particular time, but only a time in the future
  • Users can insert a move, but only if it's their turn
  • Users can message other users, but only if that user is in their friend list
  • Users can insert a post, but the post should have the system time in them, which users cannot know

Firebase security rules handle all of these very easily, and since the rules are not attached to the data, they are easy to understand & change. They are also clearly doing something very complex about transparently breaking documents apart for efficient updates & indexes, so maybe their security rules are not a helpful guideline.

I hope this is helpful!

Closing in favor of #199 . Feel free to open it again if anyone wants to discuss it more

Was this page helpful?
0 / 5 - 0 ratings

Related issues

arthurvi picture arthurvi  路  8Comments

danielmewes picture danielmewes  路  7Comments

thinklinux picture thinklinux  路  5Comments

marshall007 picture marshall007  路  8Comments

deontologician picture deontologician  路  3Comments