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