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