System permissions are important. Defining what people can and can't do with your application is a significant part of security.
There are two perspectives I tend to care about with permissioning. The first is user-orientated and the second is data-orientated. In this article I will talk about designing a user-orientated permission system.
For the purposes of this post a permission will be considered a boolean value that represents whether a person can or can't perform an operation. In other systems you might go as far as to consider the extent to which they have permission which ends up working like a priority based permissiong system. This is only really useful in my opinion if you've an operation two people can perform at once and you wish to provide a fine grained hints to the system as to who should have the operation performed first. It's something to consider but usually unnecessary and out of the scope of this article.
Users and user-level permissioning
In a user-orientated permission system what we care about is what users can do with our system as a whole. Example permissions under this context include CanCreateUsers, CanViewUsers, CanUpdateUsers, CanDeleteUsers, CanInitiateReconciliation, etc.
Concepts that exist in this permission system include:
- User
- PermissionSet
The relationship between these concepts can be described as a one-to-one as every user should have a set of permissions (even if all those permissions are set to false!). At this point we can make a decision that because our permissioning system is simply we can combine these concepts into one system entity. This entity is described in the following ER diagram.
User groups and group-level permissioning
This will work well for simply systems with a few users. What happens when our systems become more complex and busy. Let's say that our system has so many users that we want to permission groups of them at a time. This might be by their role in the system or simply that an identified collection of users will always need the same permissions. We can create the concept of a UserGroup which defines global attributes available to all Users such as permissions.
Now we have three concepts of note.
- User
- PermissionSet
- UserGroup
Since none of these can be combined we end up with a structure that is described in the following ER diagram.
The relationship between UserGroup and User is a one-to-many. One UserGroup can have many Users. In this case we can note that the relationship between UserGroup and PermissionSet is a one-to-one relationship. We could combine these into a single UserGroup entity as we did in the previous structure. However if we keep them separate we can allow fine grain control over the user permissions.
User groups and user-level permissioning
Shown below we have created a subset of permissions specific to the user. This allows the UserGroup entity to take on a more simple role-based function within our application. Used to set blanket default permissions over a large collection of users that can then be fine tuned for a single user without having to create a new very-simplier but specialised UserGroup just for that single user.
You can see here how a user has a one-to-one relationship with a permission set and that a user group has a one-to-one relationship with a permission set. The relationship between user group and user remains one-to-many. In cases where we don't care about specific user permissions we decide to allow User.userPermissionSetId to be null as indicated by the empty white circle on the relationship between user and permission set. In the application there must be some logic to determine which permission set to use. If userPermissionSetId is null then follow through the user group to find the permission set.
Imagine this scenario. A user is set into a user group with no specific user-level permissions. The administrator then gives the user specific user-level permissions. At a later date the administrator alters the permissions of the user group. Since the user has specific user-level permissions these changes don't affect the user. This could probably be a problem in that it defeats the point of user groups. To keep the relevance of user groups after a user has been given user-level permissions we allow that our permissions in the permission set may be null. What this means to our application logic is that if a user-level permission is null then look for that permission at group-level regardless of whether user-level permissions were assigned as a whole. If we get up to group-level and find a null we just assume false for safetys sake.
Example program logic that resolves these permissions:
class PermissionSet: canCreateUsers = None canViewUsers = None canUpdateUsers = None canDeleteUsers = None canInitiateReconciliation = None class UserGroup: permissions = None def __init__(self, permissions): # This object can only be constructed if it gives permissions since the relationship between user group and permission set is mandatory self.permissions = permissions class User: group = None permissions = None def __init__(self, group, permissions = None): # This object can only be constructed if it gives a group since that relationship between user and user group is mandatory # User-level permissions are option and default to not-set self.group = group self.permissions = permissions def has_permission(self, permission_name): """Returns true if this user has the named permission. False or None otherwise.""" value = None if self.permissions is not None: # We've defined user-level permissions value = getattr(self.permissions, permission_name) if self.permissions is None or value is None: # Either we didn't define user-level permissions or we have no user-level permissions value = getattr(self.group.permissions, permission_name) return value
If you want to see the above functional structure in action you can use the following code for demonstration.
p1 = PermissionSet() p1.canCreateUsers = True p1.canUpdateUsers = True p1.canViewUsers = True p1.canDeleteUsers = False p1.canInitiateReconciliation = False p2 = PermissionSet() p2.canCreateUsers = False p2.canInitiateReconciliation = True group = UserGroup(p1) user1 = User(group) # Using the default group-level permissions user2 = User(group, p2) # Using mostly group-level permissions but with user-level refinements print "User1 canCreateUsers =", user1.has_permission("canCreateUsers") # True print "User2 canCreateUsers =", user2.has_permission("canCreateUsers") # False group.permissions.canCreateUsers = False # Change group-level permissions print "User1 canCreateUsers =", user1.has_permission("canCreateUsers") # False print "User2 canCreateUsers =", user2.has_permission("canCreateUsers") # False
Now we have a pretty flexible user-orientated permission system. It can be made even more reflexible if you allow users to have multiple groups or roles but when we get to this level of flexibility we introduce the concept of conflict resolution.
Imagine if you will a user with two roles, user manager and reconciliation administrator. The former role has the first four permissions set to true and canInitiateReconciliation remains null. The latter role has canInitiateReconciliation permission set to true but the rest to null. This is fine as is but what were to happen if somebody decided that the user manager role can definitely not initiate reconciliation and sets that permission to false? We end up with two permission sets for our user with a contradicting setting for canInitiateReconciliation. One possibly resolution is to simply say that if any role permission is true, then the user has permission to perform that operation. This decision lies in the application designer and out of the scope of this artile.
Generalising to hierarchical user entities
It would be nicer if we could create a hierarchy of permission levels rather than the two level system we had before where only user-level and group-level existed. To do this we generalise the concept of what a user and group is to what I'm going to call a user entity. Simply put the entity model can have a single parent and permissions set at any level of the entities parentage. You could then organise your permission scheme into a tree where the terminal leafs represent the users themselves. An example instance view could be a small business where the following diagram describes the users and groupings that exist.
Here we see that Randall and Doug are both part of the London development group, Andrew is part of the London sales group, Joey is part of the New York marketting group and Nigel is part of the New York branch of Example Enterprises. Each of these users inherit the permissions of their parent groups all the way to the very root group.
This allows us greater flexibility about how we organise and inherit permissions. Allowing us to specialise permissions near the bottom and generalising permissions near the top. For instance in the organisation hierarchy the overall policy of the company might deny everything except read permissions. The London development division might grant full permissions to faciliate their business role. The London Sales division might need to update records only so with inheritance they can read and update. Perhaps the marketting department are responsible for initiating a reconciliation but have no other interest in the system. They can enable reconciliation and disable read access.
In order to archieve this we can modify our system to the following:
We generalise both users and groups of users to simply be some sort of user entity that has permissions associated with it. We define users to be entities where no other entity inherits from them. We define groups to be entities that are referred to by at least one other entity. In reality in order to support empty groups we'd need some sort of flag to define the entity as a user or group.
The end result of this generalisation allows us to create this implementation of a hierarchical user-orientated permissioning system that supports permission inheritance.
class PermissionSet: """A set of permissions. Must be inherited and availablePermissions defined.""" # This is a list of the names of the available permissions availablePermissions = [] def __init__(self): """Constructor.""" self.permissions = dict() for perm in self.availablePermissions: # Initialise all permissions to undefined setattr(self, perm, None) # __setattr__ is just needed for ease of use in Python to allow the usage of PermissionSet().somePermissionName = True def __setattr__(self, name, value): """Set attribute.""" if name in self.availablePermissions: # Set a permission value self.__dict__["permissions"][name] = value else: # This is needed for internal operation of this class self.__dict__[name] = value # __getattr__ is just needed for ease of use in Python to allow the usage of print PermissionSet().somePermissionName def __getattr__(self, name): """Get attribute.""" if name in self.availablePermissions: # Get a permission value return self.__dict__["permissions"][name] else: # This is needed for internal operation of this class return self.__dict__[name] # __str__ is just needed to display all the permissions this set represents neatly as a string. def __str__(self): """Return all available permissions and their current values.""" return "\n".join([" %s = %s;" % (k, v) for (k, v) in self.permissions.items()]) class UserEntity: """An abstract hierarchical user entity.""" def __init__(self, name, permissionSetClass, permissions = None, parent = None): """Constructor.""" self.name = name # The name of this entity. self.permissions = permissions # A permission set instance. This should be of the same type as permissionSetClass or None. self.parent = parent # The parent object for this entity. self.permissionSetClass = permissionSetClass # The permission set that this identity references. def get_aggregate_permissions(self, aggregate = None, undefinedList = None, visited = None): """Returns an aggregate set of permissions. These represent the actual permissions this entity has in total.""" if visited is None: # This is used to guard against cyclic relationships and infinite recursion visited = list() if aggregate is None: # Construct our aggregate permission set aggregate = self.permissionSetClass() # Make a copy of the available permissions undefinedList = list(aggregate.availablePermissions) if self.permissions is not None: # If we have some level specific permissions copy them into the aggregate. newUndefined = list(undefinedList) for k in undefinedList: v = getattr(self.permissions, k) if v is not None and getattr(aggregate, k) is None: # k is set at this level and hasn't been set at a lower level setattr(aggregate, k, v) newUndefined.remove(k) # k is now defined undefinedList = newUndefined # Update what permissions remain undefined if self.parent is not None and self.parent not in visited and undefinedList: # This isn't a top level entity and our parent hasn't previously been visited. return self.parent.get_aggregate_permissions(aggregate, undefinedList, visited) else: # There is absolutely no more definitions for this aggregation at any level in the hierarchy return aggregate def has_permission(self, permission_name, visited = None): """Returns True if this entity has permission, False if this entity does not have permission and None if permission is undefined for this entity.""" value = None if visited is None: # This is used to guard against cyclic relationships and infinite recursion visited = list() if self.permissions is not None: # We have our own set of level specific permissions. # Check if we've got a value for the named permission. value = getattr(self.permissions, permission_name) if value is not None: # We've a level specific value for this permission, return it. return value if self.parent is not None and self.parent not in visited: # This isn't a top level entity and our parent hasn't previously been checked for the permission. # Let's defer the decision of this permission to our parent. return self.parent.has_permission(permission_name, visited) else: # There is absolutely no definition for this permission at any level in the hierarchy return None # __str__ is just needed to display all the permissions this entity defines neatly as a string. def __str__(self): """Return a printable string for this entity.""" return "%s '%s' {\n%s\n}" % (self.__class__.__name__, self.name, self.get_aggregate_permissions()) class UserGroup(UserEntity): """This class provides a concrete type for a user entity that represents a group.""" def __init__(self, name, permissionSetClass, permissions, parent = None): """Constructor.""" UserEntity.__init__(self, name, permissionSetClass, permissions, parent) class User(UserEntity): """This class provides a concrete type for a user entity that represents a user. The only requirement for this qualification is that no other entities reference this object as a parent.""" def __init__(self, name, permissionSetClass, group, permissions = None): """Constructor.""" UserEntity.__init__(self, name, permissionSetClass, permissions, group)
Example usage of this code is given below.
class ExamplePermissionSet(PermissionSet): """This is a specific set of permissions.""" availablePermissions = ["canCreateUsers", "canViewUsers", "canUpdateUsers", "canDeleteUsers", "canInitiateReconciliation", "neverDefined"] class ExampleUserGroup(UserGroup): """A specific user group class that uses ExamplePermissionSet.""" def __init__(self, name, permissions, parent = None): UserGroup.__init__(self, name, ExamplePermissionSet, permissions, parent) class ExampleUser(User): """A specific user group that uses ExamplePermissionSet.""" def __init__(self, name, group, permissions = None): User.__init__(self, name, ExamplePermissionSet, group, permissions) p1 = ExamplePermissionSet() p1.canInitiateReconciliation = False p1.canDeleteUsers = False p1.canCreateUsers = True p1.canUpdateUsers = True p1.canViewUsers = True p2 = ExamplePermissionSet() p2.canInitiateReconciliation = True p3 = ExamplePermissionSet() p3.canViewUsers = False p3.canCreateUsers = False supergroup = ExampleUserGroup("SuperGroup", p1) group = ExampleUserGroup("Group", p2, parent = supergroup) user1 = ExampleUser("User 1", group) # Using the default group-level permissions user2 = ExampleUser("User 2", group, p3) # Using mostly group-level permissions but with user-level refinements all = [supergroup, group, user1, user2] for e in all: print e print "user1 canDeleteUsers =", user1.has_permission("canDeleteUsers") print "-" * 30 group.permissions.canDeleteUsers = True # Change group-level permissions this should filter down to user1 for e in all: print e print "user1 canDeleteUsers =", user1.has_permission("canDeleteUsers") print "-" * 30
That's all folks! If I get motiviated I'll discuss data-orientated permissioning and the use of access control lists.