root/oss/httpauthfilter/httpauthfilter.py

Revision 244, 8.7 kB (checked in by sylvain, 2 years ago)

Added user_name attribute. Updated copyright owner and dates

Line 
1 # -*- coding: utf-8 -*-
2
3 __authors__ = ["Sylvain Hellegouarch (sh@defuze.org)"]
4 __contributors__ = ['Tiago Cogumbreiro <cogumbreiro@users.sf.net>', 'Peter Russell']
5 __date__ = "2007/01/26"
6 __copyright__ = """
7 Copyright (c) 2005, 2006, 2007, Sylvain Hellegouarch
8 """
9
10 __license__ = """
11 All rights reserved.
12
13 Redistribution and use in source and binary forms, with or without modification,
14 are permitted provided that the following conditions are met:
15
16     * Redistributions of source code must retain the above copyright notice,
17       this list of conditions and the following disclaimer.
18     * Redistributions in binary form must reproduce the above copyright notice,
19       this list of conditions and the following disclaimer in the documentation
20       and/or other materials provided with the distribution.
21     * Neither the name of Sylvain Hellegouarch nor the names of his contributors
22       may be used to endorse or promote products derived from this software
23       without specific prior written permission.
24
25 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
26 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
27 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
28 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
29 FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
30 DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
31 SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
32 CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
33 OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
34 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
35 """
36
37 """
38 26/01/2007 - Added user_name attribute to request object (thanks Peter)
39 16/12/2006 - Added Peter Russell patch to support pre-hashed passwords
40              so that now you can store hashed values instead of clear text
41            - Should have also fixed the staticfilter case
42 10/04/2006 - Refactored the filter
43 30/12/2005 - Fixed to match lower cases attributes used by CherryPy
44 07/12/2005 - Unit test added
45              Also added support for the max replay feature
46 06/12/2005 - Updated to match CherryPy 2.2.0-beta revision 862 (Sylvain)
47 """
48
49 # This version has been test against CherryPy 2.2.2 RC2
50
51 #
52 # This is a simple filter for Digest and Basic Authorization schemes
53 # as described in RFC 2617
54 # http://www.ietf.org/rfc/rfc2617.txt
55 #
56
57 import time
58 import os.path
59 from stat import ST_MTIME
60 from os import stat
61
62 import cherrypy
63 from cherrypy.filters.basefilter import BaseFilter
64 import httpauth
65
66 class HttpAuthFilter(BaseFilter):
67     """Class that must be called in your filters list.
68
69     realm - see RFC 2617
70     privateKey - seed for the nonce field of the DIGEST algorithm
71     unauthorizedPath - a CherryPy exposed callable when we need to send the unauthorized message
72     maxReplay - allows you to set how many times the same Digest authentification value can be used from a given user agent
73         defaults to 0 which dismiss the replay checker
74     retrieveUsersFunc - a function that returns a dictionnary of
75         user:password if storRealmsWithPasswords is False, or
76         user:{realm:password} if not.  If storeHashedCredentials is
77         True, then password will be the hash of username:realm:password.
78         handy if you want to retrieve your users from a database for instance
79     cppasswordPath - path of the file maintaining the user:password tuples.
80         you can leave it to None when using retrieveUsersFunc
81     """
82     def __init__(self, realm, privateKey, unauthorizedPath,
83                  maxReplay=0, retrieveUsersFunc=None,
84                  cppasswordPath=None, storeRealmsWithPasswords=False,
85                  storeHashedCredentials=False):
86         self.cppasswordPath = cppasswordPath
87         self.cppasswordLastModified = None
88         self.users = {}
89         self.realm = realm
90         self.privateKey = privateKey
91         self.maxReplay = maxReplay
92         self.unauthorizedPath = unauthorizedPath
93         self.retrieveUsersFunc = retrieveUsersFunc
94         if retrieveUsersFunc is None:
95             self.retrieveUsersFunc = self._loadAuthorizedUsers
96         self.storeHashedCredentials = storeHashedCredentials
97         if self.storeHashedCredentials == True:
98             self.storeRealmsWithPasswords = True
99         else:
100             self.storeRealmsWithPasswords = storeRealmsWithPasswords
101
102     def _loadAuthorizedUsers(self):
103         # We don't to reload the file for each request. Let's reload it only when it is modified
104         if not self.cppasswordLastModified or self.cppasswordLastModified != stat(self.cppasswordPath)[ST_MTIME]:
105             self.cppasswordLastModified = stat(self.cppasswordPath)[ST_MTIME]
106             cppassword = file(self.cppasswordPath, 'rb')
107             for line in cppassword:
108                 if self.storeRealmsWithPasswords:
109                     username, realm, password = line.split(':')
110                     if username in self.users:
111                         self.users[username][realm] = password
112                     else:
113                         self.users[username] = {realm:password.rstrip()}
114                 else:
115                     username, password = line.split(':')
116                     self.users[username] = password.rstrip()
117             cppassword.close()
118         return self.users
119
120     def on_start_resource(self):
121         # Make sure it will not be authorized whatever happens in between
122         cherrypy.request.isAuthorized = False
123         cherrypy.request.execute_main = False
124
125     def before_request_body(self):
126         # Check if we have an authorization request header
127         if cherrypy.request.headerMap.has_key('Authorization'):
128             # Parse the header
129             ah = httpauth.parseAuthorization(cherrypy.request.headerMap['Authorization'])
130             # someone might try to trick us with a request we can't understand
131             # let's ditch the request
132             if ah is None:
133                 raise cherrypy.HTTPError(400, 'Bad Request')
134             else:
135                 # If we are dealing with hashed credentials, we need
136                 # the realm to be in the auth_map for basic authentication.
137                 if self.storeHashedCredentials and not 'realm' in ah:
138                     ah['realm'] = self.realm
139
140                 # the nc token of the Digest authentification
141                 # allows a server to check for attack replay by setting a
142                 # limit to the nc value
143                 # If yo use the value to low, you'll force your users to
144                 # retype their credentials every self.maxReplay
145                 # setting a higher value could decrease the relevancy of this feature
146                 # read http://www.apps.ietf.org/rfc/rfc2617.html#sec-4.5
147                 # this is not really powerful though because it'll only work
148                 # on user agents telling the truth on that value...
149                 if self.maxReplay > 0:
150                     nc = ah.get('nc', None)
151                     if nc is not None:
152                         if int(nc) > self.maxReplay:
153                             # we force the user agent to ask the client his credential again
154                             return
155                    
156                 # Is the user authorized?
157                 password = self.retrieveUsersFunc().get(ah["username"], None)
158                 if self.storeRealmsWithPasswords and password:
159                     password = password.get(self.realm, None)
160                 method = cherrypy.request.method.upper()
161                 if httpauth.checkResponse(ah, password,
162                                           method=method,
163                                           password_is_hashed=self.storeHashedCredentials):
164                     # yeah the user is authorized
165                     cherrypy.request.isAuthorized = True
166                     cherrypy.request.user_name = ah["username"]
167                     if method not in ["POST", "PUT"]:
168                         cherrypy.request.processRequestBody = False
169
170     def before_main(self):
171         # this should make this filter to work fine with static content as well
172         if cherrypy.config.get('static_filter.on', False) is False:
173             cherrypy.request.execute_main = True
174         if not cherrypy.request.isAuthorized:
175             cherrypy.response.status = '401 Unauthorized'
176             cherrypy.request.object_path = self.unauthorizedPath
177
178     def on_end_resource(self):
179         if not cherrypy.request.isAuthorized:
180             cherrypy.response.header_list.append(('WWW-Authenticate', httpauth.digestAuth(self.realm)))
181             cherrypy.response.header_list.append(('WWW-Authenticate', httpauth.basicAuth(self.realm)))
182            
Note: See TracBrowser for help on using the browser.