Edgewall Software

Ticket #1522: auth.py

File auth.py, 8.0 KB (added by markus, 6 years ago)
Line 
1# -*- coding: iso8859-1 -*-
2#
3# Copyright (C) 2003-2005 Edgewall Software
4# Copyright (C) 2003-2005 Jonas Borgström <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öm <jonas@edgewall.com>
16
17from __future__ import generators
18import re
19import time
20
21from trac.core import *
22from trac.web.api import IAuthenticator, IRequestHandler
23from trac.web.chrome import INavigationContributor
24from trac.util import escape, hex_entropy, TRUE
25
26
27class LoginModule(Component):
28    """Implements user authentication based on HTTP authentication provided by
29    the web-server, combined with cookies for communicating the login
30    information across the whole site.
31
32    This plugin traps any /login path request and responds with a 401 basic
33    authentication header meaning that the client's browser has to provide
34    user credentials. The web-server should be set up in a way that these
35    credentials get validated (e.g. against a NT domain) before setting the
36    remote_user server variable.
37    The login name is then stored in the database and associated with a unique
38    key that gets passed back to the user agent using the 'trac_auth' cookie.
39    This cookie is used to identify the user in subsequent requests to
40    non-protected resources.
41    Please note: trac_auth cookies are not supported on IIS5, see
42    http://support.microsoft.com/?scid=kb%3Ben-us%3B176113&x=15&y=11. In this
43    case IIS5 must provide a valid remote_user with every request.
44    """
45
46    implements(IAuthenticator, INavigationContributor, IRequestHandler)
47
48    # IAuthenticator methods
49
50    def authenticate(self, req):
51        authname = None
52
53        if req.remote_user:
54            authname = req.remote_user
55        elif req.incookie.has_key('trac_auth'):
56            authname = self._get_name_for_cookie(req, req.incookie['trac_auth'])
57
58        if not authname:
59            return None
60
61        ignore_case = self.env.config.get('trac', 'ignore_auth_case')
62        ignore_case = ignore_case.strip().lower() in TRUE
63        if ignore_case:
64            authname = authname.lower()
65        return authname
66
67    # INavigationContributor methods
68
69    def get_active_navigation_item(self, req):
70        return 'login'
71
72    def get_navigation_items(self, req):
73        if req.authname and req.authname != 'anonymous':
74            yield 'metanav', 'login', 'logged in as %s' % escape(req.authname)
75            yield 'metanav', 'logout', '<a href="%s">Logout</a>' \
76                  % escape(self.env.href.logout())
77        else:
78            yield 'metanav', 'login', '<a href="%s">Login</a>' \
79                  % escape(self.env.href.login())
80
81    # IRequestHandler methods
82
83    def match_request(self, req):
84        return re.match('/(login|logout)/?', req.path_info)
85
86    def process_request(self, req):
87        if req.path_info.startswith('/login'):
88            if not req.remote_user or req.remote_user == 'anonymous':
89                req.send_response(401)
90                req.send_header('WWW-Authenticate', 'Basic')
91                req.end_headers()
92                return
93            else:
94                self._do_login(req)
95        elif req.path_info.startswith('/logout'):
96            self._do_logout(req)
97        self._redirect_back(req)
98
99    # Internal methods
100
101    def _do_login(self, req):
102        """Log the remote user in.
103
104        This function expects to be called when the remote user name is
105        available. The user name is inserted into the `auth_cookie` table and a
106        cookie identifying the user on subsequent requests is sent back to the
107        client.
108
109        If the Authenticator was created with `ignore_case` set to true, then
110        the authentication name passed from the web server in req.remote_user
111        will be converted to lower case before being used. This is to avoid
112        problems on installations authenticating against Windows which is not
113        case sensitive regarding user names and domain names
114        """
115        assert req.remote_user, 'Authentication information not available.'
116
117        remote_user = req.remote_user
118        ignore_case = self.env.config.get('trac', 'ignore_auth_case')
119        ignore_case = ignore_case.strip().lower() in TRUE
120        if ignore_case:
121            remote_user = remote_user.lower()
122
123        assert req.authname in ('anonymous', remote_user), \
124               'Already logged in as %s.' % req.authname
125
126        cookie = hex_entropy()
127        db = self.env.get_db_cnx()
128        cursor = db.cursor()
129        cursor.execute("INSERT INTO auth_cookie (cookie,name,ipnr,time) "
130                       "VALUES (%s, %s, %s, %s)", (cookie, remote_user,
131                       req.remote_addr, int(time.time())))
132        db.commit()
133
134        req.authname = remote_user
135        req.outcookie['trac_auth'] = cookie
136        req.outcookie['trac_auth']['path'] = self.env.href()
137
138    def _do_logout(self, req):
139        """Log the user out.
140
141        Simply deletes the corresponding record from the auth_cookie table.
142        """
143        if req.authname == 'anonymous':
144            # Not logged in
145            return
146
147        # While deleting this cookie we also take the opportunity to delete
148        # cookies older than 10 days
149        db = self.env.get_db_cnx()
150        cursor = db.cursor()
151        cursor.execute("DELETE FROM auth_cookie WHERE name=%s OR time < %s",
152                       (req.authname, int(time.time()) - 86400 * 10))
153        db.commit()
154        self._expire_cookie(req)
155
156    def _expire_cookie(self, req):
157        """Instruct the user agent to drop the auth cookie by setting the
158        "expires" property to a date in the past.
159        """
160        req.outcookie['trac_auth'] = ''
161        req.outcookie['trac_auth']['path'] = self.env.href()
162        req.outcookie['trac_auth']['expires'] = -10000
163
164    def _get_name_for_cookie(self, req, cookie):
165        check_ip = self.env.config.get('trac', 'check_auth_ip')
166        check_ip = check_ip.strip().lower() in TRUE
167
168        db = self.env.get_db_cnx()
169        cursor = db.cursor()
170        if check_ip:
171            cursor.execute("SELECT name FROM auth_cookie "
172                           "WHERE cookie=%s AND ipnr=%s",
173                           (cookie.value, req.remote_addr))
174        else:
175            cursor.execute("SELECT name FROM auth_cookie WHERE cookie=%s",
176                           (cookie.value,))
177        row = cursor.fetchone()
178        if not row:
179            # The cookie is invalid (or has been purged from the database), so
180            # tell the user agent to drop it as it is invalid
181            self._expire_cookie(req)
182            return None
183
184        return row[0]
185
186    def _redirect_back(self, req):
187        """Redirect the user back to the URL she came from."""
188        referer = req.get_header('Referer')
189
190        # Since we cannot create a trac_auth cookie on IIS5, we depend
191        # on the remote_user information with every request.
192        # But the browser won't send authentication information when we
193        # redirect below the trac.cgi/ path.
194        if not self.env.abs_href.endswith('/'):
195            abs_href = self.env.abs_href + '/'
196        else:
197            abs_href = self.env.abs_href
198
199        if referer:
200            if not referer.startswith(req.base_url):
201                # only redirect to referer if the latter is from the same
202                # instance
203                referer = None
204            else:
205                if not referer.endswith('/'):
206                    referer = referer + '/'
207        req.redirect(referer or self.env.abs_href)