| 1 |
"""Permission objects |
|---|
| 2 |
|
|---|
| 3 |
Permission objects are used to define which users should have access to a particular |
|---|
| 4 |
resource. They are checked using some of the authorization objects either in the |
|---|
| 5 |
``authkit.authorize`` module or ``authkit.pylons_adaptors`` module if you are using |
|---|
| 6 |
Pylons. |
|---|
| 7 |
|
|---|
| 8 |
Permissions objects are very similar to WSGI applications and can perform a |
|---|
| 9 |
check based on the request or the response. Not all of the authorization |
|---|
| 10 |
objects have access to the response because the permission might be checked as |
|---|
| 11 |
part of a code block before the response is generated. This leads to two |
|---|
| 12 |
classes of permissions, request-based (which can be checked anywhere) and |
|---|
| 13 |
responce-based which can only be checked when the authorization object has |
|---|
| 14 |
access to the response. |
|---|
| 15 |
|
|---|
| 16 |
All the built-in AuthKit permissions are request-based but you can use the |
|---|
| 17 |
permissions objects defined in this module or create your own derived from |
|---|
| 18 |
``authkit.permission.Permission``. |
|---|
| 19 |
|
|---|
| 20 |
Permissions are described in detail in the AuthKit manual. |
|---|
| 21 |
""" |
|---|
| 22 |
|
|---|
| 23 |
from authkit.authorize import PermissionError, NotAuthenticatedError |
|---|
| 24 |
from authkit.authorize import NotAuthorizedError, middleware |
|---|
| 25 |
|
|---|
| 26 |
import datetime |
|---|
| 27 |
import logging |
|---|
| 28 |
log = logging.getLogger('authkit.permissions') |
|---|
| 29 |
|
|---|
| 30 |
class AuthKitConfigError(Exception): |
|---|
| 31 |
""" |
|---|
| 32 |
Raised when there is a problem with the |
|---|
| 33 |
configuration options chosen for the authenticate middleware |
|---|
| 34 |
""" |
|---|
| 35 |
pass |
|---|
| 36 |
|
|---|
| 37 |
no_authkit_users_in_environ = AuthKitConfigError( |
|---|
| 38 |
'No authkit.users object exists in the environment. You may have ' |
|---|
| 39 |
'forgotton to specify a Users object or are using the the default ' |
|---|
| 40 |
'valid_password() method in the authenticate middleware when you ' |
|---|
| 41 |
'may have meant to specify your own.' |
|---|
| 42 |
) |
|---|
| 43 |
|
|---|
| 44 |
# |
|---|
| 45 |
# Permission Classes |
|---|
| 46 |
# |
|---|
| 47 |
|
|---|
| 48 |
class Permission(object): |
|---|
| 49 |
""" |
|---|
| 50 |
The base class for all permissions objects. |
|---|
| 51 |
|
|---|
| 52 |
The ``check()`` method is called by the authorization object to check the |
|---|
| 53 |
permission. Permissions should return the original status, headers and |
|---|
| 54 |
response or raise a ``NotAuthorizedError`` when their ``check()`` method is |
|---|
| 55 |
called. |
|---|
| 56 |
|
|---|
| 57 |
.. Note :: |
|---|
| 58 |
|
|---|
| 59 |
The WSGI ``app`` can only be called once by the ``check()`` method. |
|---|
| 60 |
This means that you cannot write permisisons objects that perform |
|---|
| 61 |
logical ``not`` and ``or`` operations on other permissions objects |
|---|
| 62 |
since doing so might require the same app to be called multiple times. |
|---|
| 63 |
A permission object to perform an ``and`` operation is feasible and has |
|---|
| 64 |
been impleneted as the ``And`` permission class. |
|---|
| 65 |
|
|---|
| 66 |
""" |
|---|
| 67 |
|
|---|
| 68 |
def check(self, app, environ, start_response): |
|---|
| 69 |
return app(environ, start_response) |
|---|
| 70 |
|
|---|
| 71 |
class RequestPermission(Permission): |
|---|
| 72 |
""" |
|---|
| 73 |
The base class for all request-based permissions |
|---|
| 74 |
""" |
|---|
| 75 |
pass |
|---|
| 76 |
|
|---|
| 77 |
class _TestBadlyLabelledResponseBasedPermission(RequestPermission): |
|---|
| 78 |
def check(self, app, environ, start_response): |
|---|
| 79 |
def start_response(a,b,c=None): |
|---|
| 80 |
return start_response(a,b,c) |
|---|
| 81 |
return app(environ, start_response) |
|---|
| 82 |
|
|---|
| 83 |
class UserIn(RequestPermission): |
|---|
| 84 |
""" |
|---|
| 85 |
Checks the ``REMOTE_USER`` is one of the users specified. |
|---|
| 86 |
|
|---|
| 87 |
Takes the following arguments: |
|---|
| 88 |
|
|---|
| 89 |
``users`` |
|---|
| 90 |
A list of usernames which are valid |
|---|
| 91 |
|
|---|
| 92 |
If there is no ``REMOTE_USER`` a ``NotAuthenticatedError`` is raised. If |
|---|
| 93 |
the ``REMOTE_USER`` is not in ``users`` a ``NotAuthorizedError`` is raised. |
|---|
| 94 |
|
|---|
| 95 |
Usernames supplied to ``users`` are treated case insensitively. |
|---|
| 96 |
""" |
|---|
| 97 |
|
|---|
| 98 |
def __init__(self, users): |
|---|
| 99 |
if isinstance(users, list) or isinstance(users, tuple): |
|---|
| 100 |
users_ = [] |
|---|
| 101 |
for user in users: |
|---|
| 102 |
users_.append(user.lower()) |
|---|
| 103 |
self.users = users_ |
|---|
| 104 |
elif isinstance(users, str): |
|---|
| 105 |
self.users = [users] |
|---|
| 106 |
else: |
|---|
| 107 |
raise PermissionSetupError('Expected users to be a list or a string, not %r'%users) |
|---|
| 108 |
|
|---|
| 109 |
def check(self, app, environ, start_response): |
|---|
| 110 |
if 'REMOTE_USER' not in environ: |
|---|
| 111 |
raise NotAuthenticatedError('Not Authenticated') |
|---|
| 112 |
if environ['REMOTE_USER'] not in self.users: |
|---|
| 113 |
raise NotAuthorizedError('You are not one of the users allowed to access this resource.') |
|---|
| 114 |
return app(environ, start_response) |
|---|
| 115 |
|
|---|
| 116 |
class Exists(RequestPermission): |
|---|
| 117 |
""" |
|---|
| 118 |
Checks the specified key is present in the ``environ``. |
|---|
| 119 |
|
|---|
| 120 |
Takes the following arguments: |
|---|
| 121 |
|
|---|
| 122 |
``key`` |
|---|
| 123 |
The required key |
|---|
| 124 |
|
|---|
| 125 |
``error`` |
|---|
| 126 |
The error to be raised if the key is missing. XXX This argument may be deprecated soon. |
|---|
| 127 |
|
|---|
| 128 |
""" |
|---|
| 129 |
|
|---|
| 130 |
def __init__(self, key, error=NotAuthorizedError('Not Authorized')): |
|---|
| 131 |
self.key = key |
|---|
| 132 |
self.error = error |
|---|
| 133 |
|
|---|
| 134 |
def check(self, app, environ, start_response): |
|---|
| 135 |
if self.key not in environ: |
|---|
| 136 |
raise self.error |
|---|
| 137 |
return app(environ, start_response) |
|---|
| 138 |
|
|---|
| 139 |
class And(RequestPermission): |
|---|
| 140 |
""" |
|---|
| 141 |
Checks all the permission objects listed as keyword arguments in turn. |
|---|
| 142 |
Permissions are checked from left to right. The error raised by the ``And`` |
|---|
| 143 |
permission is the error raised by the first permission check to fail. |
|---|
| 144 |
""" |
|---|
| 145 |
|
|---|
| 146 |
def __init__(self, *permissions): |
|---|
| 147 |
if len(permissions) < 2: |
|---|
| 148 |
raise PermissionSetupError('Expected at least 2 permissions objects') |
|---|
| 149 |
permissions = list(permissions) |
|---|
| 150 |
permissions.reverse() |
|---|
| 151 |
self.permissions = permissions |
|---|
| 152 |
|
|---|
| 153 |
def check(self, app, environ, start_response): |
|---|
| 154 |
for permission in self.permissions: |
|---|
| 155 |
app = middleware(app, permission) |
|---|
| 156 |
#raise Exception(app, self.permissions) |
|---|
| 157 |
return app(environ, start_response) |
|---|
| 158 |
|
|---|
| 159 |
class RemoteUser(RequestPermission): |
|---|
| 160 |
""" |
|---|
| 161 |
Checks someone is signed in by checking for the presence of the |
|---|
| 162 |
``REMOTE_USER``. |
|---|
| 163 |
|
|---|
| 164 |
If ``accept_empty`` is ``False`` (the default) then an empty ``REMOTE_USER`` |
|---|
| 165 |
will not be accepted and the value of ``REMOTE_USER`` must evaluate to |
|---|
| 166 |
``True`` in Python. |
|---|
| 167 |
""" |
|---|
| 168 |
|
|---|
| 169 |
def __init__(self, accept_empty=False): |
|---|
| 170 |
self.accept_empty = accept_empty |
|---|
| 171 |
|
|---|
| 172 |
def check(self, app, environ, start_response): |
|---|
| 173 |
if 'REMOTE_USER' not in environ: |
|---|
| 174 |
raise NotAuthenticatedError('Not Authenticated') |
|---|
| 175 |
elif self.accept_empty==False and not environ['REMOTE_USER']: |
|---|
| 176 |
raise NotAuthorizedError('Not Authorized') |
|---|
| 177 |
return app(environ, start_response) |
|---|
| 178 |
|
|---|
| 179 |
# |
|---|
| 180 |
# Permissions to work with the AuthKit user management API |
|---|
| 181 |
# |
|---|
| 182 |
|
|---|
| 183 |
class HasAuthKitRole(RequestPermission): |
|---|
| 184 |
""" |
|---|
| 185 |
Designed to work with the user management API described in the AuthKit manual. |
|---|
| 186 |
|
|---|
| 187 |
This permission checks that the signed in user has any if the roles specified |
|---|
| 188 |
in ``roles``. If ``all`` is ``True``, the user must have all the roles for |
|---|
| 189 |
the permission check to pass. |
|---|
| 190 |
""" |
|---|
| 191 |
|
|---|
| 192 |
def __init__(self, roles, all=False, error=None): |
|---|
| 193 |
if isinstance(roles, str): |
|---|
| 194 |
roles = [roles] |
|---|
| 195 |
self.all = all |
|---|
| 196 |
self.roles = roles |
|---|
| 197 |
self.error = error |
|---|
| 198 |
|
|---|
| 199 |
def check(self, app, environ, start_response): |
|---|
| 200 |
""" |
|---|
| 201 |
Should return True if the user has the role or |
|---|
| 202 |
False if the user doesn't exist or doesn't have the role. |
|---|
| 203 |
|
|---|
| 204 |
In this implementation role names are case insensitive. |
|---|
| 205 |
""" |
|---|
| 206 |
|
|---|
| 207 |
if not environ.get('authkit.users'): |
|---|
| 208 |
raise no_authkit_users_in_environ |
|---|
| 209 |
if not environ.get('REMOTE_USER'): |
|---|
| 210 |
if self.error: |
|---|
| 211 |
raise self.error |
|---|
| 212 |
raise NotAuthenticatedError('Not authenticated') |
|---|
| 213 |
|
|---|
| 214 |
users = environ['authkit.users'] |
|---|
| 215 |
if not users.user_exists(environ['REMOTE_USER']): |
|---|
| 216 |
raise NotAuthorizedError('No such user') |
|---|
| 217 |
# Check the groups specified when setup actually exist |
|---|
| 218 |
for role in self.roles: |
|---|
| 219 |
if not users.role_exists(role): |
|---|
| 220 |
raise Exception("No such role %r exists"%role) |
|---|
| 221 |
if self.all: |
|---|
| 222 |
for role in self.roles: |
|---|
| 223 |
if not users.user_has_role(environ['REMOTE_USER'], role): |
|---|
| 224 |
if self.error: |
|---|
| 225 |
raise self.error |
|---|
| 226 |
else: |
|---|
| 227 |
raise NotAuthorizedError( |
|---|
| 228 |
"User doesn't have the role %s"%role.lower() |
|---|
| 229 |
) |
|---|
| 230 |
return app(environ, start_response) |
|---|
| 231 |
else: |
|---|
| 232 |
for role in self.roles: |
|---|
| 233 |
if users.user_has_role(environ['REMOTE_USER'], role): |
|---|
| 234 |
return app(environ, start_response) |
|---|
| 235 |
if self.error: |
|---|
| 236 |
raise self.error |
|---|
| 237 |
else: |
|---|
| 238 |
raise NotAuthorizedError( |
|---|
| 239 |
"User doesn't have any of the specified roles" |
|---|
| 240 |
) |
|---|
| 241 |
|
|---|
| 242 |
class HasAuthKitGroup(RequestPermission): |
|---|
| 243 |
""" |
|---|
| 244 |
Designed to work with the user management API described in the AuthKit manual. |
|---|
| 245 |
|
|---|
| 246 |
This permission checks that the signed in user is in one of the groups specified |
|---|
| 247 |
in ``groups``. |
|---|
| 248 |
""" |
|---|
| 249 |
|
|---|
| 250 |
def __init__(self, groups, error=None): |
|---|
| 251 |
if isinstance(groups, str): |
|---|
| 252 |
groups = [groups] |
|---|
| 253 |
self.groups = groups |
|---|
| 254 |
self.error = error |
|---|
| 255 |
|
|---|
| 256 |
def check(self, app, environ, start_response): |
|---|
| 257 |
""" |
|---|
| 258 |
Should return True if the user has the group or |
|---|
| 259 |
False if the user doesn't exist or doesn't have the group. |
|---|
| 260 |
|
|---|
| 261 |
In this implementation group names are case insensitive. |
|---|
| 262 |
""" |
|---|
| 263 |
if not environ.get('authkit.users'): |
|---|
| 264 |
raise no_authkit_users_in_environ |
|---|
| 265 |
if not environ.get('REMOTE_USER'): |
|---|
| 266 |
if self.error: |
|---|
| 267 |
raise self.error |
|---|
| 268 |
raise NotAuthenticatedError('Not authenticated') |
|---|
| 269 |
users = environ['authkit.users'] |
|---|
| 270 |
# Check the groups specified when setup actually exist |
|---|
| 271 |
for group in self.groups: |
|---|
| 272 |
if group is not None: |
|---|
| 273 |
if not users.group_exists(group): |
|---|
| 274 |
raise Exception("No such group %r exists"%group) |
|---|
| 275 |
|
|---|
| 276 |
if not users.user_exists(environ['REMOTE_USER']): |
|---|
| 277 |
raise NotAuthorizedError('No such user') |
|---|
| 278 |
for group in self.groups: |
|---|
| 279 |
if users.user_has_group(environ['REMOTE_USER'], group): |
|---|
| 280 |
return app(environ, start_response) |
|---|
| 281 |
if self.error: |
|---|
| 282 |
raise self.error |
|---|
| 283 |
else: |
|---|
| 284 |
raise NotAuthorizedError( |
|---|
| 285 |
"User is not a member of the specified group(s) %r"%self.groups |
|---|
| 286 |
) |
|---|
| 287 |
|
|---|
| 288 |
class ValidAuthKitUser(UserIn): |
|---|
| 289 |
""" |
|---|
| 290 |
Checks that the signed in user is one of the users specified when setting up |
|---|
| 291 |
the user management API. |
|---|
| 292 |
""" |
|---|
| 293 |
def __init__(self): |
|---|
| 294 |
pass |
|---|
| 295 |
|
|---|
| 296 |
def check(self, app, environ, start_response): |
|---|
| 297 |
if 'authkit.users' not in environ: |
|---|
| 298 |
raise no_authkit_users_in_environ |
|---|
| 299 |
if not environ.get('REMOTE_USER'): |
|---|
| 300 |
raise NotAuthenticatedError('Not Authenticated') |
|---|
| 301 |
if not environ['authkit.users'].user_exists(environ['REMOTE_USER']): |
|---|
| 302 |
raise NotAuthorizedError( |
|---|
| 303 |
'You are not one of the users allowed to access this resource.' |
|---|
| 304 |
) |
|---|
| 305 |
return app(environ, start_response) |
|---|
| 306 |
|
|---|
| 307 |
class FromIP(RequestPermission): |
|---|
| 308 |
""" |
|---|
| 309 |
Checks that the remote host specified in the environment ``key`` is one |
|---|
| 310 |
of the hosts specified in ``hosts``. |
|---|
| 311 |
""" |
|---|
| 312 |
def __init__(self, hosts, key='REMOTE_ADDR'): |
|---|
| 313 |
self.hosts = hosts |
|---|
| 314 |
if not isinstance(self.hosts, (list, tuple)): |
|---|
| 315 |
self.hosts = [hosts] |
|---|
| 316 |
self.key = key |
|---|
| 317 |
|
|---|
| 318 |
def check(self, app, environ, start_response): |
|---|
| 319 |
if self.key not in environ: |
|---|
| 320 |
raise Exception( |
|---|
| 321 |
"No such key %r in environ so cannot check the host"%self.key |
|---|
| 322 |
) |
|---|
| 323 |
if not environ.get(self.key) in self.hosts: |
|---|
| 324 |
raise NotAuthorizedError('Host %r not allowed'%environ.get(self.key)) |
|---|
| 325 |
return app(environ, start_response) |
|---|
| 326 |
|
|---|
| 327 |
class BetweenTimes(RequestPermission): |
|---|
| 328 |
""" |
|---|
| 329 |
Only grants access if the request is made on or after ``start`` and |
|---|
| 330 |
before ``end``. Times should be specified as datetime.time objects. |
|---|
| 331 |
""" |
|---|
| 332 |
def __init__(self, start, end): |
|---|
| 333 |
self.start = start |
|---|
| 334 |
self.end = end |
|---|
| 335 |
|
|---|
| 336 |
def check(self, app, environ, start_response): |
|---|
| 337 |
today = datetime.datetime.now() |
|---|
| 338 |
now = datetime.time(today.hour, today.minute, today.second, today.microsecond) |
|---|
| 339 |
error = NotAuthorizedError("Not authorized at this time of day") |
|---|
| 340 |
if self.end > self.start: |
|---|
| 341 |
if now >= self.start and now < self.end: |
|---|
| 342 |
return app(environ, start_response) |
|---|
| 343 |
else: |
|---|
| 344 |
raise error |
|---|
| 345 |
else: |
|---|
| 346 |
if now < datetime.time(23, 59, 59, 999999) and now >= self.start: |
|---|
| 347 |
return app(environ, start_response) |
|---|
| 348 |
elif now >= datetime.time(0) and now < self.end: |
|---|
| 349 |
return app(environ, start_response) |
|---|
| 350 |
else: |
|---|
| 351 |
raise error |
|---|