Run the example application provided in access2.py
and log in as admin
. You will see that besides Users and Accounts, you are presented with links to Roles and Permissions as well. If you click on Roles, you will see we have defined several roles:
As you can see in the screenshot, we have also defined a Superuser role to illustrate that it is possible to extend the concept of role-based access control to the maintenance of roles and permissions themselves.
Applications that use this form of access control have to be adapted only slightly. Take a look at access2.py:
Chapter9/access2.py
import os import cherrypy from rbacentity import AbstractEntity, Attribute, Picklist, AbstractRelation from browse import Browse from display import Display from logondb import LogonDB db="/tmp/access2.db"
Compared to our previous example, only the first part is different, in that it includes the rbacentity
instead of the entity
module. This module provides the same functionality as the entity
module, but the AbstractEntity
class defined in this module has some added magic to provide access to roles and permissions. We will not inspect that in detail here, but will comment on it when we encounter it in the following code.
The next part is the definition of the Entity
class. We could have opted for redefining the AbstractEntity
class, but here we have chosen to add the functionality to the Entity
subclass by adding and overriding methods where necessary:
Chapter9/access2.py
class Entity(AbstractEntity): database = db userentity = None logon = None @classmethod def setUserEntity(cls,entity): cls.userentity = entity @classmethod def getUserEntity(cls): return cls.userentity @classmethod def setLogon(cls,logon): cls.logon = logon @classmethod def getAuthenticatedUsername(cls): if cls.logon : return cls.logon.checkauth() return None def isallowed(self,operation): user = self.getUserEntity().list( pattern=[('name',self.getAuthenticatedUsername())])[0] entity = self.__class__.__name__ if user.name == 'admin' : return True roles = user.getRole() if len(roles): role = roles[0] permissions = role.getPermission() for p in permissions : if p.entity == entity: if p.operation=='*' or p.operation==operation: if p.level == 0 : return True elif p.level == 1: for owner in self.getUser(): if user.id == owner.id : return True return False def update(self,**kw): if self.isallowed('update'): super().update(**kw)
Instead of just defining a database
class variable, we now also define a userentity
class variable to hold a reference to the class of the entity that represents a user and a logon
class variable to hold a reference to a logon instance that can provide us with the name of an authenticated user.
This distinction is identical to examples in the previous chapters: we have a User
entity in our main database where we may store all sorts of information related to the user (like full name, telephone number, gender, and so on) and a separate password database that holds just usernames and encrypted passwords. If the user is correctly authenticated against the password database, we know his/her username, which we can then use to retrieve the corresponding User
instance with all the extra associated information. The class methods provide the means to get access to these class variables.
In this example, we only override the update()
method (highlighted) but in a full implementation you might want to override other Entity
methods as well. The pattern is simple: we call the isallowed()
method with an argument that indicates which action we would like to check and if isallowed()
returns True
, we call the original method.
The first thing the isallowed()
method itself does, is retrieve the username of the authenticated user with the getAuthenticatedUsername()
class method. It then uses this name to find a User
instance. Even though we might want to implement a role-based access scheme in our application to allow for the administration of roles and permissions by various users, we still provide a shortcut for the administrator here as a convenience (highlighted). This way we do not have to prime the database with roles and permissions for the admin user. For a real world application, you may choose differently of course.
Next we check if there are any roles associated with the user, and if this is the case, we retrieve all permissions associated with the first role (in this example, users have just one role). We then loop over all those permissions to check if there is one that applies to the entity we are interested in. If so, we check the operation
field. If this field contains an asterisk (*) or is equal to the operation we are checking, we look at the level
. If this level
is zero, this means that the current user may perform this operation on this entity even if he/she is not the owner. If the level is one, he/she is only allowed to perform the operation if he/she owns the entity, so we check whether the user is in the list of users associated with the current entity.
Retrieving roles and permissions each time an operation is performed might incur a serious performance hit. It might be a good idea to cache some of this information. You have to be careful though and invalidate that cache as soon as the set of permissions for a user changes.
The next part of access2.py
, as shown, illustrates how we may use this augmented version of the Entity
class:
Chapter9/access2.py
class Relation(AbstractRelation):
database = db
class User(Entity):
name = Attribute(notnull=True, unique=True, displayname="Name",
primary=True)
class Account(Entity):
name = Attribute(notnull=True, displayname="Name", primary=True)
class OwnerShip(Relation):
a = User
b = Account
class UserRoles(Relation):
a = User
b = User._rbac().getRole()
relation_type = "N:1"
logon = LogonDB()
Entity.setUserEntity(User)
Entity.setLogon(logon)
As before, we define User
and Account
entities, and an ownership relation between them. The rbacentity
module will have provided for Role
and Permission
classes and we can gain access to those with the _rbac()
class method available to all AbstractEntity
derived classes. The object returned by this _rbac()
method provides a getRole()
method that returns the class of the Role
entity. We use it here to create a relation between users and their roles (highlighted). The final lines associate the password database and the User
class with our new Entity
class.
To provide access to the lists of roles and permissions, we can use the same _rbac()
method to provide the Role
and Permission
classes needed to create Browse
classes:
Chapter9/access2.py
class RoleBrowser(Browse): edit = Display(User._rbac().getRole(), edit=True, logon=logon) add = Display(User._rbac().getRole(), add=True, logon=logon) class PermissionBrowser(Browse): edit = Display(User._rbac().getPermission(), edit=True, logon=logon, columns=User._rbac().getPermission().columns + [User._rbac().getRole()]) add = Display(User._rbac().getPermission(), add=True, logon=logon, columns=User._rbac().getPermission().columns + [User._ rbac().getRole()])
3.145.103.154