Package ganeti :: Package http :: Module auth
[hide private]
[frames] | no frames]

Source Code for Module ganeti.http.auth

  1  # 
  2  # 
  3   
  4  # Copyright (C) 2007, 2008 Google Inc. 
  5  # 
  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. 
 10  # 
 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. 
 15  # 
 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 
 19  # 02110-1301, USA. 
 20   
 21  """HTTP authentication module. 
 22   
 23  """ 
 24   
 25  import logging 
 26  import re 
 27  import base64 
 28  import binascii 
 29   
 30  from ganeti import compat 
 31  from ganeti import http 
 32  from ganeti import utils 
 33   
 34  from cStringIO import StringIO 
 35   
 36  # Digest types from RFC2617 
 37  HTTP_BASIC_AUTH = "Basic" 
 38  HTTP_DIGEST_AUTH = "Digest" 
 39   
 40  # Not exactly as described in RFC2616, section 2.2, but good enough 
 41  _NOQUOTE = re.compile(r"^[-_a-z0-9]+$", re.I) 
 42   
 43   
44 -def _FormatAuthHeader(scheme, params):
45 """Formats WWW-Authentication header value as per RFC2617, section 1.2 46 47 @type scheme: str 48 @param scheme: Authentication scheme 49 @type params: dict 50 @param params: Additional parameters 51 @rtype: str 52 @return: Formatted header value 53 54 """ 55 buf = StringIO() 56 57 buf.write(scheme) 58 59 for name, value in params.iteritems(): 60 buf.write(" ") 61 buf.write(name) 62 buf.write("=") 63 if _NOQUOTE.match(value): 64 buf.write(value) 65 else: 66 buf.write("\"") 67 # TODO: Better quoting 68 buf.write(value.replace("\"", "\\\"")) 69 buf.write("\"") 70 71 return buf.getvalue()
72 73
74 -class HttpServerRequestAuthentication(object):
75 # Default authentication realm 76 AUTH_REALM = "Unspecified" 77 78 # Schemes for passwords 79 _CLEARTEXT_SCHEME = "{CLEARTEXT}" 80 _HA1_SCHEME = "{HA1}" 81
82 - def GetAuthRealm(self, req):
83 """Returns the authentication realm for a request. 84 85 May be overridden by a subclass, which then can return different realms for 86 different paths. 87 88 @type req: L{http.server._HttpServerRequest} 89 @param req: HTTP request context 90 @rtype: string 91 @return: Authentication realm 92 93 """ 94 # today we don't have per-request filtering, but we might want to 95 # add it in the future 96 # pylint: disable=W0613 97 return self.AUTH_REALM
98
99 - def AuthenticationRequired(self, req):
100 """Determines whether authentication is required for a request. 101 102 To enable authentication, override this function in a subclass and return 103 C{True}. L{AUTH_REALM} must be set. 104 105 @type req: L{http.server._HttpServerRequest} 106 @param req: HTTP request context 107 108 """ 109 # Unused argument, method could be a function 110 # pylint: disable=W0613,R0201 111 return False
112
113 - def PreHandleRequest(self, req):
114 """Called before a request is handled. 115 116 @type req: L{http.server._HttpServerRequest} 117 @param req: HTTP request context 118 119 """ 120 # Authentication not required, and no credentials given? 121 if not (self.AuthenticationRequired(req) or 122 (req.request_headers and 123 http.HTTP_AUTHORIZATION in req.request_headers)): 124 return 125 126 realm = self.GetAuthRealm(req) 127 128 if not realm: 129 raise AssertionError("No authentication realm") 130 131 # Check "Authorization" header 132 if self._CheckAuthorization(req): 133 # User successfully authenticated 134 return 135 136 # Send 401 Unauthorized response 137 params = { 138 "realm": realm, 139 } 140 141 # TODO: Support for Digest authentication (RFC2617, section 3). 142 # TODO: Support for more than one WWW-Authenticate header with the same 143 # response (RFC2617, section 4.6). 144 headers = { 145 http.HTTP_WWW_AUTHENTICATE: _FormatAuthHeader(HTTP_BASIC_AUTH, params), 146 } 147 148 raise http.HttpUnauthorized(headers=headers)
149
150 - def _CheckAuthorization(self, req):
151 """Checks 'Authorization' header sent by client. 152 153 @type req: L{http.server._HttpServerRequest} 154 @param req: HTTP request context 155 @rtype: bool 156 @return: Whether user is allowed to execute request 157 158 """ 159 credentials = req.request_headers.get(http.HTTP_AUTHORIZATION, None) 160 if not credentials: 161 return False 162 163 # Extract scheme 164 parts = credentials.strip().split(None, 2) 165 if len(parts) < 1: 166 # Missing scheme 167 return False 168 169 # RFC2617, section 1.2: "[...] It uses an extensible, case-insensitive 170 # token to identify the authentication scheme [...]" 171 scheme = parts[0].lower() 172 173 if scheme == HTTP_BASIC_AUTH.lower(): 174 # Do basic authentication 175 if len(parts) < 2: 176 raise http.HttpBadRequest(message=("Basic authentication requires" 177 " credentials")) 178 return self._CheckBasicAuthorization(req, parts[1]) 179 180 elif scheme == HTTP_DIGEST_AUTH.lower(): 181 # TODO: Implement digest authentication 182 # RFC2617, section 3.3: "Note that the HTTP server does not actually need 183 # to know the user's cleartext password. As long as H(A1) is available to 184 # the server, the validity of an Authorization header may be verified." 185 pass 186 187 # Unsupported authentication scheme 188 return False
189
190 - def _CheckBasicAuthorization(self, req, in_data):
191 """Checks credentials sent for basic authentication. 192 193 @type req: L{http.server._HttpServerRequest} 194 @param req: HTTP request context 195 @type in_data: str 196 @param in_data: Username and password encoded as Base64 197 @rtype: bool 198 @return: Whether user is allowed to execute request 199 200 """ 201 try: 202 creds = base64.b64decode(in_data.encode("ascii")).decode("ascii") 203 except (TypeError, binascii.Error, UnicodeError): 204 logging.exception("Error when decoding Basic authentication credentials") 205 return False 206 207 if ":" not in creds: 208 return False 209 210 (user, password) = creds.split(":", 1) 211 212 return self.Authenticate(req, user, password)
213
214 - def Authenticate(self, req, user, password):
215 """Checks the password for a user. 216 217 This function MUST be overridden by a subclass. 218 219 """ 220 raise NotImplementedError()
221
222 - def VerifyBasicAuthPassword(self, req, username, password, expected):
223 """Checks the password for basic authentication. 224 225 As long as they don't start with an opening brace ("E{lb}"), old passwords 226 are supported. A new scheme uses H(A1) from RFC2617, where H is MD5 and A1 227 consists of the username, the authentication realm and the actual password. 228 229 @type req: L{http.server._HttpServerRequest} 230 @param req: HTTP request context 231 @type username: string 232 @param username: Username from HTTP headers 233 @type password: string 234 @param password: Password from HTTP headers 235 @type expected: string 236 @param expected: Expected password with optional scheme prefix (e.g. from 237 users file) 238 239 """ 240 # Backwards compatibility for old-style passwords without a scheme 241 if not expected.startswith("{"): 242 expected = self._CLEARTEXT_SCHEME + expected 243 244 # Check again, just to be sure 245 if not expected.startswith("{"): 246 raise AssertionError("Invalid scheme") 247 248 scheme_end_idx = expected.find("}", 1) 249 250 # Ensure scheme has a length of at least one character 251 if scheme_end_idx <= 1: 252 logging.warning("Invalid scheme in password for user '%s'", username) 253 return False 254 255 scheme = expected[:scheme_end_idx + 1].upper() 256 expected_password = expected[scheme_end_idx + 1:] 257 258 # Good old plain text password 259 if scheme == self._CLEARTEXT_SCHEME: 260 return password == expected_password 261 262 # H(A1) as described in RFC2617 263 if scheme == self._HA1_SCHEME: 264 realm = self.GetAuthRealm(req) 265 if not realm: 266 # There can not be a valid password for this case 267 raise AssertionError("No authentication realm") 268 269 expha1 = compat.md5_hash() 270 expha1.update("%s:%s:%s" % (username, realm, password)) 271 272 return (expected_password.lower() == expha1.hexdigest().lower()) 273 274 logging.warning("Unknown scheme '%s' in password for user '%s'", 275 scheme, username) 276 277 return False
278 279
280 -class PasswordFileUser(object):
281 """Data structure for users from password file. 282 283 """
284 - def __init__(self, name, password, options):
285 self.name = name 286 self.password = password 287 self.options = options
288 289
290 -def ParsePasswordFile(contents):
291 """Parses the contents of a password file. 292 293 Lines in the password file are of the following format:: 294 295 <username> <password> [options] 296 297 Fields are separated by whitespace. Username and password are mandatory, 298 options are optional and separated by comma (','). Empty lines and comments 299 ('#') are ignored. 300 301 @type contents: str 302 @param contents: Contents of password file 303 @rtype: dict 304 @return: Dictionary containing L{PasswordFileUser} instances 305 306 """ 307 users = {} 308 309 for line in utils.FilterEmptyLinesAndComments(contents): 310 parts = line.split(None, 2) 311 if len(parts) < 2: 312 # Invalid line 313 # TODO: Return line number from FilterEmptyLinesAndComments 314 logging.warning("Ignoring non-comment line with less than two fields") 315 continue 316 317 name = parts[0] 318 password = parts[1] 319 320 # Extract options 321 options = [] 322 if len(parts) >= 3: 323 for part in parts[2].split(","): 324 options.append(part.strip()) 325 else: 326 logging.warning("Ignoring values for user '%s': %s", name, parts[3:]) 327 328 users[name] = PasswordFileUser(name, password, options) 329 330 return users
331