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 from ganeti import utils
33
34 from cStringIO import StringIO
35
36
37 HTTP_BASIC_AUTH = "Basic"
38 HTTP_DIGEST_AUTH = "Digest"
39
40
41 _NOQUOTE = re.compile(r"^[-_a-z0-9]+$", re.I)
42
43
72
73
75
76 AUTH_REALM = "Unspecified"
77
78
79 _CLEARTEXT_SCHEME = "{CLEARTEXT}"
80 _HA1_SCHEME = "{HA1}"
81
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
95
96
97 return self.AUTH_REALM
98
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
110
111 return False
112
149
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
164 parts = credentials.strip().split(None, 2)
165 if len(parts) < 1:
166
167 return False
168
169
170
171 scheme = parts[0].lower()
172
173 if scheme == HTTP_BASIC_AUTH.lower():
174
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
182
183
184
185 pass
186
187
188 return False
189
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
215 """Checks the password for a user.
216
217 This function MUST be overridden by a subclass.
218
219 """
220 raise NotImplementedError()
221
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
241 if not expected.startswith("{"):
242 expected = self._CLEARTEXT_SCHEME + expected
243
244
245 if not expected.startswith("{"):
246 raise AssertionError("Invalid scheme")
247
248 scheme_end_idx = expected.find("}", 1)
249
250
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
259 if scheme == self._CLEARTEXT_SCHEME:
260 return password == expected_password
261
262
263 if scheme == self._HA1_SCHEME:
264 realm = self.GetAuthRealm(req)
265 if not realm:
266
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
281 """Data structure for users from password file.
282
283 """
284 - def __init__(self, name, password, options):
288
289
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
313
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
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