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