| | 1 | # -*- coding: iso8859-1 -*- |
| | 2 | # |
| | 3 | # Copyright (C) 2003-2005 Edgewall Software |
| | 4 | # Copyright (C) 2003-2005 Jonas Borgstr�jonas@edgewall.com> |
| | 5 | # All rights reserved. |
| | 6 | # |
| | 7 | # This software is licensed as described in the file COPYING, which |
| | 8 | # you should have received as part of this distribution. The terms |
| | 9 | # are also available at http://trac.edgewall.com/license.html. |
| | 10 | # |
| | 11 | # This software consists of voluntary contributions made by many |
| | 12 | # individuals. For the exact contribution history, see the revision |
| | 13 | # history and logs, available at http://projects.edgewall.com/trac/. |
| | 14 | # |
| | 15 | # Author: Jonas Borgstr�jonas@edgewall.com |
| | 16 | # Author: Andrew Deason <adeason@tjhsst.edu> |
| | 17 | |
| | 18 | from __future__ import generators |
| | 19 | import re |
| | 20 | import time |
| | 21 | import urllib2,base64 |
| | 22 | |
| | 23 | from trac.core import * |
| | 24 | from trac.web.api import IAuthenticator, IRequestHandler |
| | 25 | from trac.web.chrome import INavigationContributor |
| | 26 | from trac.util import escape, hex_entropy, TRUE |
| | 27 | |
| | 28 | |
| | 29 | class LoginFormModule(Component): |
| | 30 | """Implements user authentication based on HTTP authentication provided by |
| | 31 | the web-server, combined with cookies for communicating the login |
| | 32 | information across the whole site. This module makes an internal request to |
| | 33 | the webserver using HTTP authentication, instead of using HTTP |
| | 34 | authentication directly from the user, so the user has the ability to |
| | 35 | logout. |
| | 36 | |
| | 37 | This mechanism expects that the web-server is setup so that a request to the |
| | 38 | path '/login' requires authentication (just Basic authentication for now). |
| | 39 | When a user attempts to login, this module attempts to use that information |
| | 40 | to authenticate to the page https://trac.company.com/login, using HTTP |
| | 41 | authentication. If it is successful, a 'trac_auth' cookie is stored in the |
| | 42 | user's browser. This cookie is used to identify the user in subsequent |
| | 43 | requests, until it is destroyed when the user logs out. |
| | 44 | """ |
| | 45 | |
| | 46 | implements(IAuthenticator, INavigationContributor, IRequestHandler) |
| | 47 | |
| | 48 | # IAuthenticator methods |
| | 49 | |
| | 50 | def authenticate(self, req): |
| | 51 | authname = None |
| | 52 | if req.incookie.has_key('trac_auth'): |
| | 53 | authname = self._get_name_for_cookie(req, req.incookie['trac_auth']) |
| | 54 | |
| | 55 | if not authname: |
| | 56 | return None |
| | 57 | |
| | 58 | ignore_case = self.env.config.get('trac', 'ignore_auth_case') |
| | 59 | ignore_case = ignore_case.strip().lower() in TRUE |
| | 60 | if ignore_case: |
| | 61 | authname = authname.lower() |
| | 62 | return authname |
| | 63 | |
| | 64 | # INavigationContributor methods |
| | 65 | |
| | 66 | def get_active_navigation_item(self, req): |
| | 67 | return 'login' |
| | 68 | |
| | 69 | def get_navigation_items(self, req): |
| | 70 | if req.authname and req.authname != 'anonymous': |
| | 71 | yield 'metanav', 'login', 'logged in as %s' % escape(req.authname) |
| | 72 | yield 'metanav', 'logout', '<a href="%s">Logout</a>' \ |
| | 73 | % escape(self.env.href.logout()) |
| | 74 | else: |
| | 75 | yield 'metanav', 'login', '<a href="%s">Login</a>' \ |
| | 76 | % escape(self.env.href.login()) |
| | 77 | |
| | 78 | # IRequestHandler methods |
| | 79 | |
| | 80 | def match_request(self, req): |
| | 81 | return re.match('/(login|logout)/?', req.path_info) |
| | 82 | |
| | 83 | def process_request(self, req): |
| | 84 | if req.path_info.startswith('/login'): |
| | 85 | if not self._do_login(req): |
| | 86 | return 'login.cs', None |
| | 87 | elif req.path_info.startswith('/logout'): |
| | 88 | self._do_logout(req) |
| | 89 | self._redirect_back(req) |
| | 90 | |
| | 91 | # Internal methods |
| | 92 | |
| | 93 | def _do_login(self, req): |
| | 94 | """Log the remote user in. |
| | 95 | |
| | 96 | This function displays a form to the user to log themselves in, and |
| | 97 | verifies the information when the user submits that form. If the |
| | 98 | authentication is successful, the user name is inserted into the |
| | 99 | `auth_cookie` table and a cookie identifying the user on subsequent |
| | 100 | requests is sent back to the client. |
| | 101 | |
| | 102 | If the Authenticator was created with `ignore_case` set to true, then |
| | 103 | the authentication name passed from the web form 'username' variable |
| | 104 | will be converted to lower case before being used. This is to avoid |
| | 105 | problems on installations authenticating against Windows which is not |
| | 106 | case sensitive regarding user names and domain names |
| | 107 | """ |
| | 108 | |
| | 109 | if req.args.get('username'): |
| | 110 | assert req.args.get('password'), 'No password' |
| | 111 | # Test authentication |
| | 112 | |
| | 113 | try: |
| | 114 | self._try_http_auth(req.base_url[:req.base_url.find('/',8)] + '/login',req.args.get('username'),req.args.get('password')) |
| | 115 | except IOError, e: |
| | 116 | # Incorrect password |
| | 117 | req.hdf['title'] = 'Login Failed' |
| | 118 | req.hdf['login.action'] = self.env.href() + '/login' |
| | 119 | req.hdf['login.referer'] = req.args.get('ref') |
| | 120 | req.hdf['login.error'] = 'Invalid username or password' |
| | 121 | return None |
| | 122 | |
| | 123 | # Successful authentication, set cookies and stuff |
| | 124 | remote_user = req.args.get('username') |
| | 125 | ignore_case = self.env.config.get('trac', 'ignore_auth_case') |
| | 126 | ignore_case = ignore_case.strip().lower() in TRUE |
| | 127 | if ignore_case: |
| | 128 | remote_user = remote_user.lower() |
| | 129 | |
| | 130 | assert req.authname in ('anonymous', remote_user), \ |
| | 131 | 'Already logged in as %s.' % req.authname |
| | 132 | |
| | 133 | cookie = hex_entropy() |
| | 134 | db = self.env.get_db_cnx() |
| | 135 | cursor = db.cursor() |
| | 136 | cursor.execute("INSERT INTO auth_cookie (cookie,name,ipnr,time) " |
| | 137 | "VALUES (%s, %s, %s, %s)", (cookie, remote_user, |
| | 138 | req.remote_addr, int(time.time()))) |
| | 139 | db.commit() |
| | 140 | |
| | 141 | req.authname = remote_user |
| | 142 | req.outcookie['trac_auth'] = cookie |
| | 143 | req.outcookie['trac_auth']['path'] = self.env.href() |
| | 144 | req.redirect(req.args.get('ref')) |
| | 145 | else: |
| | 146 | # No authentication information passed, display a form |
| | 147 | req.hdf['title'] = 'Login' |
| | 148 | req.hdf['login.action'] = self.env.href() + '/login' |
| | 149 | req.hdf['login.referer'] = req.get_header('Referer') |
| | 150 | return None |
| | 151 | |
| | 152 | def _do_logout(self, req): |
| | 153 | """Log the user out. |
| | 154 | |
| | 155 | Simply deletes the corresponding record from the auth_cookie table. |
| | 156 | """ |
| | 157 | if req.authname == 'anonymous': |
| | 158 | # Not logged in |
| | 159 | return |
| | 160 | |
| | 161 | # While deleting this cookie we also take the opportunity to delete |
| | 162 | # cookies older than 10 days |
| | 163 | db = self.env.get_db_cnx() |
| | 164 | cursor = db.cursor() |
| | 165 | cursor.execute("DELETE FROM auth_cookie WHERE name=%s OR time < %s", |
| | 166 | (req.authname, int(time.time()) - 86400 * 10)) |
| | 167 | db.commit() |
| | 168 | self._expire_cookie(req) |
| | 169 | req.remote_user = 'anonymous' |
| | 170 | req.remote_pass = '' |
| | 171 | |
| | 172 | def _try_http_auth(self, uri, user, passw): |
| | 173 | authreq = urllib2.Request(uri) |
| | 174 | base64string = base64.encodestring('%s:%s' % (user, passw))[:-1] |
| | 175 | authheader = "Basic %s" % base64string |
| | 176 | authreq.add_header("Authorization", authheader) |
| | 177 | handle = urllib2.urlopen(authreq) |
| | 178 | |
| | 179 | def _expire_cookie(self, req): |
| | 180 | """Instruct the user agent to drop the auth cookie by setting the |
| | 181 | "expires" property to a date in the past. |
| | 182 | """ |
| | 183 | req.outcookie['trac_auth'] = '' |
| | 184 | req.outcookie['trac_auth']['path'] = self.env.href() |
| | 185 | req.outcookie['trac_auth']['expires'] = -10000 |
| | 186 | |
| | 187 | def _get_name_for_cookie(self, req, cookie): |
| | 188 | check_ip = self.env.config.get('trac', 'check_auth_ip') |
| | 189 | check_ip = check_ip.strip().lower() in TRUE |
| | 190 | |
| | 191 | db = self.env.get_db_cnx() |
| | 192 | cursor = db.cursor() |
| | 193 | if check_ip: |
| | 194 | cursor.execute("SELECT name FROM auth_cookie " |
| | 195 | "WHERE cookie=%s AND ipnr=%s", |
| | 196 | (cookie.value, req.remote_addr)) |
| | 197 | else: |
| | 198 | cursor.execute("SELECT name FROM auth_cookie WHERE cookie=%s", |
| | 199 | (cookie.value,)) |
| | 200 | row = cursor.fetchone() |
| | 201 | if not row: |
| | 202 | # The cookie is invalid (or has been purged from the database), so |
| | 203 | # tell the user agent to drop it as it is invalid |
| | 204 | self._expire_cookie(req) |
| | 205 | return None |
| | 206 | |
| | 207 | return row[0] |
| | 208 | |
| | 209 | def _redirect_back(self, req): |
| | 210 | """Redirect the user back to the URL she came from.""" |
| | 211 | referer = req.get_header('Referer') |
| | 212 | if referer and not referer.startswith(req.base_url): |
| | 213 | # only redirect to referer if the latter is from the same |
| | 214 | # instance |
| | 215 | referer = None |
| | 216 | req.redirect(referer or self.env.abs_href()) |