Hi all,

The current "security policy" abstraction in BFG is a bit brittle because it 
conflates authentication and authorization APIs.  Thus we get things like 
"RepozeWhoIdentityInheritingACLSecurityPolicy" and 
"RemoteUserInheritingACLSecurityPolicy".

I'd like to tease apart the bits of the current security policy abstraction 
related to authentication from bits related to authorization, so they can be 
overridden separately by a BFG application without needing to create a separate 
security policy each time either needs to change.

This is an explicit reversion on my thinking of it a year ago (I didn't think 
there needed to be any separation) but as I add more security policies, a 
natural break is becoming clearer.  The authorization stuff doesn't really need 
to change much; one policy would suffice for 90% of applications (the 
"inheriting ACL" policy).  But the APIs related to authentication require 
constant overriding.

So to the end of breaking them apart, I just wrote up a bit of science fiction 
in code form.  Could you take a look at the below and let me know what you 
think?

side note to Tres: Could you see what you think about the 
"IAuthenticationPolicy" implementation named "RepozeWhoAuthenticationPolicy" 
which presumes that the "api factory" is passed down in the environ, also 
science fiction at this point.

....

# Tweak BFG slightly to allow for separate authentication and
# authorization policies, so we don't have security policies named
# e.g. "RepozeWhoInheritingACLSecurityPolicy".  We'll add
# IAuthenticationPolicy objects and IAuthorizationPolicy objects;
# these will be adapters.  We'll also change ISecurityPolicy to be an
# adapter rather than a utility.  We'll tweak the function API to use
# these adapters.

# b/w incompats: the "authorization" policy needs access to the
# context, which the APIs it maps to in a current BFG security policy
# don't have; code which depends on these APIs will need to change.

from zope.interface import implements
from zope.interface import Interface

class IAuthenticationPolicy(Interface):
     """ A multi-adapter on context and request """
     def authenticated_userid():
         """ Return the authenticated userid or None if no
         authenticated userid can be found """

     def effective_principals():
         """ Return a sequence representing the effective principals
         (including the userid and any groups belonged to by the
         current user, including 'system' groups such as Everyone and
         Authenticated"""

     def challenge():
         """ Return an IResponse object representing a challenge, such
         as a login form or a basic auth dialog """

     def remember(self, principal, token):
         """ Return a set of headers suitable for 'remembering' the
         principal on subsequent requests """

     def forget():
         """ Return a set of headers suitable for 'forgetting' the
         current user on subsequent requests"""

class IAuthorizationPolicy(Interface):
     """ An adapter on context """
     def permits(self, principals, permission):
         """ Return True if any of the principals is allowed the
         permission in the current context, else return False """

     def principals_allowed_by_permission(self, permission):
         """ Return a set of principal identifiers allowed by the permission """

class ISecurityPolicy(Interface):
     """ A multi-adapter on context and request """
     def permits(permission):
         """ Returns True if the combination of the authorization
         information in the context and the authentication data in
         the request allow the action implied by the permission """

     def authenticated_userid():
         """ Return the userid of the currently authenticated user or
         None if there is no currently authenticated user """

     def effective_principals():
         """ Return the list of 'effective' principals for the request.
         This must include the userid of the currently authenticated
         user if a user is currently authenticated."""

     def principals_allowed_by_permission(permission):
         """ Return a sequence of principal identifiers allowed by the
         ``permission`` in the model implied by ``context``.  This
         method may not be supported by a given security policy
         implementation, in which case, it should raise a
         ``NotImplementedError`` exception."""

     def forbidden():
         """ This method should return an IResponse object (an object
         with the attributes ``status``, ``headerlist``, and
         ``app_iter``) as a result of a view invocation denial.  The
         ``forbidden`` method of a security policy will be called by
         ``repoze.bfg`` when view invocation is denied (usually as a
         result of the ``permit`` method of the same security policy
         returning False to the Router).

         The ``forbidden`` method of a security will not be called when
         an ``IForbiddenResponseFactory`` utility is registered;
         instead the ``IForbiddenResponseFactory`` utility will serve
         the forbidden response.

         Note that the ``repoze.bfg.message`` key in the environ passed
         to the WSGI app will contain the 'raw' reason that view
         invocation was denied by repoze.bfg.  The ``context`` object
         passed in will be the context found by ``repoze.bfg`` when the
         denial was found and the ``request`` will be the request which
         caused the denial."""

# an implementation of an authentication policy that uses repoze.who

class RepozeWhoAuthenticationPolicy(object):
     """ A BFG authentication policy which obtains data from the
     repoze.who API """
     implements(IAuthenticationPolicy)
     def __init__(self, context, request):
         self.context = context
         self.request = request
         from repoze.who.api import api_from_environ
         self.api = api_from_environ(request.environ)

     def authenticated_userid(self):
         identity = self.api.authenticate()
         if identity is None:
             return None
         return identity['repoze.who.userid']

     def effective_principals(self):
         effective_principals = [Everyone]
         identity = self.api.authenticate()
         if identity is None:
             return effective_principals

         effective_principals.append(Authenticated)
         userid = identity['repoze.who.userid']
         groups = identity.get('groups', [])
         effective_principals.append(userid)
         effective_principals.extend(groups)

         return effective_principals

     def challenge(self):
         return self.api.challenge()

     def remember(self, principal, token):
         return self.api.remember({'repoze.who.userid':principal,
                                   'password':token})

     def forget(self):
         return self.api.forget()

# an implementation of an authentication policy that uses a cookie

class StandaloneAuthenticationPolicy:
     """ A BFG authentication policy which obtains data from a cookie
     and a local storage system """
     def __init__(self, context, request):
         self.context = context
         self.request = request

     def _check_password(self, userid, password):
         """ Return true if the password is good for the userid  """
         raise NotImplementedError # you get the idea

     def _groups_from_userid(self, userid):
         """ Return a sequence of groups given a user id """
         raise NotImplementedError # you get the idea

     def _userid_from_login(self, login):
         """ Return a userid given a login name """
         raise NotImplementedError # you get the idea

     def _decrypt(self, cookieval):
         """ Return decrypted login and password"""
         raise NotImplementedError # you get the idea

     def _encrypt(self, userid, password):
         """ Return encrypted hash of userid and password for cookie val """
         raise NotImplementedError # you get the idea

     def authenticated_userid(self):
         cookieval = request.cookies.get('oatmeal')
         try:
             login, password = self._decrypt(cookieval)
         except:
             return None
         userid = self._userid_from_login(login)
         if self._check_password(userid, password):
             return userid

     def effective_principals(self):
         effective_principals = [Everyone]
         userid = self.authenticated_userid()
         if userid is None:
             return effective_principals

         effective_principals.append(Authenticated)
         groups = self._groups_from_userid(userid)
         effective_principals.append(userid)
         effective_principals.extend(groups)

         return effective_principals

     def challenge(self):
         userid = self.authenticated_userid()
         if userid:
             return render_template_to_response('templates/forbidden.pt')
         else:
             return render_template_to_response('templates/login_form.pt')

     def remember(self, principal, token):
         cookieval = self._encrypt(principal, token)
         return [ ('Set-Cookie', 'oatmeal=%s' % cookieval) ]

     def forget(self):
         return [ ('Set-Cookie', 'oatmeal=') ]

# an implementation of an authorization policy that uses ACLs

class ACLAuthorizationPolicy(object):
     implements(IAuthorizationPolicy)
     def __init__(self, context):
         self.context = context

     def permits(self, principals, permission):
         """ """
         # do stuff to figure out of any of the principals is allowed
         # the permission by any ACL in the current context's hierarchy

     def principals_allowed_by_permission(self, permission):
         """ """
         # return the sequence of principals allowed by the permission
         # according to the ACLs in the current context' hierarchy

# present a rolled up "face" to both authn and autz policies in the
# form of a "security policy"; users will interact with this API
# rather than the authn or authz policies directly.

class SecurityPolicy(object):
     """ Use separate authn and authz to form a BFG security policy;
     this will never be overridden, it is concrete. It is an adapter. """
     implements(ISecurityPolicy)
     def __init__(self, context, request):
         self.context = context
         self.request = request
         self.authn = getMultiAdapter((self.context, self.request),
                                      IAuthenticationPolicy)
         self.authz = getAdapter(context, IAuthorizationPolicy)

     def permits(self, permission):
         principals = set(self.authn.effective_principals())

         if authz.permits(principals, permission):
             return True

         return False

     def authenticated_userid(self):
         return self.authn.authenticated_userid()

     def effective_principals(self):
         return self.authn.effective_principals()

     def principals_allowed_by_permission(self):
         return self.authz.principals_allowed_by_permission()

     def forbidden(self):
         return self.authn.challenge()

     def remember(self, principal, token):
         return self.authn.remember(principal, token)

     def forget(self):
         return self.authn.forget()











_______________________________________________
Repoze-dev mailing list
Repoze-dev@lists.repoze.org
http://lists.repoze.org/listinfo/repoze-dev

Reply via email to