0001"""Permission objects
0002
0003Permission objects are used to define which users should have access to a particular
0004resource. They are checked using some of the authorization objects either in the
0005``authkit.authorize`` module or ``authkit.pylons_adaptors`` module if you are using
0006Pylons.
0007
0008Permissions objects are very similar to WSGI applications and can perform a
0009check based on the request or the response. Not all of the authorization
0010objects have access to the response because the permission might be checked as
0011part of a code block before the response is generated. This leads to two
0012classes of permissions, request-based (which can be checked anywhere) and
0013responce-based which can only be checked when the authorization object has
0014access to the response. 
0015
0016All the built-in AuthKit permissions are request-based but you can use the
0017permissions objects defined in this module or create your own derived from
0018``authkit.permission.Permission``.
0019
0020Permissions are described in detail in the AuthKit manual.
0021"""
0022
0023from authkit.authorize import PermissionError, NotAuthenticatedError
0024from authkit.authorize import NotAuthorizedError, middleware
0025
0026import datetime
0027import logging
0028log = logging.getLogger('authkit.permissions')
0029
0030class AuthKitConfigError(Exception):
0031    """
0032    Raised when there is a problem with the
0033    configuration options chosen for the authenticate middleware
0034    """
0035    pass
0036
0037no_authkit_users_in_environ = AuthKitConfigError(
0038    'No authkit.users object exists in the environment. You may have '
0039    'forgotton to specify a Users object or are using the the default '
0040    'valid_password() method in the authenticate middleware when you '
0041    'may have meant to specify your own.'
0042)
0043
0044
0045# Permission Classes
0046#
0047
0048class Permission(object):
0049    """
0050    The base class for all permissions objects. 
0051
0052    The ``check()`` method is called by the authorization object to check the
0053    permission. Permissions should return the original status, headers and
0054    response or raise a ``NotAuthorizedError`` when their ``check()`` method is
0055    called. 
0056
0057    .. Note ::
0058    
0059        The WSGI ``app`` can only be called once by the ``check()`` method.
0060        This means that you cannot write permisisons objects that perform
0061        logical ``not`` and ``or`` operations on other permissions objects
0062        since doing so might require the same app to be called multiple times.
0063        A permission object to perform an ``and`` operation is feasible and has
0064        been impleneted as the ``And`` permission class.  
0065        
0066   """
0067
0068    def check(self, app, environ, start_response):
0069        return app(environ, start_response)
0070
0071class RequestPermission(Permission):
0072    """
0073    The base class for all request-based permissions
0074    """
0075    pass
0076
0077class _TestBadlyLabelledResponseBasedPermission(RequestPermission):
0078    def check(self, app, environ, start_response):
0079        def start_response(a,b,c=None):
0080            return start_response(a,b,c)
0081        return app(environ, start_response)
0082
0083class UserIn(RequestPermission):
0084    """
0085    Checks the ``REMOTE_USER`` is one of the users specified.
0086    
0087    Takes the following arguments:
0088
0089    ``users``
0090        A list of usernames which are valid
0091
0092    If there is no ``REMOTE_USER`` a ``NotAuthenticatedError`` is raised. If
0093    the ``REMOTE_USER`` is not in ``users`` a ``NotAuthorizedError`` is raised.
0094
0095    Usernames supplied to ``users`` are treated case insensitively.
0096    """
0097
0098    def __init__(self, users):
0099        if isinstance(users, list) or isinstance(users, tuple):
0100            users_ = []
0101            for user in users:
0102                users_.append(user.lower())
0103            self.users = users_
0104        elif isinstance(users, str):
0105            self.users = [users]
0106        else:
0107            raise PermissionSetupError('Expected users to be a list or a string, not %r'%users)
0108
0109    def check(self, app, environ, start_response):
0110        if 'REMOTE_USER' not in environ:
0111            raise NotAuthenticatedError('Not Authenticated')
0112        if environ['REMOTE_USER'] not in self.users:
0113            raise NotAuthorizedError('You are not one of the users allowed to access this resource.')
0114        return app(environ, start_response)
0115
0116class Exists(RequestPermission):
0117    """
0118    Checks the specified key is present in the ``environ``.
0119    
0120    Takes the following arguments:
0121
0122    ``key``
0123        The required key
0124
0125    ``error``
0126        The error to be raised if the key is missing. XXX This argument may be deprecated soon.
0127
0128    """
0129
0130    def __init__(self, key, error=NotAuthorizedError('Not Authorized')):
0131        self.key = key
0132        self.error = error
0133
0134    def check(self, app, environ, start_response):
0135        if self.key not in environ:
0136            raise self.error
0137        return app(environ, start_response)
0138
0139class And(RequestPermission):
0140    """
0141    Checks all the permission objects listed as keyword arguments in turn.
0142    Permissions are checked from left to right. The error raised by the ``And``
0143    permission is the error raised by the first permission check to fail.
0144    """
0145
0146    def __init__(self, *permissions):
0147        if len(permissions) < 2:
0148            raise PermissionSetupError('Expected at least 2 permissions objects')
0149        permissions = list(permissions)
0150        permissions.reverse()
0151        self.permissions = permissions
0152
0153    def check(self, app, environ, start_response):
0154        for permission in self.permissions:
0155            app = middleware(app, permission)
0156        #raise Exception(app, self.permissions)
0157        return app(environ, start_response)
0158
0159class RemoteUser(RequestPermission):
0160    """
0161    Checks someone is signed in by checking for the presence of the
0162    ``REMOTE_USER``.
0163    
0164    If ``accept_empty`` is ``False`` (the default) then an empty ``REMOTE_USER``
0165    will not be accepted and the value of ``REMOTE_USER`` must evaluate to 
0166    ``True`` in Python.
0167    """
0168
0169    def __init__(self, accept_empty=False):
0170        self.accept_empty = accept_empty
0171
0172    def check(self, app, environ, start_response):
0173        if 'REMOTE_USER' not in environ:
0174            raise NotAuthenticatedError('Not Authenticated')
0175        elif self.accept_empty==False and not environ['REMOTE_USER']:
0176            raise NotAuthorizedError('Not Authorized')
0177        return app(environ, start_response)
0178
0179#
0180# Permissions to work with the AuthKit user management API
0181#
0182
0183class HasAuthKitRole(RequestPermission):
0184    """
0185    Designed to work with the user management API described in the AuthKit manual.
0186
0187    This permission checks that the signed in user has any if the roles specified
0188    in ``roles``. If ``all`` is ``True``, the user must have all the roles for
0189    the permission check to pass.
0190    """
0191
0192    def __init__(self, roles, all=False, error=None):
0193        if isinstance(roles, str):
0194            roles = [roles]
0195        self.all = all
0196        self.roles = roles
0197        self.error = error
0198
0199    def check(self, app, environ, start_response):
0200        """
0201        Should return True if the user has the role or
0202        False if the user doesn't exist or doesn't have the role.
0203
0204        In this implementation role names are case insensitive.
0205        """
0206
0207        if not environ.get('authkit.users'):
0208            raise no_authkit_users_in_environ
0209        if not environ.get('REMOTE_USER'):
0210            if self.error:
0211                raise self.error
0212            raise NotAuthenticatedError('Not authenticated')
0213
0214        users = environ['authkit.users']
0215        if not users.user_exists(environ['REMOTE_USER']):
0216            raise NotAuthorizedError('No such user')
0217        # Check the groups specified when setup actually exist
0218        for role in self.roles:
0219            if not users.role_exists(role):
0220                raise Exception("No such role %r exists"%role)
0221        if self.all:
0222            for role in self.roles:
0223                if not users.user_has_role(environ['REMOTE_USER'], role):
0224                    if self.error:
0225                        raise self.error
0226                    else:
0227                        raise NotAuthorizedError(
0228                            "User doesn't have the role %s"%role.lower()
0229                        )
0230            return app(environ, start_response)
0231        else:
0232            for role in self.roles:
0233                if users.user_has_role(environ['REMOTE_USER'], role):
0234                    return app(environ, start_response)
0235            if self.error:
0236                raise self.error
0237            else:
0238                raise NotAuthorizedError(
0239                    "User doesn't have any of the specified roles"
0240                )
0241
0242class HasAuthKitGroup(RequestPermission):
0243    """
0244    Designed to work with the user management API described in the AuthKit manual.
0245
0246    This permission checks that the signed in user is in one of the groups specified
0247    in ``groups``.
0248    """
0249
0250    def __init__(self, groups, error=None):
0251        if isinstance(groups, str):
0252            groups = [groups]
0253        self.groups = groups
0254        self.error = error
0255
0256    def check(self, app, environ, start_response):
0257        """
0258        Should return True if the user has the group or
0259        False if the user doesn't exist or doesn't have the group.
0260
0261        In this implementation group names are case insensitive.
0262        """
0263        if not environ.get('authkit.users'):
0264            raise no_authkit_users_in_environ
0265        if not environ.get('REMOTE_USER'):
0266            if self.error:
0267                raise self.error
0268            raise NotAuthenticatedError('Not authenticated')
0269        users = environ['authkit.users']
0270        # Check the groups specified when setup actually exist
0271        for group in self.groups:
0272            if group is not None:
0273                if not users.group_exists(group):
0274                    raise Exception("No such group %r exists"%group)
0275
0276        if not users.user_exists(environ['REMOTE_USER']):
0277            raise NotAuthorizedError('No such user')
0278        for group in self.groups:
0279            if users.user_has_group(environ['REMOTE_USER'], group):
0280                return app(environ, start_response)
0281        if self.error:
0282            raise self.error
0283        else:
0284            raise NotAuthorizedError(
0285                "User is not a member of the specified group(s) %r"%self.groups
0286            )
0287
0288class ValidAuthKitUser(UserIn):
0289    """
0290    Checks that the signed in user is one of the users specified when setting up
0291    the user management API.
0292    """
0293    def __init__(self):
0294        pass
0295
0296    def check(self, app, environ, start_response):
0297        if 'authkit.users' not in environ:
0298            raise no_authkit_users_in_environ
0299        if not environ.get('REMOTE_USER'):
0300            raise NotAuthenticatedError('Not Authenticated')
0301        if not environ['authkit.users'].user_exists(environ['REMOTE_USER']):
0302            raise NotAuthorizedError(
0303                'You are not one of the users allowed to access this resource.'
0304            )
0305        return app(environ, start_response)
0306
0307class FromIP(RequestPermission):
0308    """
0309    Checks that the remote host specified in the environment ``key`` is one 
0310    of the hosts specified in ``hosts``.
0311    """
0312    def __init__(self, hosts, key='REMOTE_ADDR'):
0313        self.hosts = hosts
0314        if not isinstance(self.hosts, (list, tuple)):
0315            self.hosts = [hosts]
0316        self.key = key
0317
0318    def check(self, app, environ, start_response):
0319        if self.key not in environ:
0320            raise Exception(
0321                "No such key %r in environ so cannot check the host"%self.key
0322            )
0323        if not environ.get(self.key) in self.hosts:
0324            raise NotAuthorizedError('Host %r not allowed'%environ.get(self.key))
0325        return app(environ, start_response)
0326
0327class BetweenTimes(RequestPermission):
0328    """
0329    Only grants access if the request is made on or after ``start`` and 
0330    before ``end``. Times should be specified as datetime.time objects.
0331    """
0332    def __init__(self, start, end):
0333        self.start = start
0334        self.end = end
0335
0336    def check(self, app, environ, start_response):
0337        today = datetime.datetime.now()
0338        now = datetime.time(today.hour, today.minute, today.second, today.microsecond)
0339        error = NotAuthorizedError("Not authorized at this time of day")
0340        if self.end > self.start:
0341            if now >= self.start and now < self.end:
0342                return app(environ, start_response)
0343            else:
0344                raise error
0345        else:
0346            if now < datetime.time(23, 59, 59, 999999) and now >= self.start:
0347                return app(environ, start_response)
0348            elif now >= datetime.time(0) and now < self.end:
0349                return app(environ, start_response)
0350            else:
0351                raise error