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  # All rights reserved. 
  6  # 
  7  # Redistribution and use in source and binary forms, with or without 
  8  # modification, are permitted provided that the following conditions are 
  9  # met: 
 10  # 
 11  # 1. Redistributions of source code must retain the above copyright notice, 
 12  # this list of conditions and the following disclaimer. 
 13  # 
 14  # 2. Redistributions in binary form must reproduce the above copyright 
 15  # notice, this list of conditions and the following disclaimer in the 
 16  # documentation and/or other materials provided with the distribution. 
 17  # 
 18  # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 
 19  # IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 
 20  # TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 
 21  # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR 
 22  # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 
 23  # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 
 24  # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
 25  # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 
 26  # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 
 27  # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 
 28  # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
 29   
 30  """HTTP authentication module. 
 31   
 32  """ 
 33   
 34  import logging 
 35  import re 
 36  import base64 
 37  import binascii 
 38   
 39  from ganeti import compat 
 40  from ganeti import http 
 41   
 42  from cStringIO import StringIO 
 43   
 44  # Digest types from RFC2617 
 45  HTTP_BASIC_AUTH = "Basic" 
 46  HTTP_DIGEST_AUTH = "Digest" 
 47   
 48  # Not exactly as described in RFC2616, section 2.2, but good enough 
 49  _NOQUOTE = re.compile(r"^[-_a-z0-9]+$", re.I) 
50 51 52 -def _FormatAuthHeader(scheme, params):
53 """Formats WWW-Authentication header value as per RFC2617, section 1.2 54 55 @type scheme: str 56 @param scheme: Authentication scheme 57 @type params: dict 58 @param params: Additional parameters 59 @rtype: str 60 @return: Formatted header value 61 62 """ 63 buf = StringIO() 64 65 buf.write(scheme) 66 67 for name, value in params.iteritems(): 68 buf.write(" ") 69 buf.write(name) 70 buf.write("=") 71 if _NOQUOTE.match(value): 72 buf.write(value) 73 else: 74 buf.write("\"") 75 # TODO: Better quoting 76 buf.write(value.replace("\"", "\\\"")) 77 buf.write("\"") 78 79 return buf.getvalue()
80
81 82 -class HttpServerRequestAuthentication(object):
83 # Default authentication realm 84 AUTH_REALM = "Unspecified" 85 86 # Schemes for passwords 87 _CLEARTEXT_SCHEME = "{CLEARTEXT}" 88 _HA1_SCHEME = "{HA1}" 89
90 - def GetAuthRealm(self, req):
91 """Returns the authentication realm for a request. 92 93 May be overridden by a subclass, which then can return different realms for 94 different paths. 95 96 @type req: L{http.server._HttpServerRequest} 97 @param req: HTTP request context 98 @rtype: string 99 @return: Authentication realm 100 101 """ 102 # today we don't have per-request filtering, but we might want to 103 # add it in the future 104 # pylint: disable=W0613 105 return self.AUTH_REALM
106
107 - def AuthenticationRequired(self, req):
108 """Determines whether authentication is required for a request. 109 110 To enable authentication, override this function in a subclass and return 111 C{True}. L{AUTH_REALM} must be set. 112 113 @type req: L{http.server._HttpServerRequest} 114 @param req: HTTP request context 115 116 """ 117 # Unused argument, method could be a function 118 # pylint: disable=W0613,R0201 119 return False
120
121 - def PreHandleRequest(self, req):
122 """Called before a request is handled. 123 124 @type req: L{http.server._HttpServerRequest} 125 @param req: HTTP request context 126 127 """ 128 # Authentication not required, and no credentials given? 129 if not (self.AuthenticationRequired(req) or 130 (req.request_headers and 131 http.HTTP_AUTHORIZATION in req.request_headers)): 132 return 133 134 realm = self.GetAuthRealm(req) 135 136 if not realm: 137 raise AssertionError("No authentication realm") 138 139 # Check Authentication 140 if self.Authenticate(req): 141 # User successfully authenticated 142 return 143 144 # Send 401 Unauthorized response 145 params = { 146 "realm": realm, 147 } 148 149 # TODO: Support for Digest authentication (RFC2617, section 3). 150 # TODO: Support for more than one WWW-Authenticate header with the same 151 # response (RFC2617, section 4.6). 152 headers = { 153 http.HTTP_WWW_AUTHENTICATE: _FormatAuthHeader(HTTP_BASIC_AUTH, params), 154 } 155 156 raise http.HttpUnauthorized(headers=headers)
157 158 @staticmethod
159 - def ExtractUserPassword(req):
160 """Extracts a user and a password from the http authorization header. 161 162 @type req: L{http.server._HttpServerRequest} 163 @param req: HTTP request 164 @rtype: (str, str) 165 @return: A tuple containing a user and a password. One or both values 166 might be None if they are not presented 167 """ 168 credentials = req.request_headers.get(http.HTTP_AUTHORIZATION, None) 169 if not credentials: 170 return None, None 171 172 # Extract scheme 173 parts = credentials.strip().split(None, 2) 174 if len(parts) < 1: 175 # Missing scheme 176 return None, None 177 178 # RFC2617, section 1.2: "[...] It uses an extensible, case-insensitive 179 # token to identify the authentication scheme [...]" 180 scheme = parts[0].lower() 181 182 if scheme == HTTP_BASIC_AUTH.lower(): 183 # Do basic authentication 184 if len(parts) < 2: 185 raise http.HttpBadRequest(message=("Basic authentication requires" 186 " credentials")) 187 return HttpServerRequestAuthentication._ExtractBasicUserPassword(parts[1]) 188 189 elif scheme == HTTP_DIGEST_AUTH.lower(): 190 # TODO: Implement digest authentication 191 # RFC2617, section 3.3: "Note that the HTTP server does not actually need 192 # to know the user's cleartext password. As long as H(A1) is available to 193 # the server, the validity of an Authorization header may be verified." 194 pass 195 196 # Unsupported authentication scheme 197 return None, None
198 199 @staticmethod
200 - def _ExtractBasicUserPassword(in_data):
201 """Extracts user and password from the contents of an authorization header. 202 203 @type in_data: str 204 @param in_data: Username and password encoded as Base64 205 @rtype: (str, str) 206 @return: A tuple containing user and password. One or both values might be 207 None if they are not presented 208 209 """ 210 try: 211 creds = base64.b64decode(in_data.encode("ascii")).decode("ascii") 212 except (TypeError, binascii.Error, UnicodeError): 213 logging.exception("Error when decoding Basic authentication credentials") 214 raise http.HttpBadRequest(message=("Invalid basic authorization header")) 215 216 if ":" not in creds: 217 # We have just a username without password 218 return creds, None 219 220 # return (user, password) tuple 221 return creds.split(":", 1)
222
223 - def Authenticate(self, req):
224 """Checks the credentiales. 225 226 This function MUST be overridden by a subclass. 227 228 """ 229 raise NotImplementedError()
230 231 @staticmethod
232 - def ExtractSchemePassword(expected_password):
233 """Extracts a scheme and a password from the expected_password. 234 235 @type expected_password: str 236 @param expected_password: Username and password encoded as Base64 237 @rtype: (str, str) 238 @return: A tuple containing a scheme and a password. Both values will be 239 None when an invalid scheme or password encoded 240 241 """ 242 if expected_password is None: 243 return None, None 244 # Backwards compatibility for old-style passwords without a scheme 245 if not expected_password.startswith("{"): 246 expected_password = (HttpServerRequestAuthentication._CLEARTEXT_SCHEME + 247 expected_password) 248 249 # Check again, just to be sure 250 if not expected_password.startswith("{"): 251 raise AssertionError("Invalid scheme") 252 253 scheme_end_idx = expected_password.find("}", 1) 254 255 # Ensure scheme has a length of at least one character 256 if scheme_end_idx <= 1: 257 logging.warning("Invalid scheme in password") 258 return None, None 259 260 scheme = expected_password[:scheme_end_idx + 1].upper() 261 password = expected_password[scheme_end_idx + 1:] 262 263 return scheme, password
264 265 @staticmethod
266 - def VerifyBasicAuthPassword(username, password, expected, realm):
267 """Checks the password for basic authentication. 268 269 As long as they don't start with an opening brace ("E{lb}"), old passwords 270 are supported. A new scheme uses H(A1) from RFC2617, where H is MD5 and A1 271 consists of the username, the authentication realm and the actual password. 272 273 @type username: string 274 @param username: Username from HTTP headers 275 @type password: string 276 @param password: Password from HTTP headers 277 @type expected: string 278 @param expected: Expected password with optional scheme prefix (e.g. from 279 users file) 280 @type realm: string 281 @param realm: Authentication realm 282 283 """ 284 scheme, expected_password = HttpServerRequestAuthentication \ 285 .ExtractSchemePassword(expected) 286 if scheme is None or password is None: 287 return False 288 289 # Good old plain text password 290 if scheme == HttpServerRequestAuthentication._CLEARTEXT_SCHEME: 291 return password == expected_password 292 293 # H(A1) as described in RFC2617 294 if scheme == HttpServerRequestAuthentication._HA1_SCHEME: 295 if not realm: 296 # There can not be a valid password for this case 297 raise AssertionError("No authentication realm") 298 299 expha1 = compat.md5_hash() 300 expha1.update("%s:%s:%s" % (username, realm, password)) 301 302 return (expected_password.lower() == expha1.hexdigest().lower()) 303 304 logging.warning("Unknown scheme '%s' in password for user '%s'", 305 scheme, username) 306 307 return False
308