[PEAK] peak.security

Phillip J. Eby pje at telecommunity.com
Fri Dec 5 13:21:51 EST 2003


At 09:07 AM 12/5/03 -0600, darryl wrote:

>If i understand the basic idea behind peak.security it can be used for things
>other than web interactions correct?
>
>Would anyone be willing to give a basic (simpler the better) overview of 
>how one can use it?

As always, the best place to start is with the interfaces module for the 
package.  The peak.security interfaces are:

Access Control
--------------
IInteraction - "a security-controlled user/app interaction".  An 
interaction provides the necessary context to identify what security rules 
should be used, and on whose behalf the action is being performed (i.e. the 
principal).  To determine if an access is allowed, you use the 
interaction's 'allows()' method.

IAccessAttempt - "An attempt to access a protected object".  An 
IAccessAttempt holds data specific to a particular access attempt, for the 
benefit of permission checkers (see IPermissionChecker).

IAuthorizedPrincipal - a principal (e.g. user) that may be globally granted 
or denied a permission

IPermissionChecker - "An object that can verify the presence of a permission"


Permissions
-----------
IAbstractPermission - a conceptual permission or role, like "View content" 
or "Content Manager"

IConcretePermission - a permission in context of an object type, like "View 
content for Document", or "Content Manager of Folder"


Protected Objects
-----------------

IGuardedObject - "Object that knows permissions needed to access subobjects 
by name"

IGuardedClass - "Class that can accept permission declarations for its 
attributes"

IGuardedDescriptor - "Descriptor that knows the permission required to 
access it"


So this is how the parts fit together: you use an IInteraction to check 
security.  The IInteraction has to know the user, and what protocol (rules) 
will be used to check permissions.

To check permissions, you have to have permissions.  You create these by 
subclassing security.Permission, e.g.:

     class ViewContent(security.Permission):
         """Permission to view an object's content"""

That's all it takes, because permissions are just markers for a concept.

Note that "permission" here is a catch-all for what might be called groups, 
roles, permissions, or ACLs.  It can be whatever you want it to mean, 
because it's the *rules* that give life to a permission.  The permission by 
itself means nothing.  You define required permissions on your domain 
objects or views, but then the security rules determine who has those 
permissions in relation to specific objects.

To do this, "concrete permissions are used".  To make a "concrete 
permission" for our new 'ViewContent' permission, we do something like:

     viewContentOfDocument = ViewContent.of(Document)

That is, 'permission.of(type)' returns a concrete permission, against which 
rules can be declared.  (You can also declare rules directly for 
ViewContent or other abstract permissions, and they will apply if there is 
no rule registered for a more-specific type that applies to the object 
being checked.)

So how do you define rules?  Subclass RuleSet, as in this example from 
peak.security's own code:

class Universals(RuleSet):

     rules = Items(
         allowAnybody = [Anybody],
         denyEverybody = [Nobody],
     )

     def allowAnybody(klass, attempt):
         return True

     def denyEverybody(klass, attempt):
         return False

Universals.declareRulesFor(IPermissionChecker)

When you subclass RuleSet, you'll need to define a 'rules' member that 
lists (method name, permissionsList) pairs.  (It's easiest to use 'Items()' 
for this, as shown.)  You then define methods that take an 'attempt' 
('IAccessAttempt') object.  (Notice, btw, that RuleSets are singletons, and 
so all the methods are technically class methods.  But you don't have to 
declare them as such.)

Each method should inspect the IAccessAttempt it's given, and approve it or 
deny it, by returning a true or false value.  When returning a false value, 
it's best to instantiate a 'security.Denial()' instance describing the
failure, e.g.:

class MyContentRules(RuleSet):

     rules = Items(
         membersOnly = [ViewContent.of(Document)]
     )

     def membersOnly(klass,attempt):
         if not attempt.user.isMember:
             return security.Denial("Only site members may view documents")
         return True

MyContentRules.declareRulesFor(IPermissionChecker)


The above creates a ruleset with a method that will be called whenever the 
ViewContent permission is requested for a Document (or subclass) 
instance.  Notice that rules can take advantage of application-specific 
knowledge.  The 'user' of an interaction or access attempt can be an 
application-specific type, with whatever members or methods you like.

The unit tests for the security package include a detailed example of 
security rules based on an application-specific data model, using object 
relationships (such as who works at what facility, and what facility a 
shipment is at) to determine what permissions a person should have, 
relative to the contents of shipments or batches of equipment.  This lets 
you fully represent whatever logical business rules your application may 
need, including such things as "grant permission A over object X if you 
have permission B over object X.Y and permission C over object X.Z".


Going back to the last two examples in this posting, you may have noticed 
the 'declareRulesFor()' voodoo at the end of each of these examples.  This 
is important for being able to reuse, override, and extend the rules used 
by different applications.

Let's say that you create a set of default rules for a new content 
management system, and distribute it to other users.  How will they change 
your rules?  Modifying source is a bad idea.  So, we use protocols 
(interfaces) to define a "context" for rules to be registered 
in.  Normally, one uses 'IPermissionChecker' as a kind of global default 
context to register security rules in.  However, if you need to use 
non-default rules, you must create a new "context", using either 
protocols.Variation, or simply creating a new interface.  Here's an example:

IPermissionCheckerForMySite = protocols.Variation(IPermissionChecker)

We just created a variation of 'IPermissionChecker' that is specific to our 
site.  We then can create new rulesets and call their 
'declareRulesFor(IPermissionCheckerForMySite)' methods.  Because our new 
protocol is a variation of 'IPermissionChecker', rule sets declared for 
'IPermissionChecker' will be "inherited" by our new protocol, unless 
overridden by a rule declared explicitly for the new protocol.

Now, if we did not want to inherit those existing rules, we could either 
*subclass* IPermissionChecker, e.g.:

     class IPermissionCheckerForMySite(IPermissionChecker):
         pass

or just create a whole new interface.  It doesn't much matter.  The new 
interface would not be implied by IPermissionChecker, so the default rules 
wouldn't apply.

So how do you actually check these permissions?  That's easy:

if Interaction(user=person).allows(subject,name):
     # allowed...
else:
     # denied...

Of course, if you're using a custom permissionProtocol, it's a bit more 
complex:

if Interaction(
     user=person, permissionProtocol=IPermissionCheckerForMySite
).allows(subject,name):
     # allowed...
else:
     # denied...

Of course, you'll probably just do this:

class MySiteInteraction(security.Interaction):
     permissionProtocol = IPermissionCheckerForMySite

and then:

if MySiteInteraction(user=person).allows(subject,name):
     # allowed...
else:
     # denied...

In practice, though, you'll do this a bit more like:

# do this once, then reuse 'theInteraction' each time we need a check
# for this user
theInteraction = MySiteInteraction(user=person)

# keep the result so we can use the message...
allowed = theInteraction.allows(subject,name)

if allowed:
     # do it
else:
     # use 'allowed.message' (if present) to tell the user what's wrong


Oh, I just realized I haven't talked about 'subject' and 'name' yet.  That 
brings us to the last set of interfaces, the "protected object" 
interfaces.  The 'subject' is the object that the user is trying to do 
something with, and 'name' is the name of the thing they want to access, or 
'None' if you just are checking access to the raw thing itself.  Note that 
these "names" are not intrinsically tied to methods or 
attributes.  peak.security doesn't provide any facilities to wrap or proxy 
object access in that fashion, although you could probably use Zope 3's 
security proxy classes in conjunction with peak.security to implement 
"untrusted" security within an application.

Anyway, names are just names.  What names you check is up to your 
application.  However, if an object implements (or is adaptable to) 
'IGuardedObject', then it can declare what permission is required for a 
given name (or 'None').  In practice, you don't usually need to directly 
support 'IGuardedObject'.  Instead, you use 'security.allow()':

     class SomeClass:
         security.allow(
             index_html = ViewContent
         )

By a complicated process of adaptation, 'security.allow()' first adapts 
'SomeClass' to 'IGuardedClass', then tells the adapted class what 
permissions it should use.  The default adapter to 'IGuardedClass', which 
works on any classic or newstyle class, responds by declaring an adapter to 
'IGuardedObject' for instances of the class, that knows about the declared 
permissions.  So, if you use 'security.allow()' to declare permissions for 
a class, then its instances will know what permissions should be checked by 
Interaction.allows().  (Of course, Interaction.allows() can be explicitly 
told to check a specific permission, ignoring the subject's permission 
information.)

Anyway, you only need to know about IGuardedObject or IGuardedClass if 
you're *not* using 'security.allow'.  Once you've used 'security.allow' in 
at least a class, it and all its descendants can use an additional method 
of specifying permissions: the 'permissionNeeded' attribute of methods, 
bindings, and features.  For example:

class SomeComponent(binding.Component, SomeClass):
     something = binding.Obtain(IFooBar, permissionNeeded=ViewContent)
     # ...

class SomeElement(model.Element, SomeClass):

     class something(model.Attribute):
         permissionNeeded = ViewContent
         # ...

class SomeSubclass(SomeClass):

     def something(self,foo):
         # ...

     something.permissionNeeded = ViewContent

These examples all declare that the name 'something', when checked for 
access on an instance of the appropriate class, will require the 
ViewContent permission.  (Note that since these examples don't use 
'security.allow()', I have them subclass 'SomeClass'.  Normally, you can 
just use 'security.allow()' in every class that needs it.  But, sometimes 
it's more convenient to declare a permission with the thing it protects, 
and so you can use the 'permissionNeeded' approach in these cases.)

Notice, by the way, that you always declare *abstract* permissions on 
objects, not concrete ones.  The permission machinery will convert these to 
concrete permissions for the appropriate class later.  This ensures that, 
for example, a subclass of SomeComponent will be checked for concrete 
permissions on *that* subclass, not concrete permissions of 
SomeComponent.  Thus, rules for subclasses override rules targeted at base 
classes.

I mentioned previously that 'name' can be 'None', but I haven't said how 
you define what permission is used for it.  'security.allow()' can accept 
an optional (positional) parameter to specify this, so 
'security.allow(ViewContent)' would mean that the containing class' 
instances would use 'ViewContent' as the permission for accessing the 
object itself, with no name.

Again, let me remind you that what that "means" is up to your 
application.  'peak.web' uses non-None names for checking permission to 
traverse to a name, and uses 'None' as 'name' when filter items found in a 
list or mapping.  So, in that context, the 'None' name means "whether 
someone is even allowed to know the object exists, let alone do something 
with it."  However, you're not required to follow that convention in your 
own uses of peak.security.

Speaking of peak.web, peak.web extends the IInteraction interface to 
include more web-specific goodies, and it automatically creates an 
interaction for each web hit.  There is also a hook for you to define an 
"authentication service" that will be used to figure out what user object 
should be used in the interaction.  Note that authentication is entirely 
separated from authorization, and so an authentication service need only 
identify the user object.  Your security rules then do the rest.




More information about the PEAK mailing list