The PEAK Developers' Center   SecurityRules UserPreferences
 
HelpContents Search Diffs Info Edit Subscribe XML Print View

Implementing Access Control Rules with peak.security

Applications often need to provide different access to different users. In simple applications the access may be controlled by "security levels" that apply globally, but more complex applications may have security rules that depend on the state of application objects, or the relationship between a user and some set of objects. The peak.security framework provides tools for defining and using security rules in both simple and complex applications.

Table of Contents

Terms and Concepts

The peak.security framework allows an application to find out whether a user has permission to perform an action on some subject in some context. Specifically, an application asks a context whether the user has the permission for the subject, and receives either a true value (indicating success), or a denial object, indicating failure.

Let's look at each of these five concepts in turn, using the PEAK API to build our examples:

>>> from peak.api import security, binding

Users

peak.security doesn't have any predefined notion of what a "user" or "principal" is, so you can use whatever objects make sense for your application -- even strings or integers if you want! For our examples, we'll just make up a simple TestUser class, whose instances hold a name, and when printed, show that name:

>>> class TestUser:
...     def __init__(self, name):
...         self.name = name
...     def __repr__(self):
...         return self.name

Let's see if it works:

>>> Bob = TestUser("Bob")
>>> Bob
Bob

For our examples, we could've just as easily used strings, but any significant application is going to have some kind of user object with other behaviors besides just having a name. For example, they might be stored in a database or LDAP directory, and have methods to check or change their password or other login credentials.

(Note, by the way, that the security rule framework doesn't deal with how users "log in" or "authenticate"; that's entirely up to your application. Security rules just determine what a particular user can or can't do in the application. That is, security rules determine whether or not a user has permission to do something.)

Permissions

peak.security defines access rights using "permissions". A permission is just a symbol that can be used to identify 1) a group of similar users, 2) a role that users may have with respect to some object, or 3) a group of related privileges. Here are some examples of each:

>>> class Administrator(security.Permission):
...     """A member of the administrator group"""

>>> class Attendee(security.Permission):
...     """A person who will be attending a given workshop"""

>>> class EditTotals(security.Permission):
...     """Permission to perform various total-editing functions"""

In some security systems, these functions might be separately performed by "groups", "roles", and "permissions", but peak.security doesn't make such distinctions. You have complete freedom to include or exclude such ideas from your security model, according to the needs of your application.

By themselves, permissions have no meaning. Your application gives permissions meaning by checking whether the user has them, before allowing them to perform an action. Your security rules then determine whether the action will be allowed, by checking the state of the application or by checking for other permissions. For example, you could have a rule that says anyone who has the Administrator permission automatically has the EditTotals permission as well. (By having the rule that checks for EditTotals check to see if the user has the Administrator permission.)

For your convenience, peak.security also predefines a couple of built-in permissions for you, along with default security rules to make them work:

>>> security.Anybody
<class 'peak.security...Anybody'>

>>> security.Nobody
<class 'peak.security...Nobody'>

By the default rules, every user has the security.Anybody permission in relation to every object. So, you can use this permission to indicate public access. Conversely, no user has the security.Nobody permission in relation to any object, so you can use it to indicate data or operations that should never be accessed except by the application software itself.

Subjects

A "subject" is any object that the application deals with. There are no special requirements here; it can be quite literally anything. Your security rules may test for the type or value of the subject and inspect any of its attributes, in order to determine whether a user has a permission for the object. Ordinarily, subject objects will be instances of your application's "domain model" objects, which is to say the objects that users do things with, like Message objects in an e-mail program, or Posts in a blogging application. For our simpler examples, we'll just use an anonymous object at first:

>>> aSubject = object()

But later on we'll show some more sophisticated examples from an equipment tracking application.

Context

In order to determine whether a user has some permission for a subject, we must first have a context in which our security rules will apply. The class of the context object determines what security rules will apply; the context instance can also be used to hold references to domain-specific objects needed to evaluate security rules, such as access control lists, a database connection, etc. You could probably even create a context class that would automatically cache repeated permission lookups, if you wanted to, but we won't get into that degree of complexity in this document.

Typically, you will use a subclass of security.Context, but for our first examples we only want the default behavior and security rules, so we'll just use a simple Context instance:

>>> context = security.Context()

Security contexts provide two access control methods, both implemented as generic functions:

hasPermission(user,perm,subject)
Does user have permission perm for subject?
permissionFor(subject,name=None)
What permission is needed to access attribute name of subject? (This will be discussed further in the section on Linking Actions to Permissions. For now we'll be focusing exclusively on the hasPermission() method.)

Security rules are used to define the behavior of these methods, either globally, or for instances of a specific context class. You can place rules in any number of independent context classes, and then automatically combine them using multiple inheritance (as we'll see a little later on).

Now that we have a user, permissions, a subject, and a context, we can now ask security questions like, "Does Bob have the Anybody permission on aSubject?":

>>> context.hasPermission(Bob, security.Anybody, aSubject)
True

Yes, he does. As we said before, everybody has the security.Anybody permission on everything, because of a default security rule built in to the framework. Similarly, there's a default rule that says no user has the security.Nobody permission, for any subject:

>>> context.hasPermission(Bob, security.Nobody, aSubject)
security.Denial('Access forbidden')

And, by default, permissions are denied when no applicable rules are found:

>>> context.hasPermission(Bob, Administrator, aSubject)
security.Denial('Access denied.')

Denials

You'll notice that failed permission checks return security.Denial objects, rather than False. This is so that a user interface that's checking permissions can present the user with feedback about the issue, such as telling them what permissions are required, or requesting that they log in, use a more secure access method, etc.

Denial objects are false objects, so if you code something like if context.hasPermission(...), the if will only succeed if the user has the specified permission for the specified subject. Here are examples of all the features of security.Denial instances:

>>> d = security.Denial("No soup for you!")
>>> d.message
'No soup for you!'
>>> d           # repr()
security.Denial('No soup for you!')
>>> print d     # str()/unicode()
No soup for you!
>>> bool(d)
False
>>> not d
True
>>> if d:
...     print "Got soup!"
... else:
...     print "No soup.  :("
No soup.  :(

Denial objects don't really have any other special behaviors; they're really just a convenient way for a rule to provide a false value while also supplying an explanation of why the rule denied access. For convenience in displaying the denial message, you can either take its str() or unicode() value, or just use its message attribute, as appropriate for your application.

Access Rules

Access rules are defined by adding methods to the security.hasPermission() generic function. The when() clause for a rule can test for any or all of:

In order to determine whether the rule should be applied in a given case. The body of the rule (i.e. the method) should then determine whether the user actually has the permission for the subject, and return either True or a security.Denial instance.

(Note that you must not make this determination in the when() clause, or else you won't be able to return a Denial when the check fails. Only use the when() clause to determine whether the rule should be applied, not whether the rule will actually grant the permission.)

Let's look at some examples of defining and using access rules, so you can see how this works.

Basic Rules and Inheritance

The default security rules aren't very useful by themselves in a real application, so let's define something a little bit closer to what a real application might use. For example, your application model might give user objects a flag attribute indicating that they have Administrator privileges:

>>> class AppWithFlagRule(security.Context):
...     [security.hasPermission.when("perm==Administrator")]
...     def checkAdministrator(self,user,perm,subject):
...         if getattr(user,'isAdmin',False):
...             return True
...         return security.Denial("You must be an administrator.")
>>> app = AppWithFlagRule()
>>> Bob.isAdmin = True
>>> app.hasPermission(Bob, Administrator, aSubject)
True
>>> Bob.isAdmin = False
>>> app.hasPermission(Bob, Administrator, aSubject)
security.Denial('You must be an administrator.')

Or, perhaps your application has a list of administrators, loaded from a configuration file or database:

>>> class AppWithAdminList(security.Context):
...     def __init__(self,admins):
...         self.admins = admins
...
...     [security.hasPermission.when("perm==Administrator")]
...     def checkAdministrator(self,user,perm,subject):
...         return user in self.admins or security.Denial(
...             "You must be an administrator."
...         )
>>> app = AppWithAdminList([Bob])
>>> app.hasPermission(Bob, Administrator, aSubject)
True
>>> app.admins.remove(Bob)
>>> app.hasPermission(Bob, Administrator, aSubject)
security.Denial('You must be an administrator.')

As you can see, creating access rules is just a simple matter of adding methods to the existing generic function, security.hasPermission. You can define rules for a specific permission, or any permission, as well as for specific subject classes, or any subject. In essence, security rules can be almost completely arbitrary. For example, if we want to make our "Bob" user have every permission in a given context, we can do this:

>>> class BobRules(security.Context):
...     [security.hasPermission.when("user==Bob")]
...     def BobIsGod(self,user,perm,subject):
...         return True

And in this context, Bob will have any permission we can throw at him:

>>> bobsWorld = BobRules()
>>> bobsWorld.hasPermission(Bob, Administrator, aSubject)
True

But other users aren't so lucky in Bob's world:

>>> Susan = TestUser("Susan")
>>> bobsWorld.hasPermission(Susan, Administrator, aSubject)
security.Denial('Access denied.')

Notice that the default denial rule defined for security.Context still applies to the BobRules subclass. Let's create a similar context for Susan:

>>> class SusansPlace(security.Context):
...     [security.hasPermission.when("user==Susan")]
...     def SusanIsGodHere(self,user,perm,subject):
...         return True

>>> susaphone = SusansPlace()
>>> susaphone.hasPermission(Susan, Administrator, aSubject)
True
>>> susaphone.hasPermission(Bob, Administrator, aSubject)
security.Denial('Access denied.')

No surprises there. But what happens if we combine the two:

>>> class JointRule(BobRules, SusansPlace):
...     pass
>>> jointContext = JointRule()
>>> jointContext.hasPermission(Susan, Administrator, aSubject)
True
>>> jointContext.hasPermission(Bob, Administrator, aSubject)
True

The rules from both base classes apply to the joint subclass, so you can actually create modular, independent sets of security rules, and then combine them using inheritance in a straightforward way.

If you are using multiple inheritance, however, you must take care to ensure that you do not have conflicting rules defined for the base classes. For example, if both base classes had rules for when("perm==Administrator"), then you will have a rule conflict when checking the Administrator permission. To resolve the conflict, you must create a new rule in the combined subclass for when("perm=Administrator") that determines what the actual rule should be.

Semantic Rules

So far, we've only played around with rules that check for a specific user or permission, and we've stuck to using a single "subject" for our checks. But such rules aren't much use in a sophisticated application that needs permissions to be driven by the application semantics.

For example, consider an equipment inventory tracking system for IT equipment in a corporation's multiple data centers. Staff need to be able to check out batches of inventory items (such as hard drives, CPUs, memory, etc.) and install them in computers, or ship them to other facilities. A shipment is a specialized kind of batch, that has an origin and destination facility.

We will only allow someone with the Shipper permission on a given shipment to cancel that shipment. We will only allow someone with the Receiver permission to "receive" the shipment. But, we will consider anyone who has the Staff permission for the shipment's origin facility to be a Shipper for that shipment, and anyone who has the Staff permission for the destination facility to be a Receiver for that shipment. And, we'll consider a user to have the Staff permission for a facility, if he or she is listed in the facility's staff attribute:

>>> class Shipment:
...     def __init__(self, name):
...         self.name = name
...     def __repr__(self):
...         return self.name

>>> class Facility:
...     def __init__(self, name):
...         self.name = name
...     def __repr__(self):
...         return self.name

>>> class Shipper(security.Permission):
...     """User is a "logical sender" of the shipment"""

>>> class Receiver(security.Permission):
...     """User is a "logical receiver" of the shipment"""

>>> class Staff(security.Permission):
...     """User is a member of staff at a facility"""

>>> class ShippingRules(security.Context):
...
...     [security.hasPermission.when(
...         "perm==Shipper and isinstance(subject,Shipment)")]
...     def checkShipper(self,user,perm,subject):
...         return self.hasPermission(user,Staff,subject.fromFacility)
...
...     [security.hasPermission.when(
...         "perm==Receiver and isinstance(subject,Shipment)")]
...     def checkReceiver(self,user,perm,subject):
...         return self.hasPermission(user,Staff,subject.toFacility)
...
...     [security.hasPermission.when(
...         "perm==Staff and isinstance(subject,Facility)")]
...     def checkStaffMember(self,user,perm,subject):
...         return user in subject.staff or \
...             security.Denial(
...                 "%s is not a member of staff at %s" %(user,subject)
...             )

Now let's try them out:

>>> context = ShippingRules()
>>> NewYork = Facility("New York")
>>> Paris = Facility("Paris")
>>> NewYork.staff = [Bob]
>>> Paris.staff = [Susan]
>>> Shipment1 = Shipment("Shipment One")
>>> Shipment1.fromFacility = NewYork
>>> Shipment1.toFacility = Paris

>>> context.hasPermission(Bob, Staff, NewYork)
True
>>> context.hasPermission(Susan, Staff, Paris)
True

>>> context.hasPermission(Bob, Shipper, Shipment1)
True
>>> context.hasPermission(Susan, Receiver, Shipment1)
True

>>> context.hasPermission(Susan, Shipper, Shipment1)
security.Denial('Susan is not a member of staff at New York')
>>> context.hasPermission(Bob, Receiver, Shipment1)
security.Denial('Bob is not a member of staff at Paris')

Notice that we get back helpful messages explaining why Susan can't cancel the shipment, and Bob can't receive the shipment, because the shipper/receiver rules simply return the result of the nested permission check directly. (Always make sure your security checks return a helpful Denial, even if they have to be internationalized at some level of the system.)

By the way, note that in our example, it makes no sense to have the Staff permission for a shipment, or the Shipper permission for a facility:

>>> context.hasPermission(Bob, Shipper, NewYork)
security.Denial('Access denied.')

>>> context.hasPermission(Susan, Staff, Shipment1)
security.Denial('Access denied.')

So, you only need to define rules for combinations of permissions and subjects that make sense in your application. Anything you don't define rules for will just be denied.

Also notice that our example showed delegation from one permission to another. You can use this to implement concepts like, "people in Group X get permission Y", where X and Y are both permissions. Indeed, you can implement pretty much any imaginable discretionary access control model by defining appropriate rules in a context class.

Linking Actions to Permissions

So how do applications know what permissions to check for? You can, of course hardcode permission checks, but often it's easier to link operation or attribute names with permissions, as part of an application's class metadata. That way, other frameworks like peak.web can automatically figure out what permission is needed before allowing access to an attribute.

For example:

>>> class Foo:
...     binding.metadata(foobar=Administrator)
>>> aFoo = Foo()
>>> context.permissionFor(aFoo,"foobar")
<class 'Administrator'>

Note that attributes for which no permission has been defined return None from permissionFor:

>>> print context.permissionFor(aFoo,"noSuchAttribute")
None

This allows a framework to tell the difference between an attribute declared private (e.g. with permission of security.Nobody) and attributes which have not been declared. Under most circumstances, however, these are the same thing in terms of practical effect, and the default "no undeclared permissions" rule will ensure that this doesn't lead to unintended access:

>>> context.hasPermission(Bob, None, aSubject)
security.Denial('Access denied.')

But, it's good to know that the None return value exists if you're writing code that looks up permissions for names, especially if you need to distinguish between an attribute where access is possible (but denied) and an attribute for which no access is even theoretically possible.

The "Existence" Permission

As you can see, security.Permission subclasses are suitable for use as attribute metadata, so you can use the peak.binding metadata API to declare a permission for an attribute name in a given class (and its subclasses, unless overridden by a metadata definition in the subclass).

Sometimes, however, an application needs to know whether a user is even allowed to know that a particular object exists -- let alone access any of its attributes or operations! For example, the templating system in peak.web wants to ensure that it only displays items in a list that the user has permission to know about the existence of. The "existence permission" for a given object can be obtained by calling permissionFor without a name argument:

>>> context.permissionFor(aFoo)
<class 'peak.security...Anybody'>

Notice that by default, Anybody is allowed to know an object exists. This is because under most circumstances, mere access to an object does not provide a user with any ability to do something with it -- even view it. So, this is only needed in circumstances where non-application code (like the template system of peak.web) needs to filter a list of items based on permissions. You can define the existence permission for a class using binding.metadata or any of the related APIs like binding.declareMetadata:

>>> class Baz:
...     binding.metadata(Administrator)
>>> aBaz = Baz()
>>> context.permissionFor(aBaz)
<class 'Administrator'>

So, instances of our new Baz class should now only be visible in such an interface if the user has Administrator permission on them.

Enforcement and Packaging

Note that the access control rules framework in peak.security only handles the question of whether a user should have access. It does not enforce any restrictions in and of itself. Your application code must actually ask what permission(s) are required and whether the user has them, then grant or deny access as appropriate.

However, this doesn't mean you can't use or create a framework that does the enforcement automatically. For example, you could use Zope X3's "Proxy" type to enforce permissions by looking up the needed permission and checking whether the current user has it, before allowing access to an attribute. Also, peak.web does permission checks automatically before displaying any page, attribute, or view on an object, assuming that you've declared the permissions in your sitemap or in the objects' classes, and you've declared appropriate security rules to actually check the permisssions. For example, if we wanted to use our ShippingRules for a peak.web application, we'd do something like:

from peak.api import web

class ShippingPolicy(ShippingRules, web.InteractionPolicy):
    pass

and then add something like this to our application's .ini file:

[Component Factories]
peak.web.interfaces.IInteractionPolicy = myapp.ShippingPolicy

This would configure peak.web to use a web interaction policy that includes our shipping-related security rules, so it can automatically check permissions according to the rules we've defined.

Notice, by the way, that this means an application can have a set of core classes (like Shipment, Facility, etc.) with permission metadata for core concepts like Shipper, Receiver and Staff, but it's entirely independent of what rules are used to determine who gets those permissions!

Thus, you can create a core application with default security rules, but then allow a given installation to completely customize their business rules for what users are allowed to do -- as is usually needed for "enterprise" applications.

In addition, your core application is untouched by users' modifications, so upgrading the core application doesn't mean re-applying customization patches. Instead, the separately-packaged customer rules just need to add rules for any new permissions in the core application, and rules that rely on changed features in the application will need to be updated.


PythonPowered
EditText of this page (last modified 2005-01-06 13:34:22)
FindPage by browsing, title search , text search or an index
Or try one of these actions: AttachFile, DeletePage, LikePages, LocalSiteMap, SpellCheck