1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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 from ganeti import utils
42
43 from cStringIO import StringIO
44
45
46 HTTP_BASIC_AUTH = "Basic"
47 HTTP_DIGEST_AUTH = "Digest"
48
49
50 _NOQUOTE = re.compile(r"^[-_a-z0-9]+$", re.I)
51
52
81
82
84
85 AUTH_REALM = "Unspecified"
86
87
88 _CLEARTEXT_SCHEME = "{CLEARTEXT}"
89 _HA1_SCHEME = "{HA1}"
90
92 """Returns the authentication realm for a request.
93
94 May be overridden by a subclass, which then can return different realms for
95 different paths.
96
97 @type req: L{http.server._HttpServerRequest}
98 @param req: HTTP request context
99 @rtype: string
100 @return: Authentication realm
101
102 """
103
104
105
106 return self.AUTH_REALM
107
109 """Determines whether authentication is required for a request.
110
111 To enable authentication, override this function in a subclass and return
112 C{True}. L{AUTH_REALM} must be set.
113
114 @type req: L{http.server._HttpServerRequest}
115 @param req: HTTP request context
116
117 """
118
119
120 return False
121
158
160 """Checks 'Authorization' header sent by client.
161
162 @type req: L{http.server._HttpServerRequest}
163 @param req: HTTP request context
164 @rtype: bool
165 @return: Whether user is allowed to execute request
166
167 """
168 credentials = req.request_headers.get(http.HTTP_AUTHORIZATION, None)
169 if not credentials:
170 return False
171
172
173 parts = credentials.strip().split(None, 2)
174 if len(parts) < 1:
175
176 return False
177
178
179
180 scheme = parts[0].lower()
181
182 if scheme == HTTP_BASIC_AUTH.lower():
183
184 if len(parts) < 2:
185 raise http.HttpBadRequest(message=("Basic authentication requires"
186 " credentials"))
187 return self._CheckBasicAuthorization(req, parts[1])
188
189 elif scheme == HTTP_DIGEST_AUTH.lower():
190
191
192
193
194 pass
195
196
197 return False
198
200 """Checks credentials sent for basic authentication.
201
202 @type req: L{http.server._HttpServerRequest}
203 @param req: HTTP request context
204 @type in_data: str
205 @param in_data: Username and password encoded as Base64
206 @rtype: bool
207 @return: Whether user is allowed to execute request
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 return False
215
216 if ":" not in creds:
217 return False
218
219 (user, password) = creds.split(":", 1)
220
221 return self.Authenticate(req, user, password)
222
224 """Checks the password for a user.
225
226 This function MUST be overridden by a subclass.
227
228 """
229 raise NotImplementedError()
230
232 """Checks the password for basic authentication.
233
234 As long as they don't start with an opening brace ("E{lb}"), old passwords
235 are supported. A new scheme uses H(A1) from RFC2617, where H is MD5 and A1
236 consists of the username, the authentication realm and the actual password.
237
238 @type req: L{http.server._HttpServerRequest}
239 @param req: HTTP request context
240 @type username: string
241 @param username: Username from HTTP headers
242 @type password: string
243 @param password: Password from HTTP headers
244 @type expected: string
245 @param expected: Expected password with optional scheme prefix (e.g. from
246 users file)
247
248 """
249
250 if not expected.startswith("{"):
251 expected = self._CLEARTEXT_SCHEME + expected
252
253
254 if not expected.startswith("{"):
255 raise AssertionError("Invalid scheme")
256
257 scheme_end_idx = expected.find("}", 1)
258
259
260 if scheme_end_idx <= 1:
261 logging.warning("Invalid scheme in password for user '%s'", username)
262 return False
263
264 scheme = expected[:scheme_end_idx + 1].upper()
265 expected_password = expected[scheme_end_idx + 1:]
266
267
268 if scheme == self._CLEARTEXT_SCHEME:
269 return password == expected_password
270
271
272 if scheme == self._HA1_SCHEME:
273 realm = self.GetAuthRealm(req)
274 if not realm:
275
276 raise AssertionError("No authentication realm")
277
278 expha1 = compat.md5_hash()
279 expha1.update("%s:%s:%s" % (username, realm, password))
280
281 return (expected_password.lower() == expha1.hexdigest().lower())
282
283 logging.warning("Unknown scheme '%s' in password for user '%s'",
284 scheme, username)
285
286 return False
287
288
290 """Data structure for users from password file.
291
292 """
293 - def __init__(self, name, password, options):
297
298
300 """Parses the contents of a password file.
301
302 Lines in the password file are of the following format::
303
304 <username> <password> [options]
305
306 Fields are separated by whitespace. Username and password are mandatory,
307 options are optional and separated by comma (','). Empty lines and comments
308 ('#') are ignored.
309
310 @type contents: str
311 @param contents: Contents of password file
312 @rtype: dict
313 @return: Dictionary containing L{PasswordFileUser} instances
314
315 """
316 users = {}
317
318 for line in utils.FilterEmptyLinesAndComments(contents):
319 parts = line.split(None, 2)
320 if len(parts) < 2:
321
322
323 logging.warning("Ignoring non-comment line with less than two fields")
324 continue
325
326 name = parts[0]
327 password = parts[1]
328
329
330 options = []
331 if len(parts) >= 3:
332 for part in parts[2].split(","):
333 options.append(part.strip())
334 else:
335 logging.warning("Ignoring values for user '%s': %s", name, parts[3:])
336
337 users[name] = PasswordFileUser(name, password, options)
338
339 return users
340