4 # Copyright (C) 2007, 2008 Google Inc.
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 # General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
21 """HTTP authentication module.
31 from ganeti import constants
32 from ganeti import utils
33 from ganeti import http
35 from cStringIO import StringIO
38 # Digest types from RFC2617
39 HTTP_BASIC_AUTH = "Basic"
40 HTTP_DIGEST_AUTH = "Digest"
42 # Not exactly as described in RFC2616, section 2.2, but good enough
43 _NOQUOTE = re.compile(r"^[-_a-z0-9]+$", re.I)
46 def _FormatAuthHeader(scheme, params):
47 """Formats WWW-Authentication header value as per RFC2617, section 1.2
50 @param scheme: Authentication scheme
52 @param params: Additional parameters
54 @return: Formatted header value
61 for name, value in params.iteritems():
65 if _NOQUOTE.match(value):
69 # TODO: Better quoting
70 buf.write(value.replace("\"", "\\\""))
76 class HttpServerRequestAuthentication(object):
77 # Default authentication realm
80 def GetAuthRealm(self, req):
81 """Returns the authentication realm for a request.
83 MAY be overriden by a subclass, which then can return different realms for
84 different paths. Returning "None" means no authentication is needed for a
87 @type req: L{http.server._HttpServerRequest}
88 @param req: HTTP request context
90 @return: Authentication realm
93 return self.AUTH_REALM
95 def PreHandleRequest(self, req):
96 """Called before a request is handled.
98 @type req: L{http.server._HttpServerRequest}
99 @param req: HTTP request context
102 realm = self.GetAuthRealm(req)
104 # Authentication required?
108 # Check "Authorization" header
109 if self._CheckAuthorization(req):
110 # User successfully authenticated
113 # Send 401 Unauthorized response
118 # TODO: Support for Digest authentication (RFC2617, section 3).
119 # TODO: Support for more than one WWW-Authenticate header with the same
120 # response (RFC2617, section 4.6).
122 http.HTTP_WWW_AUTHENTICATE: _FormatAuthHeader(HTTP_BASIC_AUTH, params),
125 raise http.HttpUnauthorized(headers=headers)
127 def _CheckAuthorization(self, req):
128 """Checks 'Authorization' header sent by client.
130 @type req: L{http.server._HttpServerRequest}
131 @param req: HTTP request context
133 @return: Whether user is allowed to execute request
136 credentials = req.request_headers.get(http.HTTP_AUTHORIZATION, None)
141 parts = credentials.strip().split(None, 2)
146 # RFC2617, section 1.2: "[...] It uses an extensible, case-insensitive
147 # token to identify the authentication scheme [...]"
148 scheme = parts[0].lower()
150 if scheme == HTTP_BASIC_AUTH.lower():
151 # Do basic authentication
153 raise http.HttpBadRequest(message=("Basic authentication requires"
155 return self._CheckBasicAuthorization(req, parts[1])
157 elif scheme == HTTP_DIGEST_AUTH.lower():
158 # TODO: Implement digest authentication
159 # RFC2617, section 3.3: "Note that the HTTP server does not actually need
160 # to know the user's cleartext password. As long as H(A1) is available to
161 # the server, the validity of an Authorization header may be verified."
164 # Unsupported authentication scheme
167 def _CheckBasicAuthorization(self, req, in_data):
168 """Checks credentials sent for basic authentication.
170 @type req: L{http.server._HttpServerRequest}
171 @param req: HTTP request context
173 @param in_data: Username and password encoded as Base64
175 @return: Whether user is allowed to execute request
179 creds = base64.b64decode(in_data.encode('ascii')).decode('ascii')
180 except (TypeError, binascii.Error, UnicodeError):
181 logging.exception("Error when decoding Basic authentication credentials")
187 (user, password) = creds.split(":", 1)
189 return self.Authenticate(req, user, password)
191 def Authenticate(self, req, user, password):
192 """Checks the password for a user.
194 This function MUST be overriden by a subclass.
197 raise NotImplementedError()
200 class PasswordFileUser(object):
201 """Data structure for users from password file.
204 def __init__(self, name, password, options):
206 self.password = password
207 self.options = options
210 def ReadPasswordFile(file_name):
211 """Reads a password file.
213 Lines in the password file are of the following format::
215 <username> <password> [options]
217 Fields are separated by whitespace. Username and password are mandatory,
218 options are optional and separated by comma (','). Empty lines and comments
222 @param file_name: Path to password file
224 @return: Dictionary containing L{PasswordFileUser} instances
229 for line in utils.ReadFile(file_name).splitlines():
232 # Ignore empty lines and comments
233 if not line or line.startswith("#"):
236 parts = line.split(None, 2)
247 for part in parts[2].split(","):
248 options.append(part.strip())
250 users[name] = PasswordFileUser(name, password, options)