1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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
33 from cStringIO import StringIO
34
35
36 HTTP_BASIC_AUTH = "Basic"
37 HTTP_DIGEST_AUTH = "Digest"
38
39
40 _NOQUOTE = re.compile(r"^[-_a-z0-9]+$", re.I)
41
42
71
72
74
75 AUTH_REALM = "Unspecified"
76
77
78 _CLEARTEXT_SCHEME = "{CLEARTEXT}"
79 _HA1_SCHEME = "{HA1}"
80
82 """Returns the authentication realm for a request.
83
84 May be overridden by a subclass, which then can return different realms for
85 different paths.
86
87 @type req: L{http.server._HttpServerRequest}
88 @param req: HTTP request context
89 @rtype: string
90 @return: Authentication realm
91
92 """
93
94
95
96 return self.AUTH_REALM
97
99 """Determines whether authentication is required for a request.
100
101 To enable authentication, override this function in a subclass and return
102 C{True}. L{AUTH_REALM} must be set.
103
104 @type req: L{http.server._HttpServerRequest}
105 @param req: HTTP request context
106
107 """
108
109
110 return False
111
148
150 """Checks 'Authorization' header sent by client.
151
152 @type req: L{http.server._HttpServerRequest}
153 @param req: HTTP request context
154 @rtype: bool
155 @return: Whether user is allowed to execute request
156
157 """
158 credentials = req.request_headers.get(http.HTTP_AUTHORIZATION, None)
159 if not credentials:
160 return False
161
162
163 parts = credentials.strip().split(None, 2)
164 if len(parts) < 1:
165
166 return False
167
168
169
170 scheme = parts[0].lower()
171
172 if scheme == HTTP_BASIC_AUTH.lower():
173
174 if len(parts) < 2:
175 raise http.HttpBadRequest(message=("Basic authentication requires"
176 " credentials"))
177 return self._CheckBasicAuthorization(req, parts[1])
178
179 elif scheme == HTTP_DIGEST_AUTH.lower():
180
181
182
183
184 pass
185
186
187 return False
188
190 """Checks credentials sent for basic authentication.
191
192 @type req: L{http.server._HttpServerRequest}
193 @param req: HTTP request context
194 @type in_data: str
195 @param in_data: Username and password encoded as Base64
196 @rtype: bool
197 @return: Whether user is allowed to execute request
198
199 """
200 try:
201 creds = base64.b64decode(in_data.encode("ascii")).decode("ascii")
202 except (TypeError, binascii.Error, UnicodeError):
203 logging.exception("Error when decoding Basic authentication credentials")
204 return False
205
206 if ":" not in creds:
207 return False
208
209 (user, password) = creds.split(":", 1)
210
211 return self.Authenticate(req, user, password)
212
214 """Checks the password for a user.
215
216 This function MUST be overridden by a subclass.
217
218 """
219 raise NotImplementedError()
220
222 """Checks the password for basic authentication.
223
224 As long as they don't start with an opening brace ("E{lb}"), old passwords
225 are supported. A new scheme uses H(A1) from RFC2617, where H is MD5 and A1
226 consists of the username, the authentication realm and the actual password.
227
228 @type req: L{http.server._HttpServerRequest}
229 @param req: HTTP request context
230 @type username: string
231 @param username: Username from HTTP headers
232 @type password: string
233 @param password: Password from HTTP headers
234 @type expected: string
235 @param expected: Expected password with optional scheme prefix (e.g. from
236 users file)
237
238 """
239
240 if not expected.startswith("{"):
241 expected = self._CLEARTEXT_SCHEME + expected
242
243
244 if not expected.startswith("{"):
245 raise AssertionError("Invalid scheme")
246
247 scheme_end_idx = expected.find("}", 1)
248
249
250 if scheme_end_idx <= 1:
251 logging.warning("Invalid scheme in password for user '%s'", username)
252 return False
253
254 scheme = expected[:scheme_end_idx + 1].upper()
255 expected_password = expected[scheme_end_idx + 1:]
256
257
258 if scheme == self._CLEARTEXT_SCHEME:
259 return password == expected_password
260
261
262 if scheme == self._HA1_SCHEME:
263 realm = self.GetAuthRealm(req)
264 if not realm:
265
266 raise AssertionError("No authentication realm")
267
268 expha1 = compat.md5_hash()
269 expha1.update("%s:%s:%s" % (username, realm, password))
270
271 return (expected_password.lower() == expha1.hexdigest().lower())
272
273 logging.warning("Unknown scheme '%s' in password for user '%s'",
274 scheme, username)
275
276 return False
277
278
280 """Data structure for users from password file.
281
282 """
283 - def __init__(self, name, password, options):
284 self.name = name
285 self.password = password
286 self.options = options
287
288
290 """Parses the contents of a password file.
291
292 Lines in the password file are of the following format::
293
294 <username> <password> [options]
295
296 Fields are separated by whitespace. Username and password are mandatory,
297 options are optional and separated by comma (','). Empty lines and comments
298 ('#') are ignored.
299
300 @type contents: str
301 @param contents: Contents of password file
302 @rtype: dict
303 @return: Dictionary containing L{PasswordFileUser} instances
304
305 """
306 users = {}
307
308 for line in contents.splitlines():
309 line = line.strip()
310
311
312 if not line or line.startswith("#"):
313 continue
314
315 parts = line.split(None, 2)
316 if len(parts) < 2:
317
318 continue
319
320 name = parts[0]
321 password = parts[1]
322
323
324 options = []
325 if len(parts) >= 3:
326 for part in parts[2].split(","):
327 options.append(part.strip())
328
329 users[name] = PasswordFileUser(name, password, options)
330
331 return users
332