.. _shabti_auth_xp: |today| .. _shabti-auth-xp: **shabti_auth_xp** -- row authentication ======================================== .. 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 `_ Row / instance-level permissions --------------------------------- 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 :class:`NewsItem` class might have ``edit`` and ``delete`` permissions set for each :class:`NewsItem` instance. The permissions system is implemented as an Elixir Statement, ``has_permissions``. The relevant code is located in :file:`model/permissions.py` in the Shabti application package. elixir ``has_permissions`` statement ------------------------------------ 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 :class:`User` or :class:`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. Row-level auth example ----------------------- .. code-block :: python 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 :file:`NewsItem`. This makes it quite flexible in more complex situations. ModelController, MIA -------------------- In addition the ``shabti_auth_xp`` template provides a sub-class of :class:`BaseController`, :class:`ModelController`. The :class:`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 :class:`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. .. code-block :: python 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: .. code-block :: python """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 |today|