Permalink
Browse files

first cut at moving paste.auth stuff into core

  • Loading branch information...
1 parent 0508684 commit bd0c7a6cb3d0539283b27e068671ba074e63b4e4 @mcdonc mcdonc committed Sep 5, 2011
Showing with 380 additions and 128 deletions.
  1. +9 −0 CHANGES.txt
  2. +24 −0 LICENSE.txt
  3. +149 −15 pyramid/authentication.py
  4. +198 −113 pyramid/tests/test_authentication.py
View
@@ -1,3 +1,12 @@
+Next release
+============
+
+Internal
+--------
+
+- Internalize code previously depended upon as imports from the
+ ``paste.auth`` module (futureproof).
+
1.2a5 (2011-09-04)
==================
View
@@ -135,3 +135,27 @@ following license:
IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+Portions of the code marked as "stolen from Paste" are provided under the
+following license:
+
+ Copyright (c) 2006-2007 Ian Bicking and Contributors
+
+ Permission is hereby granted, free of charge, to any person obtaining
+ a copy of this software and associated documentation files (the
+ "Software"), to deal in the Software without restriction, including
+ without limitation the rights to use, copy, modify, merge, publish,
+ distribute, sublicense, and/or sell copies of the Software, and to
+ permit persons to whom the Software is furnished to do so, subject to
+ the following conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
View
@@ -1,11 +1,10 @@
from codecs import utf_8_decode
from codecs import utf_8_encode
+from hashlib import md5
import datetime
import re
-import time
-
-from paste.auth import auth_tkt
-from paste.request import get_cookies
+import time as time_mod
+import urllib
from zope.interface import implements
@@ -16,7 +15,6 @@
from pyramid.security import Authenticated
from pyramid.security import Everyone
-
VALID_TOKEN = re.compile(r"^[A-Za-z][A-Za-z0-9+_-]*$")
class CallbackAuthenticationPolicy(object):
@@ -108,7 +106,6 @@ def effective_principals(self, request):
)
return effective_principals
-
class RepozeWho1AuthenticationPolicy(CallbackAuthenticationPolicy):
""" A :app:`Pyramid` :term:`authentication policy` which
obtains data from the :mod:`repoze.who` 1.X WSGI 'API' (the
@@ -392,6 +389,140 @@ def b64encode(v):
def b64decode(v):
return v.decode('base64')
+# this class licensed under the MIT license (stolen from Paste)
+class AuthTicket(object):
+ """
+ This class represents an authentication token. You must pass in
+ the shared secret, the userid, and the IP address. Optionally you
+ can include tokens (a list of strings, representing role names),
+ 'user_data', which is arbitrary data available for your own use in
+ later scripts. Lastly, you can override the cookie name and
+ timestamp.
+
+ Once you provide all the arguments, use .cookie_value() to
+ generate the appropriate authentication ticket.
+
+ CGI usage::
+
+ token = auth_tkt.AuthTick('sharedsecret', 'username',
+ os.environ['REMOTE_ADDR'], tokens=['admin'])
+ print 'Status: 200 OK'
+ print 'Content-type: text/html'
+ print token.cookie()
+ print
+ ... redirect HTML ...
+
+ Webware usage::
+
+ token = auth_tkt.AuthTick('sharedsecret', 'username',
+ self.request().environ()['REMOTE_ADDR'], tokens=['admin'])
+ self.response().setCookie('auth_tkt', token.cookie_value())
+ """
+
+ def __init__(self, secret, userid, ip, tokens=(), user_data='',
+ time=None, cookie_name='auth_tkt',
+ secure=False):
+ self.secret = secret
+ self.userid = userid
+ self.ip = ip
+ self.tokens = ','.join(tokens)
+ self.user_data = user_data
+ if time is None:
+ self.time = time_mod.time()
+ else:
+ self.time = time
+ self.cookie_name = cookie_name
+ self.secure = secure
+
+ def digest(self):
+ return calculate_digest(
+ self.ip, self.time, self.secret, self.userid, self.tokens,
+ self.user_data)
+
+ def cookie_value(self):
+ v = '%s%08x%s!' % (self.digest(), int(self.time),
+ urllib.quote(self.userid))
+ if self.tokens:
+ v += self.tokens + '!'
+ v += self.user_data
+ return v
+
+# this class licensed under the MIT license (stolen from Paste)
+class BadTicket(Exception):
+ """
+ Exception raised when a ticket can't be parsed. If we get far enough to
+ determine what the expected digest should have been, expected is set.
+ This should not be shown by default, but can be useful for debugging.
+ """
+ def __init__(self, msg, expected=None):
+ self.expected = expected
+ Exception.__init__(self, msg)
+
+# this function licensed under the MIT license (stolen from Paste)
+def parse_ticket(secret, ticket, ip):
+ """
+ Parse the ticket, returning (timestamp, userid, tokens, user_data).
+
+ If the ticket cannot be parsed, a ``BadTicket`` exception will be raised
+ with an explanation.
+ """
+ ticket = ticket.strip('"')
+ digest = ticket[:32]
+ try:
+ timestamp = int(ticket[32:40], 16)
+ except ValueError, e:
+ raise BadTicket('Timestamp is not a hex integer: %s' % e)
+ try:
+ userid, data = ticket[40:].split('!', 1)
+ except ValueError:
+ raise BadTicket('userid is not followed by !')
+ userid = urllib.unquote(userid)
+ if '!' in data:
+ tokens, user_data = data.split('!', 1)
+ else: # pragma: no cover (never generated)
+ # @@: Is this the right order?
+ tokens = ''
+ user_data = data
+
+ expected = calculate_digest(ip, timestamp, secret,
+ userid, tokens, user_data)
+
+ if expected != digest:
+ raise BadTicket('Digest signature is not correct',
+ expected=(expected, digest))
+
+ tokens = tokens.split(',')
+
+ return (timestamp, userid, tokens, user_data)
+
+# this function licensed under the MIT license (stolen from Paste)
+def calculate_digest(ip, timestamp, secret, userid, tokens, user_data):
+ secret = maybe_encode(secret)
+ userid = maybe_encode(userid)
+ tokens = maybe_encode(tokens)
+ user_data = maybe_encode(user_data)
+ digest0 = md5(
+ encode_ip_timestamp(ip, timestamp) + secret + userid + '\0'
+ + tokens + '\0' + user_data).hexdigest()
+ digest = md5(digest0 + secret).hexdigest()
+ return digest
+
+# this function licensed under the MIT license (stolen from Paste)
+def encode_ip_timestamp(ip, timestamp):
+ ip_chars = ''.join(map(chr, map(int, ip.split('.'))))
+ t = int(timestamp)
+ ts = ((t & 0xff000000) >> 24,
+ (t & 0xff0000) >> 16,
+ (t & 0xff00) >> 8,
+ t & 0xff)
+ ts_chars = ''.join(map(chr, ts))
+ return ip_chars + ts_chars
+
+def maybe_encode(s, encoding='utf8'):
+ if isinstance(s, unicode):
+ s = s.encode(encoding)
+ return s
+
EXPIRE = object()
class AuthTktCookieHelper(object):
@@ -401,7 +532,9 @@ class AuthTktCookieHelper(object):
:class:`pyramid.authentication.AuthTktAuthenticationPolicy` for the
meanings of the constructor arguments.
"""
- auth_tkt = auth_tkt # for tests
+ parse_ticket = staticmethod(parse_ticket) # for tests
+ AuthTicket = AuthTicket # for tests
+ BadTicket = BadTicket # for tests
now = None # for tests
userid_type_decoders = {
@@ -487,10 +620,9 @@ def identify(self, request):
""" Return a dictionary with authentication information, or ``None``
if no valid auth_tkt is attached to ``request``"""
environ = request.environ
- cookies = get_cookies(environ)
- cookie = cookies.get(self.cookie_name)
+ cookie = request.cookies.get(self.cookie_name)
- if cookie is None or not cookie.value:
+ if cookie is None:
return None
if self.include_ip:
@@ -499,15 +631,15 @@ def identify(self, request):
remote_addr = '0.0.0.0'
try:
- timestamp, userid, tokens, user_data = self.auth_tkt.parse_ticket(
- self.secret, cookie.value, remote_addr)
- except self.auth_tkt.BadTicket:
+ timestamp, userid, tokens, user_data = self.parse_ticket(
+ self.secret, cookie, remote_addr)
+ except self.BadTicket:
return None
now = self.now # service tests
if now is None:
- now = time.time()
+ now = time_mod.time()
if self.timeout and ( (timestamp + self.timeout) < now ):
# the auth_tkt data has expired
@@ -592,7 +724,7 @@ def remember(self, request, userid, max_age=None, tokens=()):
if not (isinstance(token, str) and VALID_TOKEN.match(token)):
raise ValueError("Invalid token %r" % (token,))
- ticket = self.auth_tkt.AuthTicket(
+ ticket = self.AuthTicket(
self.secret,
userid,
remote_addr,
@@ -655,3 +787,5 @@ def forget(self, request):
def unauthenticated_userid(self, request):
return request.session.get(self.userid_key)
+
+# 14a3263f21e58dc0c1a4c994ab640bff4e6448d1ZWRpdG9y!userid_type:b64unicode
Oops, something went wrong.

0 comments on commit bd0c7a6

Please sign in to comment.