[PEAK] peak.security weirdness
Phillip J. Eby
pje at telecommunity.com
Thu Jan 19 13:21:45 EST 2006
At 03:00 PM 01/18/2006 +0100, Simon Belak wrote:
>from dispatch import generic, strategy
>from peak.api import security
>
>
>class SecurityContext(security.Context):
> pass
>
>security_context = SecurityContext()
>
>def hasPermission_adaptor(func):
> def hasPermission(self, user, perm, subject):
> return func()
> return hasPermission
>
>class Bar:
> """ Poor-man's hasPermission(). """
>
> @generic()
> def foo(self, user, perm, subject): pass
>
> @foo.when(strategy.default) def
> __denyByDefault(self,user,perm,subject): return
> security.Denial("Access
> denied.") @foo.when("perm==security.Nobody") def
> __nobodyGetsNobody(self,user,perm,subject): return
> security.Denial("Access
> forbidden") @foo.when("perm==security.Anybody") def
> __anybodyGetsAnybody(self,user,perm,subject): return True
>foo = Bar.foo.im_func
There's a bug in this code. When you define a generic function method in a
class, it implicitly adds a criterion to the rule that 'self' must be a
subclass of the containing class. In other words, you're adding a
strategy.default that *only* takes effect if 'self' (the security context)
is an instance of Bar! So, the rules being used in your
pseudo-hasPermission() are *not* the same as the ones used by
peak.security, which is why there's no ambiguity in your duplicate. In
this code, the context is *never* a Bar instance, so this default rule
never applies, and so it never conflicts with the GroupA rule.
>class SecurityGroupMeta(security.PermissionType):
>
> def __init__ (cls, name, bases, attr):
> super(SecurityGroupMeta, cls).__init__(name, bases, attr)
> if "require" in attr:
> foo.when(
> "getattr(perm, '__name__', '') == '%s'" %
> name)(
> hasPermission_adaptor(attr["require"]))
> security.hasPermission.when(
> "getattr(perm, '__name__', '') == '%s'" %
> name)(
> hasPermission_adaptor(attr["require"]))
Just FYI, the above is *way* more complex than it needs to be. A simpler
and more robust way:
security.hasPermission.when("perm is cls")(lambda *args: attr["require"]())
Remember that any symbol used in a "when" expression is *immediately*
evaluated unless it is an argument name, so 'cls' is treated as a constant
in the rule. Also, this way of doing the definition is less likely to be
ambiguous, since if you define two permissions with the same __name__, the
function will not be able to distinguish them, but the permission objects
themselves are unique. Also, this way will run faster because it does not
need to do a getattr() operation on each hasPermission() call. Instead,
the object's id() is just looked up in a dictionary.
>Traceback (most recent call last):
> File "C:\Documents and Settings\Simon\My
> Documents\hruska\vsemogoce\dispatch\weird_security.py", line 69, in ?
> print security_context.hasPermission(user, GroupA, subject)
> File "<string>", line 5, in hasPermission
> File "_speedups.pyx", line 362, in _speedups.BaseDispatcher.__getitem__
> File
> "c:\python24\lib\site-packages\RuleDispatch-0.5a0.dev_r2100-py2.4-win32.egg\dispatch\interfaces.py",
> line 15, in __call__
> raise self.__class__(*self.args+(args,kw))
>dispatch.interfaces.AmbiguousMethod: ([(Signature((0, <class
>'dispatch.strategy.Node'>)=Context), <function __denyByDefault at
>0x00B58970>), (Signature((6, <class
>'dispatch.strategy.node_type'>)=Inequality("..",(('GroupA', 'GroupA'),))),
><function hasPermission at 0x00C0B9B0>)], (<__main__.SecurityContext
>instance at 0x00B2AE40>, <object object at 0x009EC450>, <class
>'__main__.GroupA'>, <object object at 0x009EC458>),
If you read this closely, you'll see that the conflict is between a test
for __name__='GroupA', and the deny-by-default rule. The deny-by-default
rule applies to any security.Context instance, but your rule does not
specify what class the security context must be. That means that both
rules apply, and neither is more specific, since they can be true or false
independently of each other. In other words, they are ambiguous.
The trivial fix is to slightly modify my suggestion for defining the rules:
security.hasPermission.when(
"isinstance(self,SecurityContext) and perm is cls"
)(lambda *args: attr["require"]())
This would also work with security.Context, since it's still more specific
than the default rule (which is just "isinstance(self,security.Context)").
By the way, when you define a rule A, and want it to be more specific than
some rule B, there must never be a time when A could apply, but B does
not. In the specific problem you're having, rule A is the GroupA rule, and
rule B is deny-by-default. It's possible for rule B to not apply when rule
A does, because rule B requires 'self' to be a security.Context, and rule A
does not. Therefore, rule A is ambiguous with respect to rule B; the
system cannot conclude that whenever A is true, B is also true, and
therefore it cannot tell if A is more specific than B. If the reverse also
applies (B is not more specific than A), then the two rules are ambiguous.
The way to fix the ambiguity is to add all of rule B's conditions (or more
specific ones) to rule A. In this case, by adding an isinstance() check on
'self' for either security.Context or a subclass.
One of the things I'd like to do in future versions of RuleDispatch is have
a more explicit way to override rules when there's an ambiguity. For
example, a way to say "this method overrides that one", and have the
conditions automatically AND-ed for you, instead of you having to spell
them out. Also, sometimes there are rules that need to be prioritized,
where you first check for one thing, and then another. These situations
can be specified using "and" and "not" conditions in a relatively simple
way, but it tends to be verbose and error prone. In the "Cecil" language
that inspired RuleDispatch, there is a concept called a "classifier" that
allows you to list a series of priority-ordered rules and the language
automatically does the and's and not's for you. So, I'd like to add some
syntax sugar for that, too.
More information about the PEAK
mailing list