[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