==================== Login Tools ==================== -------------------------------- User Login and Authentication -------------------------------- :Author: Michael Foord :Contact: fuzzyman@voidspace.org.uk :Author Homepage: Pythonutils_ :Version: 0.6.1 :Date: 2005/10/11 :License: `BSD License`_ :Home Page: `Login Tools Homepage`_ :Online Demo: `Jalopy and login demo`_ :Support and Bug Reports: `Pythonutils Mailing List`_ .. _Login Tools Homepage: http://www.voidspace.org.uk/python/logintools.html .. _Pythonutils Mailing List: http://voidspace.org.uk/mailman/listinfo/pythonutils_voidspace.org.uk .. _Pythonutils: http://voidspace.org.uk/python/index.shtml .. _BSD License: http://www.voidspace.org.uk/documents/BSD-LICENSE.txt .. _Python: http://www.python.org .. _Jalopy: http://www.voidspace.org.uk/python/jalopy.html .. _`Jalopy and logintools Zip (636k)`: http://www.voidspace.org.uk/cgi-bin/voidspace/downman.py?file=jalopy_login.zip .. _Jalopy and login demo: http://www.voidspace.org.uk/cgi-bin/jalopydemo/jalopy.py .. _ConfigObj: http://www.voidspace.org.uk/python/configobj.html .. _Voidspace: http://www.voidspace.org.uk .. meta:: :description: The homepage of Login Tools. A Python CGI framework for performing user logins and authentication. :keywords: python, script, module, login, authentication, CGI, developer, framework, user, account .. contents:: .. sectnum:: Introduction ============= **logintools** is a set of scripts for handling user accounts and authentication. It is designed to work with Python_ CGIs, and can be added to any web application with as little as two lines of code. It acts as a whole login framework, and provides new user sign ups, administrator login, and user 'edit account' features. It includes a mechanism for storing/accessing additional user preferences. logintools uses HTML templates for the various screens, meaning it can fit into any look and feel of website/application. By storing templates and the config file in different directories, a single install can handle logins for many applications. Applications can share a user base or have separate ones. They can also share html templates or have a separate directory. Various aspects are configurable - like whether or not new users can sign up. There are generally two different reasons you might want to make users login. You might have information, or an application, that you want to restrict access to. Alternatively, you may want to tailor an application to the specific user - this includes recording user preferences and tracking who has done what. You can achieve both of these with a user login. Doing this needs a whole framework - storing preferences, administering accounts, allowing users to change their password, and so on. Basic authentication on the server is one solution - but it's difficult to integrate with CGI. logintools provides a whole framework to do this. At the moment logintools comes distributed in a single zip file along with the Jalopy_ project. Jalopy is a collaborative web project that uses logintools. It gives you a website with a WYSIWYG online editor so that several people can edit the website. The private index to do this is protected by logintools - there is also a public index to just view the pages. Jalopy provides a very nice example of how to use logintools. Adding the login framework to Jalopy was really as easy as adding the following 2 lines of code : .. raw:: html {+coloring} from logintools import login # this function displays login screen if it fails action, userconfig = login(theform, userdir) {-coloring} There are a couple of other places where Jalopy prints links to log out, edit accounts and enter the admin screen - but basically that's it. The userconfig returned is an object that represents this users preferences. `The action value`_ is a special value that is described in more detail later. logintools works as described - but is still alpha quality. I will keep API changes to a minimum, but I don't guarantee this. Particularly, the way it uses cookies is likely to change. Bug reports welcomed, as are suggestions. The standard logintools package (and Jalopy) could be made to look a heck of a lot nicer if I had some decent HTML templates. If someone wanted a way to contribute, this would be a very good way ! logintools is designed to be used with Python 2.2 or later. Downloading ============ The Jalopy and logintools package can be downloaded from `Jalopy and logintools Zip (636k)`_. You can try out an online demonstration of Jalopy and logintools at `Jalopy and login demo`_. The demo should have an administrator login. This means you can create and edit user files. Obviously any logins you create can also be edited and deleted by other people. It would be helpful if you didn't change the password of the admin login. ------------------ Installing =========== Setting up login tools to work with your scripts is a several stage process. They're all simple though : * Install the modules directory somewhere your script can import from * Choose a location for the user directory * Setup your configuration in 'config.ini' * Setup your HTML templates. * Modify your script to call the login function Installing the Modules ----------------------- logintools is dependent on several modules. (which are all generally useful by the way). By default logintools comes in a directory called 'modules' which contains all it's dependencies. To use them, add the modules directory to your ``sys.path`` : .. raw:: html {+coloring} import sys moduleDirLoc = '../modules' sys.path.append(moduleDirLoc) {-coloring} See the directory structure of the Jalopy_ distribution if you want an example of how this works. The advantage of this is that other programs (especially Jalopy !) can also access these modules. Once you have installed the modules like this they can be used by several programs. Each program can have a separate 'user directory' but share the modules. User Directory --------------- Every person who has (or creates) a login will have a single config file that stores all their details. These are stored in the a directory referred to as the 'user directory' and are named 'login name' + '.ini'. If you have several scripts using logintools on the same server, they can either share a user directory or have different ones. The user directory also needs to contain the basic config files. See the 'users' directory that comes in the standard distribution. It's in the jalopy directory, because it's the user directory for the Jalopy program. This contains the following files : 1) *config.ini* - the configuration file for this setup. 2) *default.ini* - the template used to create new users. It contains some default values. 3) *admin.ini* - an example admin login. This is setup as the main administrator in config.ini. 4) *guest.ini* - an example user login. 5) *temp.ini* - used to store pending (unconfirmed) users. Every time you call the login functions, you'll need to tell it where to find this user directory. From this it can access all the users, *and* the config file. `The config file`_ tells it where to find `the HTML templates`_ as well as configuring the behaviour of logintools. Having a file called 'admin.ini' means their is a login with the username 'admin'. Having a file called 'guest.ini' means their is a login with the username 'guest'. (The other files are reserved names, not logins). These two default users start with the password 'pass1'. It is recommended that you change the name of the admin login by changing the filename (and the 'username' value inside the file). You can change the password from the 'edit account' screen. The Config File ---------------- Have a look in 'config.ini' in the users directory. You'll see it's comprised of various keywords and corresponding values. The config file, and all user files, are all basic config files. As you will discover in a bit, they are in a format specially for use with a Python module called ConfigObj_ : :: templatedir = 'logintemplates/' cookiepath='/cgi-bin/jalopy' adminuser='admin' newloginlink='Yes' adminmail='fuzzyman@voidspace.org.uk' email_subject = 'New Jalopy Login' email_message = '''You have signed up for, or been invited to join Jalopy. Clicking on this link will confirm your new login.''' templatedir ~~~~~~~~~~~~ The most important value here is the 'templatedir' one. This tells logintools where to find the HTML templates. When the login functions are called, the current working directory will usually be the directory that *the main calling script* is in. The templatedir location should either be an absolute location, or relative to the current working directory of the calling script. If several scripts in different locations share the user directory, then it will need to be an absolute location. cookiepath ~~~~~~~~~~~ logintools uses a cookie to store whether or not a user is logged in. An obfuscated hash of the password is stored in the cookie, so it's difficult to forge. It does mean that at the moment logintools doesn't play well with applications that use cookies. It's easy to program round this *or* for me to change logintools, if it's a problem for anyone. The cookie path tells the browser when to send our cookie to the server. If logintools is the only application in your domain setting a cookie then you don't need to worry about cookiepath. You could set it to ``'/'`` - which means send it with every request. If you had other applications that (potentially) set cookies, you need to set it to a unique path that identifies your application. In the default example it will only return the cookie when you access URLs that start '/cgi-bin/jalopy'. This also allows you to have several applications using logintools on the same server. With different cookiepaths they can all co-exist without interfering. I am likely to get rid of the requirement to have a unique cookiepath per application and replace it with a name for each setup. This name will be stored in the config file instead of the cookie path. (This will mean you can have several applications with separate cookies but the same cookiepath. adminuser ~~~~~~~~~~~ This value is the login name of the main admin user. This user can't be edited or deleted by any other administrator. newloginlink ~~~~~~~~~~~~~ This option determines whether or not the 'create new login' link appears on the main login screen. This value should be set to 'Yes' or 'No'. If it is 'No' then users are unable to sign up for new accounts and the link doesn't appear. You are still able to create new accounts or invite new users through the admin interface. adminmail ~~~~~~~~~~~ If 'newloginlink' is 'Yes' then users can sign up for new accounts. After entering their details, they will be emailed a link to confirm the new account. This makes creating new logins automatically harder. If adminmail is set - that email address will be mailed whenever a new account is confirmed. Set it to ``''`` to switch this off. email_subject & email_message ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ These two values make up the message that a new user is sent. It is sent to a user when they create a new account *or* they are invited by an administrator. The HTML Templates ------------------- Having set up a user directory with a 'config.ini' file you will have specified a directory that contains all the HTML templates. The one in the standard distribution is the directory called 'logintemplates' in the 'jalopy' directory. This contains five HTML files and seven text files. These are all used to generate the different screens used by logintools. This includes the main login screen, creating a new account, editing your account details and all the admin screens. Each template file contains the HTML framework and some values that are filled in by the logintools scripts. When you edit these files you must keep these values in. These values all look like this - ``**some value**``. For a full overview of what each file is for, and what all the values are for, check out the `template files and values`_ section. Many of the pages consist of a basic page (filename usually ending in *.html* in the template directory) with a form to be inserted into it. This form will usually be a file with a name ending *.txt* in the template directory. ------------------- Login Tools in Your Scripts ============================ Integrating login_tool into your scripts is very easy. Exactly how depends on how you want your application to work. There are three different possibilities : 1) You want to prevent people from using a script unless they are logged in. `No Login, No Access`_ 2) You want to prevent people from using part of a script unless they are logged in. `Protecting Part of An Application`_ 3) You want to provide a different view or different options if they are logged in. `Is the User Logged In ?`_ The basic function that handles everything is the 'login' function. The first case is the simplest to deal with, so I'll illustrate login with that. It uses the login function - in many cases it might be the only thing you need to import from logintools - ``from logintools import login``. No Login, No Access -------------------- logintools uses cookies for user authentication. If the user doesn't have a valid cookie then calls to the login function display the login screen and then call ``sys.exit()``. The login screen is made up with the `login_page.html`_ template file. This has the `newlogin_nojs.txt`_ form inserted into it. The user then has a choice. If they already have a login they can sign in with their username and password. Otherwise, assuming new logins are enabled for your application, they can hit the 'create new login' link. Either of these choices will result in a form submission *to your script* with a *login* parameter set. Your script must detect that this access is destined for the l ogin function (because it has a login parameter) and pass it to the login function. **However**, because we are in situation1 - we want to trap *all* accesses anyway. So the *first* thing your script ought to do is to call the login function. If the access results from the user doing something within the logintools, then it will be picked up automatically. Assuming you have chosen a user directory this can be done with the following code : .. raw:: html {+coloring} import cgi import sys import os sys.path.append('../modules') from logintools import login thisscript = os.environ.get('SCRIPT_NAME', '') action = None userdir = 'users' theform = cgi.FieldStorage() action, userconfig = login(theform, userdir, thisscript, action) {-coloring} If your script always starts with something like this, then the user must login or the script exits. Nice and easy. The login function returns two values - action and userconfig. If you are doing things this way, you probably don't need to use `the action value`_. In this case you can slightly simplify your call to the login function : .. raw:: html {+coloring} action, userconfig = login(theform, userdir) {-coloring} When you call login, action defaults to None and thisscript defaults to the environment variable 'SCRIPT_NAME'. This is the actual CGI script that the user called - and is put into any URLs generated by logintools. You can use the 'thisscript' parameter to divert new calls to a different CGI script. ``userconfig`` is an important creature. userconfig is an object that represents the users account. It is actually an instance of ConfigObj_ - which behaves like a dictionary. See the `user config files`_ sections for more details of the values available in it. You can use it to get the email address, real name and the login name of the current user. : .. raw:: html {+coloring} username = userconfig['username'] realname = userconfig['realname'] email = userconfig['email'] {-coloring} So long as you avoid all the keys that logintools uses, you can also use this object to store other settings etc for the user. You could use it to implement session persistence, or just save user preferences. There are lots of special methods and attributes associated with ConfigObj_ - but the important one for us is the **write** method. Simply treat the object as a dictionary and call write to save your changes. : .. raw:: html {+coloring} userconfig['favourite colour'] = 'red' userconfig['favourite number'] = '3' userconfig['list of items'] = ['choice 1', 'choice 2', 'choice 3'] userconfig.write() {-coloring} ConfigObj instances can store strings or lists of strings. You can also nest sections, which you access/set as sdictionaries. Something else that you ought to note. Because logintools uses the 'login' parameter to tell if the current call is meant for it you mustn't use 'login' as one of your cgi parameters. The same is true of 'action'. This is used to store any action value you pass to login. 'login' and 'action ' are reserved parameters. See `The Action Value`_ section for the lowdown on 'action'. Protecting Part of An Application ----------------------------------- An alternative use of authentication is to just protect *part* of an application with a login. For example you may want people to be able to browse a dynamically generated list of files - but only be able to download them if they are logged in. In fact it's possible that the call to the login function could come from several different places in your script. The problem is, that when the login screen is displayed and the user enters their password - the script is restarted. Such is the nature of CGIs. What you need is some way of knowing what the user was doing (or trying to do) when they logged in. Also, if the user selects something like an 'edit account' option then the application will be restarted with an instruction to the login function to go to display the appropriate 'edit account' screen. Your application must intercept this call and pass it straight to the login function. So we have two needs. The first is to know where in your application a call to logintools came from. The second is to always trap calls that are intended for the login function. Luckily the answers to both these problems are intertwined into a single solution. The answer involves using `the action value`_ to tell you where the call came from. Any calls to your script that use the reserved parameter 'login' need to be passed immediately to the login function. This returns your original 'action' value back to you - and tells you where the user came from. This is all explained in detail (with examples), in the section entitled `The Action Value`_. In summary, any value you pass into the login function will be preserved throughout the whole login process and returned once a successful login has been achieved. So if you pass into action something that identifies where login is called from, when you trap the call to your script right at the start - this value will be returned to you. This tells you which part of your program you need to go to. Confused ? Go and read `The Action Value`_ section. .. raw:: html {+coloring} # start of the program if theform.has_key('login'): # only called if we are in the middle of a login process action, userconfig = login(theform, userdir, thisscript) if action == 'part1': call_part1(userconfig, theform) elif action == 'part2': call_part2(userconfig, theform) elif action == 'part3': call_part3(userconfig, theform) {-coloring} Is the User Logged In ? --------------------------- A third alternative is to provide users with a different view, or options, depending on whether they are logged in or not. This means you need a function that will tell you. **isloggedin** does the job. It returns ``False`` if the current user isn't logged in, or a ``(userconfig, newcookieheader)`` tuple if they are. You can print the newcookieheader if you want - but it's not absolutely necessary. As a simple example : .. raw:: html {+coloring} from logintools import isloggedin test = isloggedin(userdir) if not test: print "The user isn't logged in." else: userconfig = test[0] {-coloring} We really ought to test for the login parameter as well, in case they have just logged in, or are even creating a new login. A more practical example might look something like : .. raw:: html {+coloring} import cgi from logintools import login, isloggedin theform = cgi.FieldStorage() userconfig = None if theform.has_key('login'): action, userconfig = login(theform, userdir) else: test = isloggedin(userdir) if test: userconfig = test[0] if userconfig: username = userconfig['username'] else: username = 'Guest User' {-coloring} If the current user isn't logged in then userconfig will be set to ``None``, if they are logged in then userconfig will be the user config file. If we had only called the login function, then the user wouldn't be able to see anything other than the login screen. A useful thing to do is to display a link to the login screen if the user isn't logged in. If they are logged in then there are various other options you may want to provide links to. The next section `Links to the Other Options`_ shows how to do this. You could also include the login form within your own page - the login form is in the *login_nojs.txt* file in the logintemplates directory. It is expected that you will edit this form to fit in with the appearance of your own pages, but don't forget it is used by the displaylogin function as well. Links to the Other Options ---------------------------- The function login that we have mainly used so far, only gives access to the login screen and create a new login screen. logintools also includes a framework for the user to edit their account, an admin login to administer all the accounts, and more besides. These can be accessed by requests encoded into a URL - in other words, by ordinary links. As we have already seen, any program that uses logintools must trap calls that use the login parameter and pass them to the login function. The value that we pass in the login parameter determines what happens. Assuming myscript.py is the path to your script, the following links below would cause the noted behaviour : *myscript.py?login=showlogin* - display the login screen *myscript.py?login=logout* - logout the current user *myscript.py?login=newlogin* - enter the create new login screen *myscript.py?login=confirm&id=ID_VALUE (sent in an email)* - confirm a pending login *myscript.py?login=editaccount(&action=ACTION_VALUE)* - edit an account, optional action value. *myscript.py?login=admin(&action=ACTION_VALUE)* - enter the admin screens, optional action value. Some of these links will only work in certain situations. For example, the admin link will only work if the currently logged in user has admin rights. The confirm a pending login link will only work if the ID_VALUE is a valid one. The create a new login one will only work if new logins are enabled on this setup. You can see that the last two links (edit account and admin screens) can have an action value added to them. If you add these links dynamically from your script, you can set the action value in the normal way to tell where the call was made from. This means when the user has finished editing their account, you can return them to where they were before. A side effect of using GET requests (requests in URLs), is that the user can bookmark the 'edit account' screen. If they return to it without being logged in - logintools will ask them to log in first, and then put them into the edit account screen. It uses the action value internally to do this. This is all used to good effect in Jalopy_. After you log into Jalopy it presents you with the main Jalopy screen. It has a logout link and an edit account link. If you have admin rights it will *also* display the admin screen link. It does this by testing the admin level in the userconfig. It also prints a personalised greeting to who ever is logged in by reading 'realname' from the userconfig. An easy way to generate a link to your script is to use the ``os.environ['SCRIPT_NAME']``. : .. raw:: html {+coloring} url = '%s' thisscript = os.environ['SCRIPT_NAME'] logoutval = thisscript + '?login=logout' edaccval = thisscript + '?login=editaccount' adminval = thisscript + '?login=admin' if action: # either use a url safe value or use urllib.quote_plus(action) actionline = '&action=' + action logoutval += actionline edaccval += actionline adminval += actionline logout_link = url %(logoutval, 'Log Out') edacc_link = url % (edaccval, 'Edit Account') if int(userconfig['admin']) > 0: # or just if int(userconfig['admin']): admin_link = url % (adminval, 'Admin Screen') else: admin_link = '' {-coloring} Below is a similar chunk of code from jalopy. It isn't so straightforward because it's doing replace in the template file, but you can see the end part of the URL being added. If the userconfig['admin'] value is greater than '0' then the comments around the admin link are removed and the link filled in. You can see that in jalopy I don't use the action value. In which case it is perfectly safe to miss it out altogether. : .. raw:: html {+coloring} privatepage = privatepage.replace('**logout**', thisscript+'?login=logout') privatepage = privatepage.replace('**edit account**', thisscript+'?login=editaccount') privatepage = privatepage.replace('', '') if int(userconfig['admin']): privatepage = privatepage.replace('', '') privatepage = privatepage.replace('**admin menu**', thisscript+'?login=admin') {-coloring} If you have any questions, about this or anything else, then feel free to ask. The best place to ask questions is the `PythonUtils Mailing List`_. -------------- User Config Files ================== We have already mentioned that the user config files are ConfigObj_ instances. We have also said that you mustn't overwrite any of the keywords that logintools uses. To see what these are I *could* tell you to look at guest.ini or admin.ini in the 'users' directory. However, I'll also list them here with a brief explanation for each. * **admin** - this is an integer representing the admin level of the user. Anything higher than 0 gives admin rights to the user. Admin users can only edit or delete users with a lower admin level. Through accessing this admin value, you have an easy way of adding admin levels to your own scripts. * **password** - this is the user password. It is encoded using a module called dataenc. This is marginally nicer than storing plain text passwords. * **max-age** - this is the maximum lifetime of the cookie we use in seconds. Having it set to '0' usually tells the browser to only keep the browser for this session. This requires the user to login again next time. Setting it to a very low, non-zero, value is not very clever. Setting it to 86400 (1 day) or 604800 (1 week) is not unsensible. * **realname** - This should be the name the user expects to be known by. It is nicer to use this than the login name. * **username** - This is obviously part of the filename. The filename can be accessed as the attribute userconfig.filename - but the username value is included for convenience. * **email** - When a user first signs up (unless an admin creates the account), this will be a verified email address. I do allow the user to change the address without verifying - so it may not always be a valid address. A basic 'syntax' check is done on all email addresses entered, but nothing more than that. * **editable** - This should be 'Yes' or 'No'. I put it in place so that I could have a guest account on some of my scripts. I didn't want people using the guest login to be able to change the password ! If an account is not editable then they won't be able to use the 'edit account' screens. * **numlogins**, **numused** - This is the number of times this user has logged in and the number of 'login' protected pages that they have accessed. The 'numused' number doesn't include pages within logintools, like the edit account screen etc. It only counts pages within your scripts. * **created**, **lastused** - These are times returned by time.time(). 'created' is when the account was created (or confirmed). 'lastused' is the last time it was used. (It would be very easy to use this value to implement a function that cleaned up unused accounts - in general it's not very useful, as it will always be a recent time if checked from a userconfig returned by the 'login' function). These times can be turned back into a date using something like ``time.ctime(float(userconfig['lastused']))``. You are free to add and remove any other keywords to the user config. You can save them by calling userconfig.write(). Creating New Accounts ----------------------- You will notice the file 'default.ini' in the 'users' directory. This is a blank user config file. New accounts are either created by new users signing up *or* from the admin interface. In either case new accounts are created by copying 'default.ini' and then filling in the values from the users choices. New accounts that require email confirmation (link sent to email address provided) are stored in 'temp.ini' until they are confirmed. Minimum details needed to create a new account are : * password * login name * real name * email address (Note that the following user names are reserved - 'config', 'default', 'temp'). If you need to be able to create accounts from your program you can import and use the 'createuser' function. This *doesn't* check first if the username already exists ! (So you could use it just to change the values) : .. raw:: html {+coloring} from logintools import createuser createuser(userdir, realname, username, email, password) {-coloring} Values in Default User ~~~~~~~~~~~~~~~~~~~~~~~~ The default user ('default.ini') contains all the values in a normal account. Most of these will be overwritten by user choices, some are filled in as the account is created/used. The ones you need to specifically set to your preferred values are : * *max-age* * *admin* * *editable* Extending the User Config File ---------------------------------- It is also possible to add your own values to 'default.ini' - these will automatically be given to any new accounts. Add to this the fact that you are always returned the userconfig file from the login function (and the ``isloggedin`` function) as well as the ultra-simple method of changing/saving it - makes the userconfig file a logical place to store user preferences. The logintools framework has already got built into it an experimental method for fetching additional values from the user when they log in. In order for it to be fully useful it needs to be extended though. These values need to be accessible in : 1) User sign up 2) edit default user 3) admin create/invite new user 4) user edit account screen At the moment I've only implemented number 1. In addition to this there would need to be a way of specifying which values the user should be allowed to edit. (As ConfigObj_ can store lists, that wouldn't be so hard). As it would be a fair amount of work I will only extend this if someone requests it and is willing to work on testing it with me. See `Keyword Lists in templates`_ for what I've done so far. It would get round a problem though. At the moment the administrator can change the login name of accounts or delete accounts. In addition to which users may need to be able to sign up for new accounts. If you *aren't* using the user config to store user preferences - but are storing them somewhere else, e.g. a database - then there is a problem keeping the logintools details in sync with the rest of your details. The easiest way round this would be to allow the programmer (you) to register functions to be called when any of these things happen. Again - if anyone needs this, and is willing to help me test it, then get in contact with me. ---------------- Using Login Tools ==================== The appearance, and to an extent the functionality, of all aspects of logintools is dependent on the HTML templates. This section just lists the different screens that logintools makes available. As a User ------------- Using logintools should be very easy - all the processes are familiar. Below are the various parts of logintools accessible to the user. Login Screen ~~~~~~~~~~~~~ This is the default screen. It is presented to the user if the login function is called without parameters and the user isn't logged in. It can also be presented by calling the *displaylogin* function directly or by calling login with 'login=displaylogin' set. If new logins are enabled, it will normally show a link to create a new user account. Sign Up For a New Account ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ It is only possible for users to sign up for new accounts if it is enabled in 'config.ini'. Clicking on a link similar to *myscript.py?login=newlogin*, will direct the user to a form asking for various details to create a new account. Details asked for so far are : * password and confirmation password * login name * real name * email address Once the user has filled this form in, a confirmation email is sent to the email address they provide. Clicking on the link sent in this email will confirm the account and drop them straight into the application. Edit Account ~~~~~~~~~~~~~~ This is only available to the user if their account is 'editable'. Options they can change are : * password * real name * email address Any email address they enter will be checked for basic validity, but will not need to be confirmed. Log Out ~~~~~~~~ Not really an alternative screen. If this is done, the users login cookie is cancelled and they are dropped back to the login screen. As an Administrator -------------------- If a user has admin rights, clicking on a link like *myscript.py?login=admin* will drop them into the admin menu. This gives them access to various screens to administrate the setup and user accounts. Providing a link like this is the job of the programmer. It is simple to test if a user has admin rights, and if they do provide a suitable link, from within the application. Edit Config File & Default User ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This menu option provides access to the options that affect the whole setup. Along with the form to change the values is an explanation of what the values are for. The Config File Values affect the way that logintools behaves, the Default User Values are values that are given to every new account created. **Config File Values** * *newloginlink* Setting this to 'Yes' or 'No' determines whether users will be able to create new logins or not. It also determines whether the 'Create New Login' appears on the login screen. * *adminmail* This is the email address that reports of new login confirmations are sent to. Setting this to '' switches this off. * *email_subject* This is the Subject Line of the emails sent to new users. * *email_message* This is the body of the message sent to new users, along with the link to confirm the account. * *Default User Values* * *max-age* This is the maximum age of the cookie we use, in seconds. Setting it to zero usually means (slightly browser dependent) the cookie will only endure for that browser session. Common values are 3600=1 hour, 86400=1 day, 604800=1 week. The cookie is reset after every new page access - so this is the maximum time in between visits that the cookie will last. After that, the user will have to login again. * *editable* This is whether the user is allowed to change their password, email address etc. Create or Invite New User ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This is where you can create new logins. If you select 'Invite User' then an email will be sent to the address you supply, asking the user to confirm the new login (and telling them their initial password). If you select 'Create New User' then the new login will be created immediately (and no email sent). Admin level should be a number between 0 and 3. For normal users the value should be 0. Obviously user names must be unique. There are a few reserved user names - 'config', 'default', 'temp', 'email'. Edit or Delete User ~~~~~~~~~~~~~~~~~~~~ From here you can edit or delete any account with a lower admin level than you. Accounts are shown in alphabetical order. You cannot edit or delete yourself, or the main administrator. To edit yourself use the normal 'edit account' option. You can even change the login name of a user here. If you are using the login name to refer to additional files for that user (or database entries) this *could* cause problems. If you want to be able to plug in a function to calls to this *or* have the option disabled, then let me know. -------------- The Action Value ================= The action value is used so that your application can tell where in the application the login attempt was made from. When you require a login it is possible that the application will have to restart. If the user isn't already logged in, the 'displaylogin' function is called. This displays the login screen and then exits. When the user does the log in, the application starts again. If you have several places in the application that could have required a login you won't know what the user was trying to do *before* they had to log in. The action value is a way round this. Any value you pass into the login function will be preserved throughout the whole login process and returned once a successful login has been achieved. This is even true of completely new logins, the action value is stored with the account details and returned once the account is confirmed. To use it you will need to recognise when a login attempt has been made. logintools uses both a 'login' parameter *and* the 'action' parameter - you shouldn't use either of these yourself. If your application is called with the 'login' parameter present you know that a login attempt is being made. You should then call the login function. If you passed into the login function a value for the action parameter - you will get this back. As an illustration, suppose there are three parts of your application that require a user to be logged in. You could achieve this with something like this... (3 chunks of code) : .. raw:: html {+coloring} #....... action = 'part1' action, userconfig = login(theform, userdir, thisscript, action) call_part1(userconfig, theform) #....... action = 'part2' action, userconfig = login(theform, userdir, thisscript, action) call_part2(userconfig, theform) #....... action = 'part1' action, userconfig = login(theform, userdir, thisscript, action) call_part3(userconfig, theform) {-coloring} If the user is already logged in, then the login function returns the userconfig and calls the relevant function - call_part1, call_part2 or call_part3. If the user isn't logged in, then the login screen will be displayed. If the user enters a password then the application is restarted (as is case with any form submission to a CGI). Your application must then pass form submission back to the login function for processing. If the login attempt is successful, then the login function returns the userconfig *and* the action value that was originally passed in. (action must be a string of course). You can then use this action value to determine what the user was trying to do, and call the correct part of your program. Code to deal with this should go right at the start of the application, and might look like this : .. raw:: html {+coloring} if theform.has_key('login'): # only called if we are in the middle of a login process action, userconfig = login(theform, userdir, thisscript) if action == 'part1': call_part1(userconfig, theform) elif action == 'part2': call_part2(userconfig, theform) elif action == 'part3': call_part3(userconfig, theform) {-coloring} If you needed to save any values on the server before calling login (session persistence), you could save the sessionid into action as well. i.e. something like ``action = 'part1||' + sessionid``, followed by : .. raw:: html {+coloring} if action.startswith('part1'): sessionid = action[7:] call_part1(userconfig, sessionid) {-coloring} I don't think logintools plays nicely with other cookies yet (it just prints it's own cookie header). It would be quite easy for me to get it to return a ``SimpleCookie`` object instead if you wanted.... The action parameter is passed from page to page to preserve it. It *could* also be passed as part of a url (a GET request). This means you should limit it's size (1024 bytes is a fair maximum to work to). The longer it is the longer each page will take to load and each form to process. When a user creates a new account they have to click on a link sent to them by email. The action parameter is saved with their account details and returned to the program when they click on this link. formencode and formdecode -------------------------- You don't have to stick with just preserving single values in the action parameter though. Often you might only want to require a user to be logged in for certain actions. These actions may include a form submission with several values in it. Using a function called 'formencode' you can encode the whole contents of the form into a single string. This string will be returned you in the normal way. You can use a function called 'formdecode' to turn it back into a dictionary of these values. Note that you should still stick with 1024 byte maximum ! This means you should only process file uploads or large form submissions once the user is into a part of the application where they are already logged in. formencode/decode definitely can't handle multipart form data - they can only handle normal single or list values. : .. raw:: html {+coloring} import cgi from logintools import formencode, formdecode, login theform = cgi.FieldStorage() action = None if not theform.has_key(login): action = formencode(theform) action, userconfig = login(theform, userdir, thisscript, action) formdict = formdecode(action) {-coloring} Note that the login function checks for an action parameter in the form first. If one is present it ignores whatever is passed into it. This means that if our user has to login - so a second form submission is received, it will ignore the action value that is passed in on the second submission. If the login is successful, the action that is returned is the *original* one. formdict is then a dictionary representing the original form passed into the script. Action is Used Internally -------------------------- The nature of logintools is that the user may jump from screen to screen within the login process. (Edit account screen etc). This means two things. 1) Your application must be able to intercept calls that need to go to the login function. As mentioned before you can do this by checking if your script is called with the login parameter : :: if theform.has_key('login'): action, userconfig = login(theform, userdir, thisscript) (If you're not using the userconfig and action values you could even just call ``login(theform, userdir, thisscript)`` which automatically discards the return values). 2) The action parameter is used internally by the logintools function. This means that it may have strange values when it is passed to your script. Sometimes you might see a url that ends something like ``&action=EMPTY_VAL_MJF``. This is an internal value for action as part of a GET request (form parameters passed in the url). These values *should* be removed before 'action' is returned to your script. You will only ever get back from action what you put in. Passing in an action value is optional. If you pass nothing in you will either get back ``''`` *or* ``None``. This means you need to avoid making ``''`` a significant value for action. If this is a problem for anyone, let me know. Dependencies ============ By default logintools comes with the modules it's dependent on. These are currently : configobj, caseless, listquote, dateutils, dataenc, pathutils and cgiutils. The normal situation is to put all these modules in a folder called modules with login.py and the logintools directory in this folder too. The home of these modules is http://www.voidspace.org.uk/python/modules.shtml They are all very useful in their own right. These modules include : * **configobj** - easy reading and writing of config files * **caseless** - a case insensitive dictionary, list and sort function * **dataenc** - timestamp and encode values for including in form fields and cookies * **dateutils** - functions for dealing with dates, particularly useful is the date to *julian day number* * **listquote** - reading lists and quoting/unquoting elements * **cgiutils** - a few standard functions for use with CGI (including form handling and sending email) * **pathutils** - basic file and path handling functions. logintools will only work on Python 2.2 or more recent. --------------------- Keyword Lists in templates =========================== The userconfig file make a very logical place to store user preferences etc. As I discussed in `Extending the User Config File`_, this means that you may need to provide a web interface to amending values in the userconfig file. This would be for both your users *and* for admin purposes. It is made more complicated by : * You wouldn't want to edit every value * Some values should only be editable by admin * Some values may be more appropriately edited from a checkbox or drop down list It is perfectly possible to develop a schema that implements all this, but it's not straightforward. I will only implement this fully if *I* need it, or if someone else asks for it and is willing to help me test it. I have already implemented a start of this in the user sign up section. (untested so far, but it should work.) It works like this. In the template file **newlogin_nojs.txt** is the form used to get new logins. By default it only collects the username, real name, password and email. You can extend this form to include extra values with text inputs. These extra values are just put straight into the user config file. Easy hey ! There's a bit more to it though. When a validation error occurs the form is reprinted for the error to be corrected. This could be something like the email address being invalid, the passwords not matching, or the username already existing. When this happens it is much more friendly if the values the user has just entered are kept in the form (rather than them losing everything they have typed). To do this, logintools needs to know what fields you are using and where it should put these values. For every additional input field in your form you need to add ``value="**fieldname**"`` . You should also include somewhere in the page a comment containing a list of all the fields to be filled in. The list should look as follows ````. The key thing is that it starts ````. - ```` This is the comment end. It is also removed if new logins are allowed. login_nojs.txt -------------- This is the standard form that is put into the login page. If you edit this, you must leave all the input fields within the form. Changing any part of the form will affect logintools. - ``**script**`` The main script to send the form submissions to. Not optional. - ```` Replaced with the action parameter. Not optional. newlogin_page.html ------------------ This is the page that is displayed when someone wants to sign up for a new account. - ```` This is used to display messages to the user if any errors occur. Not optional. - ``**new login form**`` This is replaced with the new login form. Not optional. newlogin_nojs.txt ----------------- This is the standard form that goes into the newlogin page. - ``**script**`` The main script to send the form submissions to. Not optional. - ``**realname**`` This is replaced with the realname in the event of error. Not optional. - ``**username**`` This is replaced with the login name in the event of error. Not optional. - ``**email**`` This is replaced with the email in the event of error. Not optional. login_done.html --------------- This is the page displayed when the new login process is completed. - ``**this script**`` A link back to the main script. Optional edacc_page.html --------------- This is the page that is displayed when the user edits their account. - ```` This is used to display messages to the user if any errors occur. Not optional. - ``**created on**`` Shows when the account was created. Optional - ``**num logins**`` Shows how many times this user has logged in. Optional - ``**num used**`` Shows how many protected pages the user has accessed. Optional - ``**last used**`` Shows when the account was last used - will always be recent ! Optional - ``**this script**`` Link back to the main script. Optional - ``**edit account form**`` Replaced with the edit account form. Not optional. editform_nojs.txt ------------------ This is the form that goes into the user edit account page. - ``**script**`` The main script to send the form submissions to. Not optional. - ``**realname**`` This is replaced with the realname in the event of error. Not optional. - ``**email**`` This is replaced with the email in the event of error. Not optional. - ```` Replaced with the action parameter. Not optional. admin_page.html ---------------- This is the main template for the admin pages. It is used for all the pages accessed through the admin menu. The contents of the page is inserted where the ``**admin**`` marker appears. The differing contents for the various pages are based on the text files that start ``'admin_'``. - ``**this script**`` Link back to the main script. Optional - ``**admin menu**`` Link back to the admin menu. Optional - ```` This is used to display messages to the user if any errors occur. Not optional. - ``**admin**`` The actual admin page being viewed. Not optional admin_menu.txt ---------------- This is the main admin menu screen. - ``**invite**`` Link to invite/create new users. Not optional. - ``**edit users**`` Link to edit/delete users. Not optional. - ``**edit config**`` Link to edit the config file/default user. Not optional. admin_eduser.txt ------------------ This is the header of the page for editing and deleting users. The actual table for each user is hardwired into admin.py. It could probably be broken out into a template if anyone wanted to edit it. - ``**account table**`` Replaced with the table of accounts. Not optional. admin_config.txt ------------------- This is the template used when you are in the edit config file/default user page. - ``**script**`` The main script to send the form submissions to. Not optional. - ``**action**`` Replaced with the action parameter. Not optional. The rest of the values are filled in with entries from the default user and the config file. None of these are optional. - ``**loginlink**`` - ``**adminmail**`` - ``**email subject**`` - ``**email message**`` - ``**maxage**`` - ``**editable**`` admin_invite.txt ----------------- This is the template used when you are in the invite/create new user screen. - ``**script**`` The main script to send the form submissions to. Not optional. - ``**action**`` Replaced with the action parameter. Not optional. The following values are filled in if you make a mistake in your form (email address invalid, user already exists etc). They ensure that the values you typed first time are preserved. The exception is the password, which is randomly generated. None of them are optional. - ``**create1**`` - ``**create2**`` - ``**realname**`` - ``**username**`` - ``**email**`` - ``**adminlev**`` - ``**pass1**`` - ``**pass2**`` -------------------------- TODO ==== This TODO list is the list of features/fixes that I am likely to actually get round to at some point. They aren't in order of priority, but some are obviously more important than others. If any particularly matter to you, or you have suggestions for things that ought to be on the list, then let me know. If you wanted to tackle any yourself I can give you lots of help. Some of the items can already be done, either through the templates or just by writing external functions - but they ought to be built into logintools. * formencode and formdecode functions need a max form size to prevent file uploads being passed around the pages. (and prevent DOS attacks). The max size should preferably be configurable. This max size should (also ?) be checked inside the login function... (can this be put inside the cgi.FieldStorage code ?) * Count and log failed login attempts ? (email account holder after three failed login attempts ?) * Display message after failed login - pass message to the displaylogin function. Also - put last used username back into the login form. * 'Forgotten Your password ?' link. (will email password - so need to keep a handy email list) * Max one login per email address ? * Write (copy !) a privacy policy - regarding cookies and email addresses. * dataenc the password when it is stored in temp.ini (currently plaintext) * Change to use multiple cookies rather than cookiepath. Would like to allow multiple cookies on same path, this means need to have an individual 'name' per site. Less brittle than using cookiepath though. * Need an option to return the cookie object rather than just automatically print the cookie header. Should it be a SimpleCookie object or a Morsel object. * Improve the generic error function (error page should be a template) * Propagate action parameter through the error function - with link back to main script. * In create/invite new user, we ought to be able to set/unset the 'editable' flag. * clear up the '' and None confusion for action. Make '' a valid value. * extend the user sign-up system, by having it fill in additional values from 'default.ini'. WISH LIST ========== This is the part of the TODO list that I will only work on if time permits or if someone really pesters me for it. If you really want anything from this list you'll have to let me know and I'll have to try and move it up the priority list. As above, if you wanted to tackle any yourself I can give you lots of help. * Allow cookie max-age to be set in the login screen. * Plugin mechanism for allowing python functions to generate the static HTML pages, rather than templates. (Allowing the look and feel of all the pages to be dynamically generated). Could do this by putting all the calls to read the template files (using readfile) into a separate set of functions which could be overloaded by the programmer. * Don't store the password - store the hash. (? - Not sure about this one. It is more secure for the user, but it means you can never remind them of their password. Have an optional validation method perhaps - stored password *or* stored hash.) * Allow the adding of extra values into the default user. (And the editing of these from the admin screen - in the create/invite new user menu option....) * Provide a 'mass invite' option - that allows many people to be invited from just their email address. * makeuser.py - for manually making new user files from the command line. * Allow a form of SSI in the templates - making it easier to maintain a standard look and feel in the pages - through individual files. (Every time an HTML template file is loaded, run it through a 'parse HTML' function - includer.py ?) * Unicode - UTF8 encoding for the user config files ? Allow a way of specifying the encoding in the config.ini (This will require unicode support in ConfigObj) * Unicode - optional UTF-8 for the template files. Easy to serve UTF-8 pages, but need a reliable way of detecting encoding. (Make it a config option ?) * login with javascript encryption (The javascript encryption routines are all done, but I wanted to make it optional, which is a bit more tricky) * New login - ability to make additional form fields required parameters. * Allow turning off the feature requiring email address to create a new login ? (automatic login creation - security risk of 'automated login creation'). * A global 'change max-age' option available to admin. This could change the value for all users. * Should we give the password hash in the cookie an expiry date ? (they are already timestamped - so easy to implement). We could avoid updating the timestamp when we send new-cookie. This would give every cookie an automatic maximum life. * An additional return value for the main login.py that tells you what function it has returned from (i.e. what the user has just done - login, new login, already logged in etc). This could also be done by filling in a 'lastaction' value in the userconfig. * Allow the user config file to be a multi-section file ? This allows the programmer to store other user details in the same file, by section. Just use config[''] (the empty section) for all our stuff. (and change all ConfigObj calls to include flatfile=False) * The default password when a new account is created is just an 8 character random string. Would a pronounceable one be better ? * The highest admin level is currently hardwired into admin.py as MAXADMINLEV. This ought to be configurable through 'config.ini'. The same is true for the minimum value for cookie max-age - MINMAXAGE in admin.py. * A separate email message for those who are invited - as opposed to sign up themselves. * In the 'edit user' page of admin.py - the number of users displayed on a page is hardwired (numonpage) - this could be a user configuration. * There is quite a chunk of HTML for the 'account display' hardwired into admin.py. This *could* be in a template.... * Emailed links, when someone creates a new login, are valid for 30 days. This amount could be configurable. * Optionally log everything. * Provide a 'request account' page (with appropriate options in config.ini) - at moment only options are invite or straight email verification. This is like 'create new account', but requires admin approval before the account is confirmed. * Clear up unused user accounts ? (last used time is already saved in user config file, so an external function could do it). * Way of telling who else is logged in (active in last 15 mins ?), and how many people have been online today (and in the last hour ?). * Have the time 'last logged in' stored as well. More interesting than 'last used', when displayed in the 'edit account' screen. ('last used' will always be recent !) * In the docs, a tutorial that takes you through an example of setting up an application that just presents a series of password protected webpages. Then modifying it so that only some of the links are protected. * Example in the docs on 'configuring Login Tools', that programmers can distribute with *their* distributions. This could be based on the section in the Jalopy documentation. This is for programmers who use Login Tools to inform their users how to set it up. * Allow applications to share a userbase without sharing a config file. * Recognise links that have *already* been used to create a new user. Give them a message that says 'Your account has already been confirmed', rather than 'Your user id is forged or out of date' (because it's no longer recognised). ISSUES ====== These 'issues' are things that don't work as they should. Some of them are bugs, some of them have solutions, and some aren't very important but are worth being aware of. * Lowercase/uppercase usernames. On the windoze platform, filenames are case insensitive. This means that you can't have username 'mike' and username 'MIKE' - but in the temporary store for unconfirmed logins you can. This leads to a potential conflict where you have two pending usernames - 'mike' and 'MIKE'. The last one to confirm will fail. * Old pending logins are only cleaned up when the 'savedetails function is run (when a new user creates a login). If no new user login are created, the old ones can hang around for more than the 30 day limit. We could check the date when the login is confirmed ? (but why refuse someone who actually *wants* to confirm a login ? - the purpose of clearing out old ones is to ensure the 'pending file' doesn't grow in an unlimited way). * In newlogin.py - admin email feature and emailing of link uses the sendmailme function. This is *not* cross-platform as it uses the UNIX/Linux sendmail program. The path to sendmail must be put in the cgiutils script. In cgiutils.py there is a cross platform solution that works if the server allows you access to the SMTP server as localhost. Mine doesn't. I could set this up as a config option if anyone wanted. Alternatively do a find and replace, 'sendmailme' to 'mailme' in newlogin.py. Unfortunately this isn't an ideal solution as mailme will throw an exception if the email fails to send. I ought to put each call to the mail function inside a try-except block and make it configurable which one is used. * Using the admin menu you can change the login name of a user. If you are referencing other files/options by that name, you lose the connection. We could resolve this by optionally switching off the ability to change usernames *or* by registering a 'plugin' function to be called when it happens. The same is true of the 'delete user' option from the admin menu. * As an administrator you can only edit users with an admin level *less* than yours. Should this be optional ? This means that admin accounts that are not editable, can *only* be edited by a higher level administrator. This is intentional but possibly inconvenient. * The administrator has no access to pending logins. Could add an additional admin menu option, allowing you to view/delete them. * In admin.py the scriptlocation is needed to send the link to newly invited users. The location is got from ``'http://' + os.environ['HTTP_HOST']``. This hardwires the protocol to be http. Ought to use 'SCRIPT_NAME' I *think*. * concurrency. When writing any large scale web application, concurrency becomes an issue. Having a single file per user helps on this - your user is unlikely to access the same file more than once simultaneously. Concurrency should only really be an issue when your number of users starts to get into the many thousands, you ought to have moved on from CGIs before this anyway. * currently the 'login=logout' option drops the user back to the login screen. This may not always be the desired behaviour. (The 'logout' function, which only returns the cookie header is always available). * unicode support - The easiest way would be to make all the template files UTF-8. This means the right charset meta header must also be present in them. This is because ConfigObj requires an ascii compatible encoding and UTF-8 is the only one that supports full unicode. Alternatively put the ``_charset_`` field in all forms. Changelog ========== 2005/10/11 Version 0.6.1 ----------------------------- Bugfix in newlogin.py 2005/09/21 Version 0.6.0 ----------------------------- Changes to use the Pythonutils 0.2.2 modules. 2004/11/29 Version 0.5.0a ------------------------------ First release into the wild. Packaged along with (and built for) Jalopy_. Works well. License ========= logintools is licensed under the BSD license. This is a very unrestrictive license - but it comes with the usual disclaimer. This is free software - test it, break it, just don't blame me if it eats your data ! (If it does though, let me know and I'll fix it so that it doesn't happen to anyone else {sm;:-)} :: Copyright (c) 2003-2005, Michael Foord All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Michael Foord nor the name of Voidspace may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. You should also be able to find a copy of this license at : `BSD License`_ If you use this program, please help `Sponsor Voidspace`_, to assist with the costs of hosting Voidspace_. Even $1 or $2 is helpful ! .. raw:: html



Certified Open Source



.. _sponsor voidspace: https://www.paypal.com/xclick/business=michael.foord%40tbsmerchants.co.uk&item_name=Voidspace+and+Python+Utils&no_note=1&tax=0¤cy_code=GBP