David Tulloh
2016-04-20 7483ece150564b242ba0ac5c091319ee570dd9e1
commit | author | age
299b4c 1 import datetime
4f3525 2 from calendar import timegm
TS 3 from email.utils import formatdate
b95a59 4 from codecs import utf_8_decode
CM 5 from codecs import utf_8_encode
0ee58d 6 import os
cd1198 7 import time
7483ec 8 try:
DT 9     import hashlib
10 except ImportError:
11     import md5 as hashlib # Will only support md5 algorithm
4f3525 12 from wsgiref.handlers import _monthname     # Locale-independent, RFC-2616
TS 13 from wsgiref.handlers import _weekdayname   # Locale-independent, RFC-2616
8729b3 14 try:
J 15     from urllib.parse import urlencode, parse_qsl
16 except ImportError:
17     from urllib import urlencode
18     from urlparse import parse_qsl
b95a59 19
1fa560 20 from zope.interface import implementer
a5b033 21
cb5426 22 from repoze.who.interfaces import IIdentifier
b0f81f 23 from repoze.who.interfaces import IAuthenticator
6919cb 24 from repoze.who._compat import get_cookies
TS 25 import repoze.who._auth_tkt as auth_tkt
9df42f 26 from repoze.who._compat import STRING_TYPES
d6fa0a 27 from repoze.who._compat import u
a5b033 28
4f3525 29 _UTCNOW = None  # unit tests can replace
TS 30 def _utcnow():  #pragma NO COVERAGE
31     if _UTCNOW is not None:
32         return _UTCNOW
33     return datetime.datetime.utcnow()
798feb 34
7483ec 35 DEFAULT_DIGEST = hashlib.md5
e43169 36
1fa560 37 @implementer(IIdentifier, IAuthenticator)
a5b033 38 class AuthTktCookiePlugin(object):
779caf 39
0fa6f9 40     userid_typename = 'userid_type'
d6fa0a 41     userid_type_decoders = {'int': int,
TS 42                             'unicode': lambda x: utf_8_decode(x)[0],
43                            }
779caf 44
d6fa0a 45     userid_type_encoders = {int: ('int', str),
TS 46                            }
6919cb 47     try:
TS 48         userid_type_encoders[long] = ('int', str)
49     except NameError: #pragma NO COVER Python >= 3.0
50         pass
51     try:
52         userid_type_encoders[unicode] = ('unicode',
53                                          lambda x: utf_8_encode(x)[0])
54     except NameError: #pragma NO COVER Python >= 3.0
55         pass
56  
a5b033 57     def __init__(self, secret, cookie_name='auth_tkt',
cd1198 58                  secure=False, include_ip=False,
7483ec 59                  timeout=None, reissue_time=None, userid_checker=None,
DT 60                  digest_algo=DEFAULT_DIGEST):
a5b033 61         self.secret = secret
CM 62         self.cookie_name = cookie_name
63         self.include_ip = include_ip
64         self.secure = secure
cd1198 65         if timeout and ( (not reissue_time) or (reissue_time > timeout) ):
CM 66             raise ValueError('When timeout is specified, reissue_time must '
67                              'be set to a lower value')
68         self.timeout = timeout
69         self.reissue_time = reissue_time
a6f6dc 70         self.userid_checker = userid_checker
7483ec 71         self.digest_algo = digest_algo
a5b033 72
CM 73     # IIdentifier
74     def identify(self, environ):
75         cookies = get_cookies(environ)
76         cookie = cookies.get(self.cookie_name)
77
78         if cookie is None or not cookie.value:
40a968 79             return None
a5b033 80
CM 81         if self.include_ip:
82             remote_addr = environ['REMOTE_ADDR']
83         else:
84             remote_addr = '0.0.0.0'
85         
86         try:
87             timestamp, userid, tokens, user_data = auth_tkt.parse_ticket(
7483ec 88                 self.secret, cookie.value, remote_addr, self.digest_algo)
a5b033 89         except auth_tkt.BadTicket:
a6f6dc 90             return None
CM 91
cd1198 92         if self.timeout and ( (timestamp + self.timeout) < time.time() ):
40a968 93             return None
779caf 94
0fa6f9 95         user_data_dict = dict(parse_qsl(user_data))
JV 96         userid_type = user_data_dict.get(self.userid_typename)
97         if userid_type:
98             decoder = self.userid_type_decoders.get(userid_type)
99             if decoder:
100                 userid = decoder(userid)
a5b033 101             
CM 102         environ['REMOTE_USER_TOKENS'] = tokens
103         environ['REMOTE_USER_DATA'] = user_data
104         environ['AUTH_TYPE'] = 'cookie'
56d0c5 105
a5b033 106         identity = {}
CM 107         identity['timestamp'] = timestamp
b0f81f 108         identity['repoze.who.plugins.auth_tkt.userid'] = userid
a5b033 109         identity['tokens'] = tokens
0fa6f9 110         identity['userdata'] = user_data_dict
a5b033 111         return identity
519300 112
CM 113     # IIdentifier
114     def forget(self, environ, identity):
115         # return a set of expires Set-Cookie headers
798feb 116         return self._get_cookies(environ, 'INVALID', 0)
a5b033 117     
CM 118     # IIdentifier
119     def remember(self, environ, identity):
120         if self.include_ip:
121             remote_addr = environ['REMOTE_ADDR']
122         else:
123             remote_addr = '0.0.0.0'
124
125         cookies = get_cookies(environ)
126         old_cookie = cookies.get(self.cookie_name)
127         existing = cookies.get(self.cookie_name)
128         old_cookie_value = getattr(existing, 'value', None)
299b4c 129         max_age = identity.get('max_age', None)
a5b033 130
fc9a88 131         timestamp, userid, tokens, userdata = None, '', (), ''
a5b033 132
CM 133         if old_cookie_value:
134             try:
135                 timestamp,userid,tokens,userdata = auth_tkt.parse_ticket(
7483ec 136                     self.secret, old_cookie_value, remote_addr,
DT 137                     self.digest_algo)
a5b033 138             except auth_tkt.BadTicket:
CM 139                 pass
fc9a88 140         tokens = tuple(tokens)
a5b033 141
cb5426 142         who_userid = identity['repoze.who.userid']
fc9a88 143         who_tokens = tuple(identity.get('tokens', ()))
0fa6f9 144         who_userdata_dict = identity.get('userdata', {})
779caf 145
CM 146         encoding_data = self.userid_type_encoders.get(type(who_userid))
147         if encoding_data:
148             encoding, encoder = encoding_data
149             who_userid = encoder(who_userid)
0fa6f9 150             who_userdata_dict[self.userid_typename] = encoding
JV 151
152         who_userdata = urlencode(who_userdata_dict)
153
a5b033 154         old_data = (userid, tokens, userdata)
cb5426 155         new_data = (who_userid, who_tokens, who_userdata)
a5b033 156
cd1198 157         if old_data != new_data or (self.reissue_time and
CM 158                 ( (timestamp + self.reissue_time) < time.time() )):
a5b033 159             ticket = auth_tkt.AuthTicket(
CM 160                 self.secret,
cb5426 161                 who_userid,
a5b033 162                 remote_addr,
cb5426 163                 tokens=who_tokens,
CM 164                 user_data=who_userdata,
a5b033 165                 cookie_name=self.cookie_name,
7483ec 166                 secure=self.secure,
DT 167                 digest_algo=self.digest_algo)
a5b033 168             new_cookie_value = ticket.cookie_value()
519300 169             
a5b033 170             if old_cookie_value != new_cookie_value:
519300 171                 # return a set of Set-Cookie headers
299b4c 172                 return self._get_cookies(environ, new_cookie_value, max_age)
a5b033 173
b0f81f 174     # IAuthenticator
TS 175     def authenticate(self, environ, identity):
176         userid = identity.get('repoze.who.plugins.auth_tkt.userid')
177         if userid is None:
178             return None
179         if self.userid_checker and not self.userid_checker(userid):
180             return None
181         identity['repoze.who.userid'] = userid
182         return userid
183
184     def _get_cookies(self, environ, value, max_age=None):
185         if max_age is not None:
15e365 186             max_age = int(max_age)
4f3525 187             later = _utcnow() + datetime.timedelta(seconds=max_age)
b0f81f 188             # Wdy, DD-Mon-YY HH:MM:SS GMT
4f3525 189             expires = "%s, %02d %3s %4d %02d:%02d:%02d GMT" % (
TS 190                 _weekdayname[later.weekday()],
191                 later.day,
192                 _monthname[later.month],
193                 later.year,
194                 later.hour,
195                 later.minute,
196                 later.second,
197             )
b0f81f 198             # the Expires header is *required* at least for IE7 (IE7 does
TS 199             # not respect Max-Age)
200             max_age = "; Max-Age=%s; Expires=%s" % (max_age, expires)
201         else:
202             max_age = ''
203
d7e647 204         secure = ''
BS 205         if self.secure:
3b5782 206             secure = '; secure; HttpOnly'
d7e647 207
b0f81f 208         cur_domain = environ.get('HTTP_HOST', environ.get('SERVER_NAME'))
7f9907 209         cur_domain = cur_domain.split(':')[0] # drop port
b0f81f 210         wild_domain = '.' + cur_domain
TS 211         cookies = [
d7e647 212             ('Set-Cookie', '%s="%s"; Path=/%s%s' % (
BS 213             self.cookie_name, value, max_age, secure)),
214             ('Set-Cookie', '%s="%s"; Path=/; Domain=%s%s%s' % (
215             self.cookie_name, value, cur_domain, max_age, secure)),
216             ('Set-Cookie', '%s="%s"; Path=/; Domain=%s%s%s' % (
217             self.cookie_name, value, wild_domain, max_age, secure))
b0f81f 218             ]
TS 219         return cookies
220
a5b033 221     def __repr__(self):
394ea6 222         return '<%s %s>' % (self.__class__.__name__,
TS 223                             id(self)) #pragma NO COVERAGE
a5b033 224
515c69 225 def _bool(value):
9df42f 226     if isinstance(value, STRING_TYPES):
515c69 227         return value.lower() in ('yes', 'true', '1')
TS 228     return value
229
230 def make_plugin(secret=None,
0ee58d 231                 secretfile=None,
a5b033 232                 cookie_name='auth_tkt',
0ee58d 233                 secure=False,
TS 234                 include_ip=False,
cd1198 235                 timeout=None,
CM 236                 reissue_time=None,
a6f6dc 237                 userid_checker=None,
7483ec 238                 digest_algo=DEFAULT_DIGEST,
0ee58d 239                ):
a6f6dc 240     from repoze.who.utils import resolveDotted
0ee58d 241     if (secret is None and secretfile is None):
TS 242         raise ValueError("One of 'secret' or 'secretfile' must not be None.")
243     if (secret is not None and secretfile is not None):
244         raise ValueError("Specify only one of 'secret' or 'secretfile'.")
245     if secretfile:
246         secretfile = os.path.abspath(os.path.expanduser(secretfile))
247         if not os.path.exists(secretfile):
248             raise ValueError("No such 'secretfile': %s" % secretfile)
c272ce 249         with open(secretfile) as f:
TS 250             secret = f.read().strip()
cd1198 251     if timeout:
CM 252         timeout = int(timeout)
253     if reissue_time:
254         reissue_time = int(reissue_time)
a6f6dc 255     if userid_checker is not None:
CM 256         userid_checker = resolveDotted(userid_checker)
7483ec 257     if isinstance(digest_algo, str):
DT 258         try:
259             digest_algo = getattr(hashlib, digest_algo)
260         except AttributeError:
261             raise ValueError("No such 'digest_algo': %s" % digest_algo)
262
cd1198 263     plugin = AuthTktCookiePlugin(secret,
CM 264                                  cookie_name,
265                                  _bool(secure),
266                                  _bool(include_ip),
267                                  timeout,
268                                  reissue_time,
a6f6dc 269                                  userid_checker,
7483ec 270                                  digest_algo,
cd1198 271                                  )
a5b033 272     return plugin
CM 273