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 utils 
 31  from ganeti import http 
 32   
 33  from cStringIO import StringIO 
 34   
 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 = None 77
78 - def GetAuthRealm(self, req):
79 """Returns the authentication realm for a request. 80 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 83 request. 84 85 @type req: L{http.server._HttpServerRequest} 86 @param req: HTTP request context 87 @rtype: str or None 88 @return: Authentication realm 89 90 """ 91 return self.AUTH_REALM
92
93 - def PreHandleRequest(self, req):
94 """Called before a request is handled. 95 96 @type req: L{http.server._HttpServerRequest} 97 @param req: HTTP request context 98 99 """ 100 realm = self.GetAuthRealm(req) 101 102 # Authentication not required, and no credentials given? 103 if realm is None and http.HTTP_AUTHORIZATION not in req.request_headers: 104 return 105 106 if realm is None: # in case we don't require auth but someone 107 # passed the crendentials anyway 108 realm = "Unspecified" 109 110 # Check "Authorization" header 111 if self._CheckAuthorization(req): 112 # User successfully authenticated 113 return 114 115 # Send 401 Unauthorized response 116 params = { 117 "realm": realm, 118 } 119 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). 123 headers = { 124 http.HTTP_WWW_AUTHENTICATE: _FormatAuthHeader(HTTP_BASIC_AUTH, params), 125 } 126 127 raise http.HttpUnauthorized(headers=headers)
128
129 - def _CheckAuthorization(self, req):
130 """Checks 'Authorization' header sent by client. 131 132 @type req: L{http.server._HttpServerRequest} 133 @param req: HTTP request context 134 @rtype: bool 135 @return: Whether user is allowed to execute request 136 137 """ 138 credentials = req.request_headers.get(http.HTTP_AUTHORIZATION, None) 139 if not credentials: 140 return False 141 142 # Extract scheme 143 parts = credentials.strip().split(None, 2) 144 if len(parts) < 1: 145 # Missing scheme 146 return False 147 148 # RFC2617, section 1.2: "[...] It uses an extensible, case-insensitive 149 # token to identify the authentication scheme [...]" 150 scheme = parts[0].lower() 151 152 if scheme == HTTP_BASIC_AUTH.lower(): 153 # Do basic authentication 154 if len(parts) < 2: 155 raise http.HttpBadRequest(message=("Basic authentication requires" 156 " credentials")) 157 return self._CheckBasicAuthorization(req, parts[1]) 158 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." 164 pass 165 166 # Unsupported authentication scheme 167 return False
168
169 - def _CheckBasicAuthorization(self, req, in_data):
170 """Checks credentials sent for basic authentication. 171 172 @type req: L{http.server._HttpServerRequest} 173 @param req: HTTP request context 174 @type in_data: str 175 @param in_data: Username and password encoded as Base64 176 @rtype: bool 177 @return: Whether user is allowed to execute request 178 179 """ 180 try: 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") 184 return False 185 186 if ":" not in creds: 187 return False 188 189 (user, password) = creds.split(":", 1) 190 191 return self.Authenticate(req, user, password)
192
193 - def Authenticate(self, req, user, password):
194 """Checks the password for a user. 195 196 This function MUST be overridden by a subclass. 197 198 """ 199 raise NotImplementedError()
200 201
202 -class PasswordFileUser(object):
203 """Data structure for users from password file. 204 205 """
206 - def __init__(self, name, password, options):
207 self.name = name 208 self.password = password 209 self.options = options
210 211
212 -def ReadPasswordFile(file_name):
213 """Reads a password file. 214 215 Lines in the password file are of the following format:: 216 217 <username> <password> [options] 218 219 Fields are separated by whitespace. Username and password are mandatory, 220 options are optional and separated by comma (','). Empty lines and comments 221 ('#') are ignored. 222 223 @type file_name: str 224 @param file_name: Path to password file 225 @rtype: dict 226 @return: Dictionary containing L{PasswordFileUser} instances 227 228 """ 229 users = {} 230 231 for line in utils.ReadFile(file_name).splitlines(): 232 line = line.strip() 233 234 # Ignore empty lines and comments 235 if not line or line.startswith("#"): 236 continue 237 238 parts = line.split(None, 2) 239 if len(parts) < 2: 240 # Invalid line 241 continue 242 243 name = parts[0] 244 password = parts[1] 245 246 # Extract options 247 options = [] 248 if len(parts) >= 3: 249 for part in parts[2].split(","): 250 options.append(part.strip()) 251 252 users[name] = PasswordFileUser(name, password, options) 253 254 return users
255