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.
30 from ganeti import utils
31 from ganeti import http
33 from cStringIO import StringIO
36 # Digest types from RFC2617
37 HTTP_BASIC_AUTH = "Basic"
38 HTTP_DIGEST_AUTH = "Digest"
40 # Not exactly as described in RFC2616, section 2.2, but good enough
41 _NOQUOTE = re.compile(r"^[-_a-z0-9]+$", re.I)
44 def _FormatAuthHeader(scheme, params):
45 """Formats WWW-Authentication header value as per RFC2617, section 1.2
48 @param scheme: Authentication scheme
50 @param params: Additional parameters
52 @return: Formatted header value
59 for name, value in params.iteritems():
63 if _NOQUOTE.match(value):
67 # TODO: Better quoting
68 buf.write(value.replace("\"", "\\\""))
74 class HttpServerRequestAuthentication(object):
75 # Default authentication realm
78 def GetAuthRealm(self, req):
79 """Returns the authentication realm for a request.
81 MAY be overridden by a subclass, which then can return different realms for
82 different paths. Returning "None" means no authentication is needed for a
85 @type req: L{http.server._HttpServerRequest}
86 @param req: HTTP request context
88 @return: Authentication realm
91 return self.AUTH_REALM
93 def PreHandleRequest(self, req):
94 """Called before a request is handled.
96 @type req: L{http.server._HttpServerRequest}
97 @param req: HTTP request context
100 realm = self.GetAuthRealm(req)
102 # Authentication not required, and no credentials given?
103 if realm is None and http.HTTP_AUTHORIZATION not in req.request_headers:
106 if realm is None: # in case we don't require auth but someone
107 # passed the crendentials anyway
108 realm = "Unspecified"
110 # Check "Authorization" header
111 if self._CheckAuthorization(req):
112 # User successfully authenticated
115 # Send 401 Unauthorized response
120 # TODO: Support for Digest authentication (RFC2617, section 3).
121 # TODO: Support for more than one WWW-Authenticate header with the same
122 # response (RFC2617, section 4.6).
124 http.HTTP_WWW_AUTHENTICATE: _FormatAuthHeader(HTTP_BASIC_AUTH, params),
127 raise http.HttpUnauthorized(headers=headers)
129 def _CheckAuthorization(self, req):
130 """Checks 'Authorization' header sent by client.
132 @type req: L{http.server._HttpServerRequest}
133 @param req: HTTP request context
135 @return: Whether user is allowed to execute request
138 credentials = req.request_headers.get(http.HTTP_AUTHORIZATION, None)
143 parts = credentials.strip().split(None, 2)
148 # RFC2617, section 1.2: "[...] It uses an extensible, case-insensitive
149 # token to identify the authentication scheme [...]"
150 scheme = parts[0].lower()
152 if scheme == HTTP_BASIC_AUTH.lower():
153 # Do basic authentication
155 raise http.HttpBadRequest(message=("Basic authentication requires"
157 return self._CheckBasicAuthorization(req, parts[1])
159 elif scheme == HTTP_DIGEST_AUTH.lower():
160 # TODO: Implement digest authentication
161 # RFC2617, section 3.3: "Note that the HTTP server does not actually need
162 # to know the user's cleartext password. As long as H(A1) is available to
163 # the server, the validity of an Authorization header may be verified."
166 # Unsupported authentication scheme
169 def _CheckBasicAuthorization(self, req, in_data):
170 """Checks credentials sent for basic authentication.
172 @type req: L{http.server._HttpServerRequest}
173 @param req: HTTP request context
175 @param in_data: Username and password encoded as Base64
177 @return: Whether user is allowed to execute request
181 creds = base64.b64decode(in_data.encode('ascii')).decode('ascii')
182 except (TypeError, binascii.Error, UnicodeError):
183 logging.exception("Error when decoding Basic authentication credentials")
189 (user, password) = creds.split(":", 1)
191 return self.Authenticate(req, user, password)
193 def Authenticate(self, req, user, password):
194 """Checks the password for a user.
196 This function MUST be overridden by a subclass.
199 raise NotImplementedError()
202 class PasswordFileUser(object):
203 """Data structure for users from password file.
206 def __init__(self, name, password, options):
208 self.password = password
209 self.options = options
212 def ReadPasswordFile(file_name):
213 """Reads a password file.
215 Lines in the password file are of the following format::
217 <username> <password> [options]
219 Fields are separated by whitespace. Username and password are mandatory,
220 options are optional and separated by comma (','). Empty lines and comments
224 @param file_name: Path to password file
226 @return: Dictionary containing L{PasswordFileUser} instances
231 for line in utils.ReadFile(file_name).splitlines():
234 # Ignore empty lines and comments
235 if not line or line.startswith("#"):
238 parts = line.split(None, 2)
249 for part in parts[2].split(","):
250 options.append(part.strip())
252 users[name] = PasswordFileUser(name, password, options)