I got an interesting question about handling complex security conditions that I thought would be perfect to illustrate the underlying design principles for Rhino Security.
The problem is securing surveys. A survey has a start date, an end date, can be marked as applicable to a specific population, may be public or private, etc. The specification says that a survey that the user does not own should only be visible to a user iff:
- The survey is public
- The survey is active
- The survey has started
- The survey has not ended
- The survey is for a population that the user is a member of
Can you imagine the amount of work that is required to set up this sort of rule properly? And apply it consistently all over the application? It is exactly for those sort of scenarios that I created Rhino Security.
Let us see how we are going to design the security infrastructure for the application. We are going to specify the following:
- The owning user for a survey is allowed access at a high level:
permissionsBuilderService .Allow("/Survey") // allow to do everything on a survey .For(survery.Owner) .On(survey) .Level(10) .Save();
- If the survey is for a particular population, that population is created as a user group, and we create a rule that allow that user group access to the survey:
permissionsBuilderService .Allow("/Survey/Vote") .For(survery.Population.Name) // the user group name .On(survey) .Level(1) .Save();
- The other permissions are specified globally, we need to do an and between all of them, and Rhino Security doesn’t give us direct support for that. What we do get, however, is Deny and levels, so we specify those permissions using the following:
permissionsBuilderService .Deny("/Survey/Vote") .For("Everyone") // all users .On("Inactive Surveys") // an entity group .Level(6) .Save(); permissionsBuilderService .Deny("/Survey/Vote") .For("Everyone") // all users .On("Private Surveys") // an entity group .Level(6) .Save(); permissionsBuilderService .Deny("/Survey/Vote") .For("Everyone") // all users .On("Completed Surveys") // an entity group .Level(6) .Save(); permissionsBuilderService .Deny("/Survey/Vote") .For("Everyone") // all users .On("Unstarted Surveys") // an entity group .Level(6) .Save();
Now, that we have set it up, what we need to do now is to add and remove the survey to the appropriate entity groups, based on whatever business conditions that we have. Note that those are explicit state transitions. This isn’t just us setting IsActive and IsPublic flag, or verifying date ranges. We have to take explicit action to add and remove the entities from the appropriate groups.
There are two reasons of doing things in this way, the first, it make it significantly easier to handle security, since we don’t have murky rules or implicit state transitions. Second, from a design standpoint, it leads to a situation where we don’t work with just dump objects and queries, but meaningful transitions between states.
Yes, that does implies that you would have to have some sort of a background process to move surveys between groups based on time. That is a good thing, but I talked about this in another post.