September 06, 2010
Note
The code for this feature originally appeared on the wiki but code in a testable template is better than copying and pasting from a wiki page.
Warning
The shabti_auth_xp template is experimental (i.e. for study purposes only) and will be merged with the shabti_auth template in future versions.
Note
shabti_auth_xp source code is in the bitbucket code repository
The shabti_auth_xp template includes code for row-level permissions. This allows the developer to set permissions on individual instances rather than just system-wide permissions. For example, a NewsItem class might have edit and delete permissions set for each NewsItem instance.
The permissions system is implemented as an Elixir Statement, has_permissions. The relevant code is located in model/permissions.py in the Shabti application package.
Elixir statement for handling row-level permissions. Creates associated tables using associable using {{package}}.model.user.Permission Entity class.
has_permissions takes a list of permission names (or name/description tuples) and three optional callbacks:
precheck(self, principal, permission_name) :: checked before actual permission checking. If returns True, permission check is halted and True returned. Argument principal may be User or Group instance.
postcheck(self, principal, permission_name) :: checked after actual permission checking, if check returns False. Can therefore override permission result if False.
default_permissions(self) :: assign permissions to user(s) and/or group(s) once permissions for that instance have been created.
from elixir import *
from permissions import has_permissions
class NewsItem(Entity):
has_field('title', Unicode(100))
has_field('content', Unicode)
has_field('created', DateTime, default=datetime.now)
belongs_to('office', of_kind='Office')
belongs_to('author', of_kind='{{package}}.model.User')
has_permissions(['edit', 'delete'],
precheck='_is_author',
postcheck='_check_office_permission')
def _is_author(self, principal, perm):
if self.author == principal: return True
def _check_office_permission(self, principal, perm):
if self.office:
return self.office.has_permission(perm, principal)
One feature of this system is that permission checking can be overridden, for example granting default edit permission to the author of a NewsItem. This makes it quite flexible in more complex situations.
In addition the shabti_auth_xp template provides a sub-class of BaseController, ModelController. The ModelController instance will automatically look up an instance of the given model class if the id parameter is passed in the URL, also handling permission checks if needed.
Note
The implementation of ModelController has evaporated into the aether and can no longer be found in any repository. At some point in the future, it will be re-coalesced.
Note
Late-breaking news, the ModelController has been discovered living an obscure and quiet existence in an old revision. Some therapy is being attempted.
from authprojectname.lib.base import *
class AuthController(BaseController):
__permission__=permissions.SignedIn()
__excludes__=['post', 'add_user']
def index(self):
return render('/index.mako')
@authorize(permissions.InGroup('Admins'))
def post(self):
return 'ok'
@authorize(permissions.HasPermission('add_users'))
def add_user(self):
return 'ok'
Coupled with the following extended BaseController operations:
"""The base Controller API
Provides the BaseController class for subclassing.
"""
from pylons.controllers import WSGIController
from pylons.templating import render_mako as render
from pylons import config
from pylons import response, tmpl_context as c, g, cache, request, session
from pylons.decorators import jsonify, validate
from pylons.helpers import abort, redirect_to, etag_cache
from pylons.i18n import N_, _, ungettext
from myproj.lib.auth import get_user, redirect_to_login
from myproj.lib.helpers import get_object_or_404
from myproj.lib.decorators import authorize
import myproj.lib.permissions as permissions
import myproj.lib.helpers as h
from myproj import model as model
class BaseController(WSGIController):
"""
# Subclassed thusly ...
from authprojectname.lib.base import *
class AuthController(BaseController):
__permission__=permissions.SignedIn()
__excludes__=['post', 'add_user']
def index(self):
return render('/index.mako')
@authorize(permissions.InGroup('Admins'))
def post(self):
return 'ok'
@authorize(permissions.HasPermission('add_users'))
def add_user(self):
return 'ok'
"""
__model__ = None
__permission__ = None
__excludes__ = []
def __call__(self, environ, start_response):
"""Invoke the Controller"""
# WSGIController.__call__ dispatches to the Controller method
# the request is routed to. This routing information is
# available in environ['pylons.routes_dict']
# Insert any code to be run per request here.
# Refresh database session
model.resync()
try:
return WSGIController.__call__(self, environ, start_response)
finally:
model.Session.remove()
def __before__(self):
self._check_action()
self._load_model()
self._authorize()
self._context()
def _load_model(self):
"""
If __model__ variable is set will automatically load model instance
into context if "id" is in Routes. The name used in the context is
the same name as the model (in lowercase); otherwise you can use
the __name__ attribute.
"""
if self.__model__:
routes_id = request.environ['pylons.routes_dict']['id']
if routes_id:
instance = get_object_or_404(self.__model__, id=routes_id)
name = getattr(self,
'__name__',
self.__model__.__name__.lower())
setattr(c, name, instance)
def _context(self):
"""
Put common context variables in here
"""
pass
def _check_action(self):
"""
Do a check for action: otherwise NotImplemented error raised
Is this is still true? The check remains a valid thing to do but
perhaps this is now pointless.
"""
action = request.environ['pylons.routes_dict']['action']
if not hasattr(self, action):
abort(404)
def _authorize(self):
"""
Flexible action/permission declarations: If the __permission__
variable is set to a an instance of a permission such as SignedIn()
and the action is not in the __excludes__ variable list of excluded
actions and the permission check fails, reroute the request to the
login controller. Fails soft.
Rerouting an already signed-in user to the login page could be a
source of misunderstanding, although it could be argued that the
purpose is to allow the user to switch to an account that has the
requisite permissions. It might be nice for login to detect a
signed-in userand offer a different template for logging in to
another account as opposed to simply signing in.
"""
# add user to context for convenience
c.auth_user = get_user()
if self.__permission__ and \
request.environ['pylons.routes_dict']['action'] \
not in self.__excludes__ and \
not self.__permission__.check():
redirect_to_login()
# Include the '_' function in the public names
__all__ = [__name for __name in locals().keys() \
if not __name.startswith('_') or __name == '_']
author: | Graham Higgins <gjh@bel-epa.com> |
---|
September 06, 2010