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
42 from cStringIO import StringIO
43
44
45 HTTP_BASIC_AUTH = "Basic"
46 HTTP_DIGEST_AUTH = "Digest"
47
48
49 _NOQUOTE = re.compile(r"^[-_a-z0-9]+$", re.I)
80
83
84 AUTH_REALM = "Unspecified"
85
86
87 _CLEARTEXT_SCHEME = "{CLEARTEXT}"
88 _HA1_SCHEME = "{HA1}"
89
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
103
104
105 return self.AUTH_REALM
106
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
118
119 return False
120
157
158 @staticmethod
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
173 parts = credentials.strip().split(None, 2)
174 if len(parts) < 1:
175
176 return None, None
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 HttpServerRequestAuthentication._ExtractBasicUserPassword(parts[1])
188
189 elif scheme == HTTP_DIGEST_AUTH.lower():
190
191
192
193
194 pass
195
196
197 return None, None
198
199 @staticmethod
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
218 return creds, None
219
220
221 return creds.split(":", 1)
222
224 """Checks the credentiales.
225
226 This function MUST be overridden by a subclass.
227
228 """
229 raise NotImplementedError()
230
231 @staticmethod
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
245 if not expected_password.startswith("{"):
246 expected_password = (HttpServerRequestAuthentication._CLEARTEXT_SCHEME +
247 expected_password)
248
249
250 if not expected_password.startswith("{"):
251 raise AssertionError("Invalid scheme")
252
253 scheme_end_idx = expected_password.find("}", 1)
254
255
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
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
290 if scheme == HttpServerRequestAuthentication._CLEARTEXT_SCHEME:
291 return password == expected_password
292
293
294 if scheme == HttpServerRequestAuthentication._HA1_SCHEME:
295 if not realm:
296
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