diff options
Diffstat (limited to 'repoze/bfg/scifi.py')
| -rw-r--r-- | repoze/bfg/scifi.py | 257 |
1 files changed, 257 insertions, 0 deletions
diff --git a/repoze/bfg/scifi.py b/repoze/bfg/scifi.py new file mode 100644 index 000000000..dcc8fa1c1 --- /dev/null +++ b/repoze/bfg/scifi.py @@ -0,0 +1,257 @@ +# 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() |
