summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris McDonough <chrism@plope.com>2012-10-14 16:01:21 -0400
committerChris McDonough <chrism@plope.com>2012-10-14 16:01:21 -0400
commit6c4a1e7bdc4239e3c8fa3cfe6673a6e800ce113d (patch)
tree01b40784f098d34236e798b61f2fa897f5c115c5
parent1207dbed3f2e4cc765fd2078f9c622329324ffe0 (diff)
parent67fa5ad66216b6a1aa71fed639e3d2d82aa4458d (diff)
downloadpyramid-6c4a1e7bdc4239e3c8fa3cfe6673a6e800ce113d.tar.gz
pyramid-6c4a1e7bdc4239e3c8fa3cfe6673a6e800ce113d.tar.bz2
pyramid-6c4a1e7bdc4239e3c8fa3cfe6673a6e800ce113d.zip
Merge branch 'feature-basic-auth'
-rw-r--r--.gitignore2
-rw-r--r--CHANGES.txt5
-rw-r--r--docs/api/authentication.rst6
-rw-r--r--pyramid/authentication.py108
-rw-r--r--pyramid/tests/test_authentication.py100
5 files changed, 196 insertions, 25 deletions
diff --git a/.gitignore b/.gitignore
index 8e2f83e7d..5fa2a2ee4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,6 +5,7 @@
*.pt.py
*.txt.py
*~
+.*.swp
.coverage
.tox/
nosetests.xml
@@ -21,3 +22,4 @@ bookenv/
jyenv/
pypyenv/
env*/
+venv/
diff --git a/CHANGES.txt b/CHANGES.txt
index fbac16117..9b3ce1253 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -11,6 +11,9 @@ Features
- Comments with references to documentation sections placed in scaffold
``.ini`` files.
+- Added an HTTP Basic authentication policy
+ at ``pyramid.authentication.BasicAuthAuthenticationPolicy``.
+
Bug Fixes
---------
@@ -30,7 +33,7 @@ Bug Fixes
- When registering a view configuration that named a Chameleon ZPT renderer
with a macro name in it (e.g. ``renderer='some/template#somemacro.pt``) as
well as a view configuration without a macro name it it that pointed to the
- same template (e.g. ``renderer='some/template.pt'), internal caching could
+ same template (e.g. ``renderer='some/template.pt'``), internal caching could
confuse the two, and your code might have rendered one instead of the
other.
diff --git a/docs/api/authentication.rst b/docs/api/authentication.rst
index 5d4dbd9e3..587026a3b 100644
--- a/docs/api/authentication.rst
+++ b/docs/api/authentication.rst
@@ -10,12 +10,14 @@ Authentication Policies
.. autoclass:: AuthTktAuthenticationPolicy
- .. autoclass:: RepozeWho1AuthenticationPolicy
-
.. autoclass:: RemoteUserAuthenticationPolicy
.. autoclass:: SessionAuthenticationPolicy
+ .. autoclass:: BasicAuthAuthenticationPolicy
+
+ .. autoclass:: RepozeWho1AuthenticationPolicy
+
Helper Classes
~~~~~~~~~~~~~~
diff --git a/pyramid/authentication.py b/pyramid/authentication.py
index 83bdb13d1..d4fd7ab8b 100644
--- a/pyramid/authentication.py
+++ b/pyramid/authentication.py
@@ -1,3 +1,4 @@
+import binascii
from codecs import utf_8_decode
from codecs import utf_8_encode
from hashlib import md5
@@ -330,13 +331,13 @@ class AuthTktAuthenticationPolicy(CallbackAuthenticationPolicy):
Optional.
``path``
-
+
Default: ``/``. The path for which the auth_tkt cookie is valid.
May be desirable if the application only serves part of a domain.
Optional.
-
+
``http_only``
-
+
Default: ``False``. Hide cookie from JavaScript by setting the
HttpOnly flag. Not honored by all browsers.
Optional.
@@ -553,7 +554,7 @@ class AuthTktCookieHelper(object):
text_type: ('b64unicode', lambda x: b64encode(utf_8_encode(x)[0])),
binary_type: ('b64str', lambda x: b64encode(x)),
}
-
+
def __init__(self, secret, cookie_name='auth_tkt', secure=False,
include_ip=False, timeout=None, reissue_time=None,
max_age=None, http_only=False, path="/", wild_domain=True):
@@ -632,7 +633,7 @@ class AuthTktCookieHelper(object):
remote_addr = environ['REMOTE_ADDR']
else:
remote_addr = '0.0.0.0'
-
+
try:
timestamp, userid, tokens, user_data = self.parse_ticket(
self.secret, cookie, remote_addr)
@@ -641,7 +642,7 @@ class AuthTktCookieHelper(object):
now = self.now # service tests
- if now is None:
+ if now is None:
now = time_mod.time()
if self.timeout and ( (timestamp + self.timeout) < now ):
@@ -689,7 +690,7 @@ class AuthTktCookieHelper(object):
environ = request.environ
request._authtkt_reissue_revoked = True
return self._get_cookies(environ, '', max_age=EXPIRE)
-
+
def remember(self, request, userid, max_age=None, tokens=()):
""" Return a set of Set-Cookie headers; when set into a response,
these headers will represent a valid authentication ticket.
@@ -783,7 +784,7 @@ class SessionAuthenticationPolicy(CallbackAuthenticationPolicy):
Pyramid debug logger about the results of various authentication
steps. The output from debugging is useful for reporting to maillist
or IRC channels when asking for support.
-
+
"""
def __init__(self, prefix='auth.', callback=None, debug=False):
@@ -806,3 +807,94 @@ class SessionAuthenticationPolicy(CallbackAuthenticationPolicy):
def unauthenticated_userid(self, request):
return request.session.get(self.userid_key)
+
+@implementer(IAuthenticationPolicy)
+class BasicAuthAuthenticationPolicy(CallbackAuthenticationPolicy):
+ """ A :app:`Pyramid` authentication policy which uses HTTP standard basic
+ authentication protocol to authenticate users. To use this policy you will
+ need to provide a callback which checks the supplied user credentials
+ against your source of login data.
+
+ Constructor Arguments
+
+ ``check``
+
+ A callback function passed a username, password and request, in that
+ order as positional arguments. Expected to return ``None`` if the
+ userid doesn't exist or a sequence of principal identifiers (possibly
+ empty) if the user does exist.
+
+ ``realm``
+
+ Default: ``"Realm"``. The Basic Auth Realm string. Usually displayed to
+ the user by the browser in the login dialog.
+
+ ``debug``
+
+ Default: ``False``. If ``debug`` is ``True``, log messages to the
+ Pyramid debug logger about the results of various authentication
+ steps. The output from debugging is useful for reporting to maillist
+ or IRC channels when asking for support.
+
+ **Issuing a challenge**
+
+ Regular browsers will not send username/password credentials unless they
+ first receive a challenge from the server. The following recipe will
+ register a view that will send a Basic Auth challenge to the user whenever
+ there is an attempt to call a view which results in a Forbidden response::
+
+ from pyramid.httpexceptions import HTTPForbidden
+ from pyramid.httpexceptions import HTTPUnauthorized
+ from pyramid.security import forget
+ from pyramid.view import view_config
+
+ @view_config(context=HTTPForbidden)
+ def basic_challenge(request):
+ response = HTTPUnauthorized()
+ response.headers.update(forget(request))
+ return response
+ """
+ def __init__(self, check, realm='Realm', debug=False):
+ self.check = check
+ self.realm = realm
+ self.debug = debug
+
+ def unauthenticated_userid(self, request):
+ credentials = self._get_credentials(request)
+ if credentials:
+ return credentials[0]
+
+ def remember(self, request, principal, **kw):
+ return []
+
+ def forget(self, request):
+ return [('WWW-Authenticate', 'Basic realm="%s"' % self.realm)]
+
+ def callback(self, username, request):
+ # Username arg is ignored. Unfortunately _get_credentials winds up
+ # getting called twice when authenticated_userid is called. Avoiding
+ # that, however, winds up duplicating logic from the superclass.
+ credentials = self._get_credentials(request)
+ if credentials:
+ username, password = credentials
+ return self.check(username, password, request)
+
+ def _get_credentials(self, request):
+ authorization = request.headers.get('Authorization')
+ if not authorization:
+ return None
+ try:
+ authmeth, auth = authorization.split(' ', 1)
+ except ValueError: # not enough values to unpack
+ return None
+ if authmeth.lower() != 'basic':
+ return None
+ try:
+ auth = b64decode(auth.strip()).decode('ascii')
+ except (TypeError, binascii.Error): # can't decode
+ return None
+ try:
+ username, password = auth.split(':', 1)
+ except ValueError: # not enough values to unpack
+ return None
+ return username, password
diff --git a/pyramid/tests/test_authentication.py b/pyramid/tests/test_authentication.py
index e513b9a48..dfe3cf0b0 100644
--- a/pyramid/tests/test_authentication.py
+++ b/pyramid/tests/test_authentication.py
@@ -14,7 +14,7 @@ class TestCallbackAuthenticationPolicyDebugging(unittest.TestCase):
def tearDown(self):
del self.config
-
+
def debug(self, msg):
self.messages.append(msg)
@@ -151,7 +151,7 @@ class TestRepozeWho1AuthenticationPolicy(unittest.TestCase):
def _makeOne(self, identifier_name='auth_tkt', callback=None):
return self._getTargetClass()(identifier_name, callback)
-
+
def test_class_implements_IAuthenticationPolicy(self):
from zope.interface.verify import verifyClass
from pyramid.interfaces import IAuthenticationPolicy
@@ -251,7 +251,7 @@ class TestRepozeWho1AuthenticationPolicy(unittest.TestCase):
result = policy.remember(request, 'fred')
self.assertEqual(result[0], request.environ)
self.assertEqual(result[1], {'repoze.who.userid':'fred'})
-
+
def test_forget_no_plugins(self):
request = DummyRequest({})
policy = self._makeOne()
@@ -276,7 +276,7 @@ class TestRemoteUserAuthenticationPolicy(unittest.TestCase):
def _makeOne(self, environ_key='REMOTE_USER', callback=None):
return self._getTargetClass()(environ_key, callback)
-
+
def test_class_implements_IAuthenticationPolicy(self):
from zope.interface.verify import verifyClass
from pyramid.interfaces import IAuthenticationPolicy
@@ -301,7 +301,7 @@ class TestRemoteUserAuthenticationPolicy(unittest.TestCase):
request = DummyRequest({})
policy = self._makeOne()
self.assertEqual(policy.authenticated_userid(request), None)
-
+
def test_authenticated_userid(self):
request = DummyRequest({'REMOTE_USER':'fred'})
policy = self._makeOne()
@@ -326,7 +326,7 @@ class TestRemoteUserAuthenticationPolicy(unittest.TestCase):
policy = self._makeOne()
result = policy.remember(request, 'fred')
self.assertEqual(result, [])
-
+
def test_forget(self):
request = DummyRequest({'REMOTE_USER':'fred'})
policy = self._makeOne()
@@ -375,7 +375,7 @@ class TestAutkTktAuthenticationPolicy(unittest.TestCase):
request = DummyRequest({})
policy = self._makeOne(None, None)
self.assertEqual(policy.authenticated_userid(request), None)
-
+
def test_authenticated_userid_callback_returns_None(self):
request = DummyRequest({})
def callback(userid, request):
@@ -426,7 +426,7 @@ class TestAutkTktAuthenticationPolicy(unittest.TestCase):
result = policy.remember(request, 'fred', a=1, b=2)
self.assertEqual(policy.cookie.kw, {'a':1, 'b':2})
self.assertEqual(result, [])
-
+
def test_forget(self):
request = DummyRequest({})
policy = self._makeOne(None, None)
@@ -482,7 +482,7 @@ class TestAuthTktCookieHelper(unittest.TestCase):
request = self._makeRequest(None)
result = helper.identify(request)
self.assertEqual(result, None)
-
+
def test_identify_good_cookie_include_ip(self):
helper = self._makeOne('secret', include_ip=True)
request = self._makeRequest('ticket')
@@ -605,7 +605,7 @@ class TestAuthTktCookieHelper(unittest.TestCase):
request = self._makeRequest('ticket')
result = helper.identify(request)
self.assertEqual(result, None)
-
+
def test_identify_cookie_timed_out(self):
helper = self._makeOne('secret', timeout=1)
request = self._makeRequest({'HTTP_COOKIE':'auth_tkt=bogus'})
@@ -828,7 +828,7 @@ class TestAuthTktCookieHelper(unittest.TestCase):
self.assertEqual(result[1][0], 'Set-Cookie')
self.assertTrue(result[1][1].endswith('; Path=/; Domain=example.com'))
self.assertTrue(result[1][1].startswith('auth_tkt='))
-
+
def test_remember_binary_userid(self):
import base64
helper = self._makeOne('secret')
@@ -1106,6 +1106,78 @@ class TestSessionAuthenticationPolicy(unittest.TestCase):
self.assertEqual(request.session.get('userid'), None)
self.assertEqual(result, [])
+class TestBasicAuthAuthenticationPolicy(unittest.TestCase):
+ def _getTargetClass(self):
+ from pyramid.authentication import BasicAuthAuthenticationPolicy as cls
+ return cls
+
+ def _makeOne(self, check):
+ return self._getTargetClass()(check, realm='SomeRealm')
+
+ def test_class_implements_IAuthenticationPolicy(self):
+ from zope.interface.verify import verifyClass
+ from pyramid.interfaces import IAuthenticationPolicy
+ verifyClass(IAuthenticationPolicy, self._getTargetClass())
+
+ def test_unauthenticated_userid(self):
+ import base64
+ request = testing.DummyRequest()
+ request.headers['Authorization'] = 'Basic %s' % base64.b64encode(
+ bytes_('chrisr:password')).decode('ascii')
+ policy = self._makeOne(None)
+ self.assertEqual(policy.unauthenticated_userid(request), 'chrisr')
+
+ def test_unauthenticated_userid_no_credentials(self):
+ request = testing.DummyRequest()
+ policy = self._makeOne(None)
+ self.assertEqual(policy.unauthenticated_userid(request), None)
+
+ def test_unauthenticated_bad_header(self):
+ request = testing.DummyRequest()
+ request.headers['Authorization'] = '...'
+ policy = self._makeOne(None)
+ self.assertEqual(policy.unauthenticated_userid(request), None)
+
+ def test_unauthenticated_userid_not_basic(self):
+ request = testing.DummyRequest()
+ request.headers['Authorization'] = 'Complicated things'
+ policy = self._makeOne(None)
+ self.assertEqual(policy.unauthenticated_userid(request), None)
+
+ def test_unauthenticated_userid_corrupt_base64(self):
+ request = testing.DummyRequest()
+ request.headers['Authorization'] = 'Basic chrisr:password'
+ policy = self._makeOne(None)
+ self.assertEqual(policy.unauthenticated_userid(request), None)
+
+ def test_authenticated_userid(self):
+ import base64
+ request = testing.DummyRequest()
+ request.headers['Authorization'] = 'Basic %s' % base64.b64encode(
+ bytes_('chrisr:password')).decode('ascii')
+ def check(username, password, request):
+ return []
+ policy = self._makeOne(check)
+ self.assertEqual(policy.authenticated_userid(request), 'chrisr')
+
+ def test_unauthenticated_userid_invalid_payload(self):
+ import base64
+ request = testing.DummyRequest()
+ request.headers['Authorization'] = 'Basic %s' % base64.b64encode(
+ bytes_('chrisrpassword')).decode('ascii')
+ policy = self._makeOne(None)
+ self.assertEqual(policy.unauthenticated_userid(request), None)
+
+ def test_remember(self):
+ policy = self._makeOne(None)
+ self.assertEqual(policy.remember(None, None), [])
+
+ def test_forget(self):
+ policy = self._makeOne(None)
+ self.assertEqual(policy.forget(None), [
+ ('WWW-Authenticate', 'Basic realm="SomeRealm"')])
+
+
class DummyContext:
pass
@@ -1130,7 +1202,7 @@ class DummyRequest:
class DummyWhoPlugin:
def remember(self, environ, identity):
return environ, identity
-
+
def forget(self, environ, identity):
return environ, identity
@@ -1164,7 +1236,7 @@ class DummyAuthTktModule(object):
raise self.BadTicket()
return self.timestamp, self.userid, self.tokens, self.user_data
self.parse_ticket = parse_ticket
-
+
class AuthTicket(object):
def __init__(self, secret, userid, remote_addr, **kw):
self.secret = secret
@@ -1186,4 +1258,4 @@ class DummyAuthTktModule(object):
class DummyResponse:
def __init__(self):
self.headerlist = []
-
+