/[theodore]/bunnyblog/modules/logintools/login.py


UCC Code Repository

Contents of /bunnyblog/modules/logintools/login.py

Parent Directory Parent Directory | Revision Log Revision Log


Revision 1 - (show annotations) (download) (as text)
Tue Jan 29 14:32:01 2008 UTC (12 years ago) by svn-admin
File MIME type: text/x-python
File size: 14776 byte(s)
Re-import of repository after repository database corruption.

1 # login.py
2 # part of logintools
3 # A CGI Authentication and user account system
4 # Copyright (C) 2004/2005 Michael Foord
5 # E-mail: fuzzyman AT voidspace DOT org DOT uk
6
7 # Released subject to the BSD License
8 # Please see http://www.voidspace.org.uk/documents/BSD-LICENSE.txt
9
10 # Scripts maintained at http://www.voidspace.org.uk/python/index.shtml
11 # Comments, suggestions and bug reports welcome.
12
13 # For information about bugfixes, updates and support, please join the Pythonutils mailing list.
14 # http://voidspace.org.uk/mailman/listinfo/pythonutils_voidspace.org.uk
15 # Comments, suggestions and bug reports welcome.
16
17 import sys
18 import cgi
19 import os
20 import sha
21 from time import time
22
23 # FIXME: cgitb is insecure and should be removed from production code.
24 import cgitb
25 cgitb.enable()
26
27
28 sys.path.append('../modules')
29 from configobj import ConfigObj
30 from dataenc import pass_enc, pass_dec, unexpired, table_enc, table_dec
31 from Cookie import SimpleCookie
32 from pathutils import *
33 from cgiutils import *
34 from loginutils import *
35
36 # a default max age of 15 days. This ensures encoded cookies don't have
37 # unlimited lifetimes.
38 AGETEST = (15,0,0)
39 #THISSCRIPT = os.environ['SCRIPT_NAME']
40 THISSCRIPT = "admin.cgi"
41
42 ####################################################
43 # The names for the various template files used by login_utils
44 # These must be stored in templatedir
45 # Which must be given in 'config.ini' in userdir
46
47 logintemplate = 'login_page.html'
48 form_nojs = 'login_nojs.txt'
49
50 #####################################################
51
52 """
53 Cookie string format is : ::
54
55 username||password SHA||random string``
56
57 which is dataenc'ed with the date-time the cookie was generated.
58
59 The cookie string must successfully de-enc and the username - pass hash match
60 for the cookie to be a valid login. The password is hashed along with the
61 random string, so that the hash is different every time.
62
63 .. warning::
64
65 We don't log failed login attempts. We could return ``None`` for no cookie
66 and ``False`` for a failed login or 'bad' cookie.
67
68 The function called is usually determined by user choices. E.g.
69
70 * Login
71 * perform admin functions
72 * create new login
73
74 The actual action is saved in the form field 'login'.
75
76 The function login determines which function to call. This means most scripts
77 only need directly import and call this function.
78
79 If there is no 'login' form field then the login function assumes that you
80 want to check if the user already has a valid login. This is the 'checklogin'
81 function.
82
83 Many operations of these scripts will generate new pages asking for further
84 intervention from the user. For example - login generates a login page which
85 waits for the user to enter a password. This means if the action interrupts a
86 script (e.g. before performing an action the script must verify user identity)
87 information about what the script was about to do may be lost. The login
88 function allows you to pass in an 'action' parameter. This must be a string
89 escaped to be URL safe. You can use this value to store information about what
90 was being done before the interruption. (A simple way of saving state).
91 """
92 ######################################################################
93 # login is the function most frequently imported
94 # and called directly by external scripts
95
96 def login(theform, userdir, thisscript=None, action=None):
97 """From the form decide which function to call."""
98 # FIXME: this function got a bit more complicated than intended
99 # it also handles checking logins when editaccount or admin
100 # functions are being called.
101 br = False
102 loginaction = ''
103 if thisscript is None:
104 thisscript = THISSCRIPT
105 #
106 # used so that we can tell if we have a genuinely empty action
107 # or not later on....
108 action = sortaction(theform.getfirst('action') or action)
109 #
110 # let's work out what we're supposed - if nothing is specified we'll just
111 # check if we have a valid login and then return
112 try:
113 loginaction = theform['login'].value
114 except KeyError:
115 # this function displays login and exits if there is no valid login
116 action, userconfig, newcookie = checklogin(userdir, thisscript, action)
117 # break - means go straight to the end
118 br = True
119 #
120 # dologin is special
121 # it can be called by functions here - they may require a user to login before continuing.
122 # So if we are returning from a login - we may need to go onto another function
123 if loginaction.startswith('login'):
124 action, userconfig, newcookie = dologin(theform, userdir, thisscript,
125 action) # this function displays login and exits if username doesn't exist or password doesn't match
126 # if this function returns, then the login attempt was succesful
127 userconfig['numlogins'] = str(int(userconfig['numlogins']) + 1) # increment the number of logins in the user config
128 userconfig.write()
129 if action[:11] == 'edacc-mjf||':
130 loginaction = 'editaccount'
131 elif action[:11] == 'admin-mjf||':
132 loginaction = 'admin'
133 else:
134 br = True
135
136 # this is the list of possible actions
137 # they must either call sys.exit() or return action, userconfig, newcookie
138 # functions which can be called without an explicit checklogin must do their own first (to get the new cookie header) - e.g. admin and edit account (otherwise you could access them without being logged in)
139 # if we have already done all this, then br will be set to True
140 if br:
141 pass
142 elif loginaction == 'showlogin':
143 displaylogin(userdir, thisscript)
144 elif loginaction == 'logout':
145 # this means a logout link can be implemented as myscript.py?login=logout
146 print logout(userdir) # this prints the empty cookie
147 displaylogin(userdir, thisscript) # we don't use 'action' in this case because user has explicitly logged out, outstanding action is lost.
148 elif loginaction == 'newlogin':
149 from newlogin import newlogin
150 newlogin(userdir, thisscript, action) # this script always exits - so no return, preserves action though if needed
151 elif loginaction.startswith('donewlogin'):
152 from newlogin import donewlogin
153 donewlogin(theform, userdir, thisscript, action) # this script always exits - so no return, preserves action though if needed
154 elif loginaction == 'confirm':
155 from newlogin import confirm
156 action, userconfig, newcookie = confirm(theform, userdir, thisscript) # no action sent in, because the action is stored in the temporary user store
157 userconfig['created'] = str(time())
158 elif loginaction.startswith('editaccount'):
159 from newlogin import editaccount
160 if action[:11] != 'edacc-mjf||':
161 action, userconfig, newcookie = checklogin(userdir, thisscript, 'edacc-mjf||' + action)
162 action = action[11:]
163 if not istrue(userconfig['editable']):
164 error('Sorry, this account cannot be edited.')
165 editaccount(userdir, thisscript, userconfig, sortaction(action), newcookie) # we have checked that we have a valid login. This script prints a page, so no return
166 elif loginaction.startswith('doeditaccount'):
167 from newlogin import doeditaccount
168 action, userconfig, newcookie = checklogin(userdir, thisscript, action)
169 if not istrue(userconfig['editable']):
170 error('Sorry, this account cannot be edited.')
171 action, userconfig, newcookie = doeditaccount(theform, userconfig, userdir, thisscript, sortaction(action), newcookie) # we have checked that we have a valid login
172 elif loginaction == 'admin':
173 from admin import admin
174 if action[:11] != 'admin-mjf||':
175 action, userconfig, newcookie = checklogin(userdir, thisscript, 'admin-mjf||' + action)
176 action = action[11:]
177 adminlevel = int(userconfig['admin'])
178 if not adminlevel:
179 error("You're not authorised to do that. Sorry.")
180 action, userconfig, newcookie = admin(theform, userdir, thisscript, userconfig, sortaction(action), newcookie) # we have checked that we have a valid login.
181
182 else:
183 displaylogin(userdir, thisscript, action) # we haven't understood - just display the login screen XXXX error message instead ?
184
185 print newcookie # XXXX ought to be a way of returning the cookie object instead...
186 if action == 'EMPTY_VAL_MJF': # the programmer ought never to 'see' this value, although it might get passed to his script by the login code a few times....
187 action = None
188 userconfig['lastused'] = str(time())
189 userconfig['numused'] = str(int(userconfig['numused']) + 1)
190 userconfig.write()
191 return action, userconfig
192
193 ############################################################
194 # Action functions - check a login etc
195
196 def checklogin(userdir, thisscript=None, action=None):
197 """Check if a user has a valid cookie and return an updated one
198 or display the default login screen and exit."""
199 if thisscript == None:
200 thisscript = THISSCRIPT
201 test = isloggedin(userdir)
202 if test:
203 return action, test[0], test[1]
204 else:
205 displaylogin(userdir, thisscript, action)
206
207 def dologin(theform, userdir, thisscript=None, action=None):
208 """Ths is the function called by login when the results of a login form are posted.
209 I.e. this actually does the login
210 If any errors occur (like wrong/missing password) it calls displaylogin and exits.
211 """
212 loginaction = theform['login'].value # used when we have js and nojs versions of dologin
213 theform = getform(['username', 'pass'], theform)
214 if thisscript == None:
215 thisscript = THISSCRIPT
216 if loginaction == 'loginnojs':
217 username = theform['username']
218 password = theform['pass']
219 test = checkpass(username, password, userdir, thisscript, action)
220 return test or displaylogin(userdir, thisscript, action)
221 displaylogin(userdir, thisscript, action) # XXXX should display error message
222
223 def displaylogin(userdir, thisscript=None, action=None):
224 """This function will display the login page and then exit.
225 Usually called if the user has no cookie or an expired/forged cookie.
226 """
227 if thisscript == None:
228 thisscript = THISSCRIPT
229 config = ConfigObj(userdir + 'config.ini')
230 templatedir = config['templatedir']
231
232 loginform = readfile(templatedir+form_nojs)
233 loginform = loginform.replace('**script**', thisscript)
234
235 loginpage = readfile(templatedir+logintemplate)
236 loginpage = loginpage.replace('**login form**', loginform)
237
238 if istrue(config['newloginlink']):
239 loginpage = loginpage.replace('<!-- **commstart**', '')
240 loginpage = loginpage.replace('**commend** -->', '')
241 loginpage = loginpage.replace('**new login link**', thisscript+'?login=newlogin')
242
243 if action:
244 loginpage = loginpage.replace('<!-- **action** -->', actionline % action)
245 print serverline
246 print '\r'
247 print loginpage
248
249 sys.exit()
250
251 def logout(userdir):
252 """Returns the cookie header in the case of an explicit logout."""
253 config = ConfigObj(userdir + 'config.ini')
254 cookiepath = config['cookiepath']
255 return emptycookie(cookiepath)
256
257 ############################################################################
258
259 def isloggedin(userdir):
260 """If user has sent us an in date, valid cookie then return updated cookie header,
261 otherwise return False."""
262 try:
263 rawcookie = os.environ['HTTP_COOKIE']
264 except KeyError:
265 return False
266 thecookie = SimpleCookie(rawcookie)
267 try:
268 cookiestring = thecookie['userid'].value
269 except KeyError:
270 return False
271 test = decodestring(cookiestring, userdir)
272 if not test:
273 return False
274 user, password, cookiepath = test
275 thecookie = makecookie(user, password, cookiepath)
276 return user, thecookie
277
278 def decodestring(cookiestring, userdir):
279 """Given a username/password encoded into a string - decode it and check it's validity.
280 It checks the username against the one stored in the user file..
281 """
282 # try decoding the string, if it's badly formed then it may raise an excpetion - in which case we just return False
283 try:
284 instring, daynumber, timestamp = pass_dec(cookiestring)
285 except:
286 return False
287 # check it's not a really old (or copied) cookie
288 if not unexpired(daynumber, timestamp, AGETEST):
289 return False
290 # we've extracted the timestamped string from the cookie string.
291 # Let's pull out the username and password hash
292 try:
293 username, passhash, ranstring = instring.split('||')
294 except ValueError:
295 return False
296 if not len(ranstring) == 10:
297 return False
298 # Now we need to check it's a valid username and check the password
299 if username in RESERVEDNAMES or not os.path.isfile(userdir+username+'.ini'):
300 return False
301 user = ConfigObj(userdir+username+'.ini')
302 stampedpass = user['password']
303 maxage = user['max-age']
304 cookiepath = ConfigObj(userdir+'config.ini')['cookiepath']
305 # the password is time stamped - so we need to decode it
306 try:
307 password, daynumber, timestamp = pass_dec(stampedpass)
308 except:
309 return False
310 thishash = sha.new(password+ranstring).hexdigest()
311 if thishash != passhash:
312 return False
313 return user, password, cookiepath
314
315 def encodestring(username, password):
316 """Given a username and password return a new encoded string for use by decodecookie."""
317 ranstring = randomstring(10)
318 thishash = sha.new(password + ranstring).hexdigest()
319 return pass_enc('||'.join([username, thishash, ranstring]), daynumber=True, timestamp=True)
320
321 def checkpass(username, password, userdir, thisscript, action):
322 """Check the password from a new login."""
323 # XXXX log failed login attempts
324 if username in RESERVEDNAMES:
325 return False
326 if not os.path.isfile(userdir+username+'.ini'):
327 return False
328 user = ConfigObj(userdir+username+'.ini')
329 stampedpass = user['password']
330 cookiepath = ConfigObj(userdir+'config.ini')['cookiepath']
331 # we need to un-time stamp the password
332 realpass, daynumber, timestamp = pass_dec(stampedpass)
333 if realpass != password:
334 return False
335 open('test.txt', 'w').write(str(user))
336 # if we've got this far then the login was successful and we need to return a cookie
337 thecookie = makecookie(user, password, cookiepath)
338 return action, user, thecookie
339
340 """
341 CHANGELOG
342 =========
343
344 2005/09/09
345 ----------
346
347 Changes to work with pythonutils 0.2.2
348
349 """

Managed by UCC Webmasters ViewVC Help
Powered by ViewVC 1.1.26