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