0001"""Authentication middleware
0002
0003This module provides one piece of middleware named
0004``authkit.authenticate.middleware`` which is used to intercept responses with
0005a specified status code, present a user with a means of authenticating
0006themselves and handle the sign in process.
0007
0008Each of the authentication methods supported by the middleware is described in
0009detail in the main AuthKit manual. The methods include:
0010
0011* HTTP Basic (``basic``)
0012* HTTP Digest (``digest``)
0013* OpenID Passurl (``openid``)
0014* Form and Cookie (``form``)
0015* Forward (``forward``)
0016* Redirect (``redirect``)
0017
0018The authenticate middleware can be configured directly or by means of a Paste
0019deploy config file as used by Pylons. It can be used directly like this:
0020
0021.. code-block:: Python
0022
0023 from authkit.authenticate import middleware, test_app
0024 from paste.httpserver import serve
0025
0026 import sys
0027 app = middleware(
0028 test_app,
0029 enable = True,
0030 method = 'passurl',
0031 cookie_secret='some_secret',
0032 )
0033
0034 serve(app, host='0.0.0.0', port=8000)
0035
0036"""
0037
0038import types
0039import warnings
0040import logging
0041import os
0042import os.path
0043
0044from paste.util.import_string import eval_import
0045from multi import MultiHandler, status_checker
0046from pkg_resources import iter_entry_points, load_entry_point
0047from paste.deploy.converters import asbool
0048from paste.httpexceptions import HTTPExceptionHandler
0049
0050from authkit.authorize import authorize_request
0051from authkit.permissions import RemoteUser, no_authkit_users_in_environ, AuthKitConfigError
0053
0054
0055
0056class AuthKitAuthHandler(object):
0057 """
0058 The base class for all middleware responsible for handling
0059 authentication and setting whatever needs to be set so that the
0060 ``AuthKitUserSetter`` middleware can set REMOTE_USER on subsequent
0061 requests. ``AuthKitAuthHandler``s only get inserted into the
0062 middleware stack if an appropriate status code (as set in the
0063 ``authkit.setup.intercept`` config option) is intercepted by the
0064 authentication middleware.
0065 """
0066 pass
0067
0068class AuthKitUserSetter(object):
0069 """
0070 The base class for all middleware responsible for attempting to set
0071 REMOTE_USER on each request. The class is overridden by the induvidual
0072 handlers.
0073 """
0074 pass
0075
0076
0077
0078log = logging.getLogger('authkit.authenticate')
0079
0080def strip_base(conf, base):
0081 result = {}
0082 for key in conf.keys():
0083 if key.startswith(base):
0084 result[key[len(base):]] = conf[key]
0085 return result
0086
0087def swap_underscore(*confs):
0088 results = []
0089 for conf in confs:
0090 result = {}
0091 for k,v in conf.items():
0092 result[k.replace('.','_')] = v
0093 results.append(result)
0094 return results
0095
0096def valid_password(environ, username, password):
0097 """
0098 A function which can be used with the ``basic`` and ``form`` authentication
0099 methods to validate a username and passowrd.
0100
0101 This implementation is used by default if no other method is specified. It
0102 checks the for an ``authkit.users`` object present in the ``environ``
0103 dictionary under the ``authkit.users`` key and uses the information there
0104 to validate the username and password.
0105
0106 In this implementation usernames are case insensitive and passwords are
0107 case sensitive. The function returns ``True`` if the user ``username`` has
0108 the password specified by ``password`` and returns ``False`` if the user
0109 doesn't exist or the password is incorrect.
0110
0111 If you create and specify your own ``authkit.users`` object with the same
0112 API, this method will also work correctly with your custom solution. See
0113 the AuthKit manual for information on the user management api, how to
0114 specify a different ``authkit.users`` object (say to read user information
0115 from a file rather than have it specified directly) and for information on
0116 how to create your own ``Users`` objects.
0117 """
0118 log.debug("valid_password called. username: %s", username)
0119 if not environ.has_key('authkit.users'):
0120 raise no_authkit_users_in_environ
0121 users = environ['authkit.users']
0122 if not users.user_exists(username):
0123 return False
0124 elif users.user_has_password(username.lower(), password):
0125 return True
0126 return False
0127
0128def digest_password(environ, realm, username):
0129 """
0130 This is similar to ``valid_password()`` but is used with the ``digest``
0131 authentication method and rather than checking a username and password and
0132 returning ``True`` or ``False`` it takes the realm and username as input,
0133 looks up the correct password and and returns a digest by calling the
0134 ``authkit.authenticate.digest.digest_password()`` function with the
0135 parameters ``realm``, ``username`` and ``password`` respectively. The
0136 digest returned is then compared with the one submitted by the browser.
0137
0138 As with ``valid_password()`` this method is designed to work with the user
0139 management API so you can use it with ``authkit.users`` objects or your own
0140 custom ``Users`` objects. Alternatively you can specify your own function
0141 which can lookup the password in whichever way you prefer, perhaps from a
0142 database or LDAP connection.
0143
0144 Only required if you intend to use HTTP digest authentication.
0145 """
0146 log.debug(
0147 "digest_password called. username: %s, realm: %s", username, realm
0148 )
0149 if not environ.has_key('authkit.users'):
0150 raise no_authkit_users_in_environ
0151 users = environ['authkit.users']
0152 if users.user_exists(username):
0153 password = users.user(username)['password']
0154 return digest.digest_password(realm, username, password)
0155
0156
0157 return None
0158
0159def get_authenticate_function(app, authenticate_conf, format, prefix):
0160 """
0161 Sets up the users object, adds the middleware to add the users object
0162 to the environ and then returns authenticate methods to check a password
0163 and a digest.
0164 """
0165 function = None
0166 users = None
0167 if len(authenticate_conf) < 1:
0168 raise AuthKitConfigError('Expected at least one authenticate key, not'
0169 ' %r'%authenticate_conf)
0170 if authenticate_conf.keys() == ['function']:
0171 function = authenticate_conf['function']
0172 if isinstance(function, (str, unicode)):
0173 function = eval_import(function)
0174 else:
0175 user_conf = strip_base(authenticate_conf, 'user.')
0176 if not user_conf:
0177 raise AuthKitConfigError('No authenticate function or users specified')
0178 else:
0179 if user_conf.has_key('encrypt'):
0180 enc_func = eval_import(user_conf['encrypt'])
0181 secret = user_conf.get('encrypt.secret','')
0182 def encrypt(password):
0183 return enc_func(password, secret)
0184 else:
0185 encrypt = None
0186 user_object = 'authkit.users.UsersFromString'
0187 if 'type' in user_conf.keys():
0188 user_object = user_conf['type']
0189 if isinstance(user_object, (str, unicode)):
0190 user_object = eval_import(user_object)
0191 users = user_object(user_conf['data'], encrypt)
0192 app = AddToEnviron(app, 'authkit.users', users)
0193 log.debug("authkit.users added to environ")
0194 if format == 'basic':
0195 function = valid_password
0196 log.debug("valid_password chosen %r", function)
0197 elif format == 'digest':
0198 log.debug("digest_password chosen %r", function)
0199 function = digest_password
0200 else:
0201 raise Exception('Invalid format for authenticate function %r'
0202 % format)
0203 return app, function, users
0204
0205def get_template(template_conf, prefix):
0206 """
0207 Another utility method to reduce code duplication. This function parses a
0208 template from one of the available template options:
0209
0210 ``string``
0211 The template as a string
0212
0213 ``file``
0214 A file containing the template
0215
0216 ``obj``
0217 A paste eval_import string or callable which returns a string
0218
0219 authkit.form.template.string =
0220 authkit.form.template.file =
0221 authkit.form.template.obj =
0222
0223 """
0224 template = None
0225 if len(template_conf) != 1:
0226 raise AuthKitConfigError('Expected one template entry, not %r' %
0227 (', '.join(template_conf.keys())))
0228 if template_conf.keys()[0] not in ['string', 'file', 'obj']:
0229 raise AuthKitConfigError("Template option can only be 'string', 'file'"
0230 " or 'obj'")
0231 if template_conf.keys()[0] == 'string':
0232 template = template_conf['string']
0233 elif template_conf.keys()[0] == 'file':
0234 if not os.path.exists(template_conf['file']):
0235 raise AuthKitConfigError('No such file %r exists. It was specified'
0236 ' by config option %r' %
0237 (template_conf['file'], prefix+'file'))
0238 fp = open(template_conf['file'], 'r')
0239 template = fp.read()
0240 fp.close()
0241 if not template:
0242 raise AuthKitConfigError('No data in template file %s specified by'
0243 ' config option %r' %
0244 (template_conf['file'], prefix+'file'))
0245 elif template_conf.keys()[0] == 'obj':
0246 template = eval_import(template_conf['obj'])
0247 if not template:
0248 raise AuthKitConfigError('No data in template obj %s specified by '
0249 'config option %r' %
0250 (template_conf['obj'], prefix+'obj'))
0251 else:
0252 raise AuthKitConfigError("Unknown option %r" %
0253 (prefix+template_conf.keys()[0]))
0254 if not template:
0255 raise AuthKitConfigError("The template loaded did not contain any data")
0256 if isinstance(template, (str, unicode)):
0257 def render_template():
0258 return template
0259 return render_template
0260 return template
0261
0262
0263
0264
0265
0266class AddToEnviron(object):
0267 """
0268 Simple middleware which adds a key to the ``environ`` dictionary.
0269
0270 Used to add the ``authkit.users`` key to the environ when this is
0271 appropriate.
0272 """
0273 def __init__(self, app, key, object):
0274 self.app = app
0275 self.key = key
0276 self.object = object
0277
0278 def __call__(self, environ, start_response):
0279 environ[self.key] = self.object
0280 return self.app(environ, start_response)
0281
0282class AddDictToEnviron(object):
0283 """Simple middleware which adds the values of a dict to the environ."""
0284 def __init__(self, app, dct):
0285 self.app = app
0286 self.dct = dct
0287
0288 def __call__(self, environ, start_response):
0289 environ.update(self.dct)
0290 return self.app(environ, start_response)
0291
0292class RequireEnvironKey(object):
0293 def __init__(self, app, key, missing_error=None):
0294 self.app = app
0295 self.key = key
0296 self.missing_error = missing_error or 'Missing the key %(key)s from the environ. Have you setup the ' 'correct middleware?'
0299
0300 def __call__(self, environ, start_response):
0301 if not environ.has_key(self.key):
0302 raise Exception(self.missing_error%{'key':self.key})
0303 return self.app(environ, start_response)
0304
0305def get_methods():
0306 """Get a dictionary of the available method entry points."""
0307 available_methods = {}
0308 for method_handler in iter_entry_points(group='authkit.method', name=None):
0309 available_methods[method_handler.name] = method_handler
0310 return available_methods
0311
0312def load_method(name, from_these=None):
0313 if from_these:
0314 return from_these[name].load()
0315 else:
0316 return load_entry_point('AuthKit','authkit.method',name)
0317
0318def load_config(options, app_conf, prefix):
0319 merged = strip_base(app_conf, prefix)
0320
0321
0322 for key, value in options.items():
0323 if merged.has_key(key):
0324 warnings.warn(
0325 'Key %s with value %r set in the config file is being ' + 'replaced with value %r set in the application'%(
0327 key,
0328 auth_conf_options[key],
0329 value
0330 )
0331 )
0332 merged[key.replace('_','.')] = value
0333 return merged
0334
0335def middleware(app, app_conf=None, global_conf=None, prefix='authkit.',
0336 handle_httpexception=True, middleware=None, **options):
0337 """
0338 This function sets up the AuthKit authenticate middleware and its use and
0339 options are described in detail in the AuthKit manual.
0340
0341 The function takes the following arguments and returns a WSGI application
0342 wrapped in the appropriate AuthKit authentication middleware based on the
0343 options specified:
0344
0345 ``app``
0346 The WSGI application the authenticate middleware should wrap
0347
0348 ``app_conf``
0349 A paste deploy ``app_conf`` dictionary to be used to setup the
0350 middleware
0351
0352 ``global_conf``
0353 A paste deploy ``global_conf`` dictionary
0354
0355 ``prefix``
0356 The prefix which all authkit related options in the config file will
0357 have prefixed to their names. This defaults to ``authkit.`` and
0358 shouldn't normally need overriding.
0359
0360 ``middleware``
0361 A make_middleware function which should be called directly instead of
0362 loading and calling a function based on the method name. If this is
0363 set then ``authkit.setup.methof`` should not be set.
0364
0365 ``**options``
0366 Any AuthKit options which are setup directly in Python code. If
0367 specified, these options will override any options specifed in a config
0368 file.
0369
0370 All option names specified in the config file will have their prefix
0371 removed and any ``.`` characters replaced by ``_`` before the options
0372 specified by ``options`` are merged in. This means that the the option
0373 ``authkit.cookie.name`` specified in a config file sets the same options as
0374 ``cookie_name`` specified directly as an option.
0375 """
0376 if handle_httpexception:
0377 app = HTTPExceptionHandler(app)
0378
0379
0380
0381 if global_conf is None:
0382 global_conf = {}
0383 if app_conf is None:
0384 app_conf = {}
0385 if not isinstance(app_conf, dict):
0386 raise AuthKitConfigError(
0387 "Expected app_conf to be paste deploy app_conf dictionary "
0388 "from not %r" % app_conf
0389 )
0390
0391
0392 available_methods = get_methods()
0393
0394 all_conf = load_config(options, app_conf, prefix)
0395 if middleware is not None and all_conf.has_key('setup.method'):
0396 raise AuthKitConfigError(
0397 'You cannot specify a middleware function '
0398 'and an authkit.setup.method'
0399 )
0400 if not middleware and not all_conf.has_key('setup.method'):
0401 raise AuthKitConfigError('No authkit.setup.method was specified')
0402
0403
0404 if asbool(all_conf.get('setup.enable', True)) == False:
0405 warnings.warn("AuthKit middleware has been turned off by the config "
0406 "option authkit.setup.enable")
0407 return app
0408
0409
0410 intercept = [str(x).strip() for x in all_conf.get('setup.intercept','401').split(',')]
0412 if not '401' in intercept:
0413 warnings.warn(
0414 "AuthKit is configured via the authkit.setup.intercept option not "
0415 "to intercept 401 responses so the authentication middleware will "
0416 "not be triggered even if a 401 Unauthenticated response is "
0417 "returned.")
0418
0419 if middleware:
0420 prefix_ = prefix
0421 app = middleware(
0422 app,
0423 auth_conf=all_conf,
0424 app_conf=app_conf,
0425 global_conf=global_conf,
0426 prefix=prefix_,
0427 )
0428 else:
0429 methods = [method.strip() for method in all_conf['setup.method'].split(',')]
0430 log.debug("Trying to load the following methods: %r", methods)
0431 for method in methods:
0432 if method in ['setup','config']:
0433 raise AuthKitConfigError("The name %s is reserved cannot be used "
0434 "as a method name" % method)
0435 if not available_methods.has_key(method):
0436 raise AuthKitConfigError(
0437 'The authkit method %r is not available. The available methods '
0438 'are %s and %s'%(
0439 all_conf['setup.method'],
0440 ', '.join(available_methods.keys()[:-1]),
0441 available_methods.keys()[-1],
0442 )
0443 )
0444 prefix_ = prefix+method+'.'
0445 auth_conf = strip_base(all_conf, method+'.')
0446
0447 app = available_methods[method].load()(
0448 app,
0449 auth_conf=auth_conf,
0450 app_conf=app_conf,
0451 global_conf=global_conf,
0452 prefix=prefix_,
0453 )
0454 app = AddDictToEnviron(
0455 app,
0456 {
0457 'authkit.config':strip_base(all_conf, 'config.'),
0458 'authkit.intercept':intercept,
0459 'authkit.authenticate': True,
0460 }
0461 )
0462 return app
0463
0464def sample_app(environ, start_response):
0465 """
0466 A sample WSGI application that returns a 401 status code when the path
0467 ``/private`` is entered, triggering the authenticate middleware to
0468 prompt the user to sign in.
0469
0470 If used with the authenticate middleware's form method, the path
0471 ``/signout`` will display a signed out message if
0472 ``authkit.cookie.signout = /signout`` is specified in the config file.
0473
0474 If used with the authenticate middleware's forward method, the path
0475 ``/signin`` should be used to display the sign in form.
0476
0477 The path ``/`` always displays the environment.
0478 """
0479 if environ['PATH_INFO']=='/private':
0480 authorize_request(environ, RemoteUser())
0481 if environ['PATH_INFO'] == '/signout':
0482 start_response('200 OK', [('Content-type', 'text/plain; charset=UTF-8')])
0483 if environ.has_key('REMOTE_USER'):
0484 return ["Signed Out"]
0485 else:
0486 return ["Not signed in"]
0487 elif environ['PATH_INFO'] == '/signin':
0488 start_response('200 OK', [('Content-type', 'text/plain; charset=UTF-8')])
0489 return ["Your application would display a \nsign in form here."]
0490 else:
0491 start_response('200 OK', [('Content-type', 'text/plain; charset=UTF-8')])
0492 result = ['You Have Access To This Page.\n\nHere is the environment...\n\n']
0493 for k,v in environ.items():
0494 result.append('%s: %s\n'%(k,v))
0495 return result