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
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
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
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
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
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