RavenDB Authorization Bundle Design
I used to be able to just sit down and write some code, and eventually things would work. Just In Time Design. That is how I wrote things like Rhino Mocks, for example.
Several years ago (2007, to be exact) I started doing more detailed upfront design, those designs aren’t curved in stone, but they are helpful in setting everything in motion properly. Of course, in some cases those design need a lot of time to percolate. At any rate, this is the design for the Authorization Bundle for RavenDB. I would welcome any comments about it. I gave some background on some of the guiding thoughts about the subject in this post.
Note: This design is written before the code, it reflect the general principles of how I intend to approach the problem, but it is not a binding design, things will change.
Rhino Security design has affected the design of this system heavily. In essence, this is a port (of a sort) of Rhino Security to RavenDB, with the necessary changes to make the move to a NoSQL database. I am pretty happy with the design and I actually think that we might do back porting to Rhino Security at some point.
Important Assumptions
The most important assumption that we make for the first version is that we can trust the client not to lie about whose user it is executing a certain operation. That one assumes the following deployment scenario:
In other words, only the application server can talk to the RavenDB server and the application server is running trusted code.
To be clear, this design doesn’t not apply if users can connect directly to the database and lie about who they are. However, that scenario is expected to crop up, even though it is out of scope for the current version. Our design need to be future proofed in that regard.
Context & User
Since we can trust the client calling us, we can rely on the client to tell us which user a particular action is executed on behalf of, and what is the context of the operation.
From the client API perspective, we are talking about:
using(var session = documentStore.OpenSession()) { session.SecureFor("raven/authorization/users/8458", "/Operations/Debt/Finalize"); var debtsQuery = from debt in session.Query<Debt>("Debts/ByDepartment") where debt.Department == department select debt orderby debt.Amount; var debts = debtsQuery.Take(25).ToList(); // do something with the debts }
I am not really happy with this API, but I think it would do for now. There are a couple of things to note with regards to this API:
- The user specified is using the reserved namespace “raven/”. This allows the authorization bundle to have a well known format for the users documents.
- The operation specified is using the Rhino Security conventions for operations. By using this format, we can easily construct hierarchical permissions.
Defining Users
The format of the authorization user document is as follows:
// doc id /raven/authorization/users/2929
{ "Name": "Ayende Rahien", "Roles": [ "/Administrators", "/DebtAgents/Managers"], "Permissions": [ { "Operation": "/Operations/Debts/Finalize", "Tag": "/Tags/Debts/High", "Allow": true, "Priority": 1, } ] }
There are several things to note here:
- The format isn’t what an application needs for a User document. This entry is meant for the authorization bundle’s use, not for an application’s use. You can use the same format for both, of course, by extending the authorization user document, but I’ll ignore this for now.
- Note that the Roles that we have are hierarchical as well. This is important, since we would use that when defining permissions. Beyond that, Roles are used in a similar manner to groups in something like Active Directory. And the hierarchical format allows to manage that sort of hierarchical grouping inside Raven easily.
- Note that we can also define permissions on the user for documents that are tagged with a particular tag. This is important if we want to grant a specific user permission for a group of documents.
Roles
The main function of roles is to define permissions for a set of tagged documents. A role document will look like this:
// doc id /raven/authorization/roles/DebtAgents/Managers
{ "Permissions": [ { "Operation": "/Operations/Debts/Finalize", "Tag": "/Tags/Debts/High", "Allow": true, "Priority": 1, } ] }
Defining permissions
Permissions are defined on individual documents, using RavenDB’s metadata feature. Here is an example of one such document, with the authorization metadata:
//docid-/debts/2931 { "@metadata": { "Authorization": { "Tags": [ "/Tags/Debts/High" ], "Permissions": [ { "User": "raven/authorization/users/2929", "Operation": "/Operations/Debts", "Allow": true, "Priority": 3 }, { "User": "raven/authorization/roles/DebtsAgents/Managers", "Operation": "/Operations/Debts", "Allow": false, "Priority": 1 } ] } }, "Amount": 301581.92, "Debtor": { "Name": "Samuel Byrom", "Id": "debots/82985" } //more document data }
Tags, operations and roles are hierarchical. But the way they work is quite different.
- For Tags and Operations, having permission for “/Debts” gives you permission to “/Debts/Finalize”.
- For roles, it is the other way around, if you are a member of “/DebtAgents/Managers”, you are also a memeber of “/DebtAgents”.
The Authorization Bundle uses all of those rules to apply permissions.
Applying permissions
I think that it should be pretty obvious by now how the Authorization Bundle makes a decision about whatever a particular operation is allowed or denied, but the response for denying an operation are worth some note.
- When performing a query over a set of documents, some of which we don’t have the permission for under the specified operation, those documents are filtered out from the query.
- When loading a document by id, when we don’t have the permission to do so under the specified operation, an error is raised.
- When trying to write to a document (either PUT or DELETE), when we don’t have the permission to do so under the specified operation, an error is raised.
That is pretty much as detailed as I want things to be at this stage. Thoughts?
Comments
How this relates to your recent post about not relying on infrastructure handling the authorization? I thought the application server is supposed tohandle authorization, Raven is clearly a part of infrastructure here and shouldn't be involved in auth at all.
'When performing a query over a set of documents, some of which we don’t have the permission for under the specified operation, those documents are filtered out from the query.'
-I can see why it would behave this way but it could be nasty as it is sort of a 'silent' failure. Security concerns are effectively filtering the data set. Maybe a hard error should be configurable?
Rafal,
Two distinct things. The first is how you use authorization, how you set things up, etc.
The second is how you implement authorization, and that should be in the infrastructure.
Using auth should be a high level concept.
Rhino Security & the Authorization Bundle design both fall into this campl.
Bob,
That is why it is optional, you have to explicitly want to use security for this behavior to kick in.
And note that there is a difference between filter a query and accessing a secured document explicitly.
In the first case, we assume that you want to query stuff and filter out things you don't have access to.
In the second, you obviously trying to access something that you don't have rights to, at which point we throw.
@Ayende
I don't think so. You've pushed some idea of 'permissions' into Raven that are meaningless for a document database. What does 'Operations/Debt/Finalize' permission mean for a document db? Nothing, I suppose. I think there should be a fixed set of operations relevant to a document db (read, insert, update, delete, query, etc) and the authorization implementation should allow you to grant/deny rights to execute these operations using Raven API. In your approach you just moved a part of application's logic into the database, but it's not a complete security solution - you'll have to duplicate some security code in application anyway.
This security model is very general and yet simple and pragmatic. I really like it. However this hat little to do with RavenDB. Nothing in this concept depends on it (except filtering documents), it could easily be generalized to any IAuthorizationStore. So I would advocate implementing it as a provider-agnostic lib first.
And I just realize that I am in line with Rafal. I think the only Raven specific thing about this model is the filtering of documents. Ayende, could that be done at the client level too or would that be much slower? How would you implement it? Could you leverage server-side caches or a more efficient algorithm? At first sight it would seem that you would have to make a loop join to the authorization documents that could be done at the client level too by making a second request.
Sorry if I'm missing something obvious but how in the world do you ensure this??? (trusting the client)
"Since we can trust the client calling us"
Thanks
/Jonas
Rafal,
Absolutely nothing!
It isn't the DB that gives meaning to this, but the application using it.
Doing read/write/delete/query permissions leads to exactly the type of system that I described in my earlier post.
You can write to the document to add background information about the debt, but you can't write to the document to finalize the debt.
If you are limited to just "write" permission, then this is useless in most common business scenarios.
This is how Rhino Security has been working for a long time, and it works, very nicely, too.
Tobi,
No, it can't really be done in an agnostic way.
Rhino Security and RavenDB Authorization Bundle both share the same general logic, but the implementation is drastically different because of the different nature of the two data stores.
Jonas,
That is simple, you are the client :-)
In other words, this is applicable when you have:
User -} App Server -} RavenDB
And no user can access RavenDB directly.
Since we wrote the app server code, we know that we can trust it.
That is an extremely common deployment scenario, so that is not a big issue.
I'm still confused..... If "no user can access RavenDB" how can your app server access it????
/Jonas
Jonas,
A user can't access RavenDB, because there is a firewall in the middle.
The app can, because the firewall allows it.
Ayende,
:-)
1) OK so "someone" can talk to the app server. (that would be my client code)
2) The app server will "forward" this request to RavenDB. behind the firewall
How do you ensure that the message going from my client to your app server is "kosher"????
/Jonas
Jonas,
I don't. The client doesn't talk to Raven.
The client make requests to the app server to do some application operation.
The app server then does it things, and uses the user context and the current operation to decide what security should be applied.
Then the auth bundle enforces that decision.
OK Thanks ;)
a real off-topic (the theme is close), what would you suggest as a membership provider for ASP.NET MVC v2 project using RavenDB ?
I don't really know if it is possible to write membership provider against RavenDB datastore, so in the end I end up using some "SQL" database anyway (say Postgre), which I wouldn't want (due to scalability and I want to rely completely on RavenDB only).
Thanks
Uzivatel,
Someone created a membership provider for raven.
If you ask in the user group, maybe he'll share it.
And yes, it is entirely possible
@ayende: wow that's great! RavenDB rocks!
Comment preview