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 resourcegrant -- 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 haveThe 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
This sounds really good to me.
A few details that I didn't see in the proposal which we'll need to figure out:
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:
update permission different from replace? And why isn't upsert just insert AND update?Links session is actually reaching the same conclusion I did, which is that ownership semantics aren't powerful enough for some tasks.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:
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:
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
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
grantcalls? 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
senderfield is set to your user ID." and "It's legal to read or delete a message in the messages collection if thereceiverfield is set to your user ID.". There's no possibility of cheating or shitty edge conditions if a client crashes before callinggrant, 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:
updatepermission different fromreplace? And why isn'tupsertjustinsert AND update?Linkssession is actually reaching the same conclusion I did, which is that ownership semantics aren't powerful enough for some tasks.organization_idis 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.)