root / lib / http / auth.py @ 81b59aaf
History | View | Annotate | Download (6.8 kB)
1 | be500c29 | Michael Hanselmann | #
|
---|---|---|---|
2 | be500c29 | Michael Hanselmann | #
|
3 | be500c29 | Michael Hanselmann | |
4 | be500c29 | Michael Hanselmann | # Copyright (C) 2007, 2008 Google Inc.
|
5 | be500c29 | Michael Hanselmann | #
|
6 | be500c29 | Michael Hanselmann | # This program is free software; you can redistribute it and/or modify
|
7 | be500c29 | Michael Hanselmann | # it under the terms of the GNU General Public License as published by
|
8 | be500c29 | Michael Hanselmann | # the Free Software Foundation; either version 2 of the License, or
|
9 | be500c29 | Michael Hanselmann | # (at your option) any later version.
|
10 | be500c29 | Michael Hanselmann | #
|
11 | be500c29 | Michael Hanselmann | # This program is distributed in the hope that it will be useful, but
|
12 | be500c29 | Michael Hanselmann | # WITHOUT ANY WARRANTY; without even the implied warranty of
|
13 | be500c29 | Michael Hanselmann | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
14 | be500c29 | Michael Hanselmann | # General Public License for more details.
|
15 | be500c29 | Michael Hanselmann | #
|
16 | be500c29 | Michael Hanselmann | # You should have received a copy of the GNU General Public License
|
17 | be500c29 | Michael Hanselmann | # along with this program; if not, write to the Free Software
|
18 | be500c29 | Michael Hanselmann | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
19 | be500c29 | Michael Hanselmann | # 02110-1301, USA.
|
20 | be500c29 | Michael Hanselmann | |
21 | be500c29 | Michael Hanselmann | """HTTP authentication module.
|
22 | be500c29 | Michael Hanselmann |
|
23 | be500c29 | Michael Hanselmann | """
|
24 | be500c29 | Michael Hanselmann | |
25 | be500c29 | Michael Hanselmann | import logging |
26 | be500c29 | Michael Hanselmann | import time |
27 | be500c29 | Michael Hanselmann | import re |
28 | be500c29 | Michael Hanselmann | import base64 |
29 | be500c29 | Michael Hanselmann | import binascii |
30 | be500c29 | Michael Hanselmann | |
31 | be500c29 | Michael Hanselmann | from ganeti import constants |
32 | be500c29 | Michael Hanselmann | from ganeti import utils |
33 | be500c29 | Michael Hanselmann | from ganeti import http |
34 | be500c29 | Michael Hanselmann | |
35 | be500c29 | Michael Hanselmann | from cStringIO import StringIO |
36 | be500c29 | Michael Hanselmann | |
37 | be500c29 | Michael Hanselmann | |
38 | be500c29 | Michael Hanselmann | # Digest types from RFC2617
|
39 | be500c29 | Michael Hanselmann | HTTP_BASIC_AUTH = "Basic"
|
40 | be500c29 | Michael Hanselmann | HTTP_DIGEST_AUTH = "Digest"
|
41 | be500c29 | Michael Hanselmann | |
42 | be500c29 | Michael Hanselmann | # Not exactly as described in RFC2616, section 2.2, but good enough
|
43 | 8a088b79 | Guido Trotter | _NOQUOTE = re.compile(r"^[-_a-z0-9]+$", re.I)
|
44 | be500c29 | Michael Hanselmann | |
45 | be500c29 | Michael Hanselmann | |
46 | be500c29 | Michael Hanselmann | def _FormatAuthHeader(scheme, params): |
47 | be500c29 | Michael Hanselmann | """Formats WWW-Authentication header value as per RFC2617, section 1.2
|
48 | be500c29 | Michael Hanselmann |
|
49 | be500c29 | Michael Hanselmann | @type scheme: str
|
50 | be500c29 | Michael Hanselmann | @param scheme: Authentication scheme
|
51 | be500c29 | Michael Hanselmann | @type params: dict
|
52 | be500c29 | Michael Hanselmann | @param params: Additional parameters
|
53 | be500c29 | Michael Hanselmann | @rtype: str
|
54 | be500c29 | Michael Hanselmann | @return: Formatted header value
|
55 | be500c29 | Michael Hanselmann |
|
56 | be500c29 | Michael Hanselmann | """
|
57 | be500c29 | Michael Hanselmann | buf = StringIO() |
58 | be500c29 | Michael Hanselmann | |
59 | be500c29 | Michael Hanselmann | buf.write(scheme) |
60 | be500c29 | Michael Hanselmann | |
61 | be500c29 | Michael Hanselmann | for name, value in params.iteritems(): |
62 | be500c29 | Michael Hanselmann | buf.write(" ")
|
63 | be500c29 | Michael Hanselmann | buf.write(name) |
64 | be500c29 | Michael Hanselmann | buf.write("=")
|
65 | be500c29 | Michael Hanselmann | if _NOQUOTE.match(value):
|
66 | be500c29 | Michael Hanselmann | buf.write(value) |
67 | be500c29 | Michael Hanselmann | else:
|
68 | be500c29 | Michael Hanselmann | buf.write("\"")
|
69 | be500c29 | Michael Hanselmann | # TODO: Better quoting
|
70 | be500c29 | Michael Hanselmann | buf.write(value.replace("\"", "\\\"")) |
71 | be500c29 | Michael Hanselmann | buf.write("\"")
|
72 | be500c29 | Michael Hanselmann | |
73 | be500c29 | Michael Hanselmann | return buf.getvalue()
|
74 | be500c29 | Michael Hanselmann | |
75 | be500c29 | Michael Hanselmann | |
76 | be500c29 | Michael Hanselmann | class HttpServerRequestAuthentication(object): |
77 | be500c29 | Michael Hanselmann | # Default authentication realm
|
78 | be500c29 | Michael Hanselmann | AUTH_REALM = None
|
79 | be500c29 | Michael Hanselmann | |
80 | be500c29 | Michael Hanselmann | def GetAuthRealm(self, req): |
81 | be500c29 | Michael Hanselmann | """Returns the authentication realm for a request.
|
82 | be500c29 | Michael Hanselmann |
|
83 | be500c29 | Michael Hanselmann | MAY be overriden by a subclass, which then can return different realms for
|
84 | be500c29 | Michael Hanselmann | different paths. Returning "None" means no authentication is needed for a
|
85 | be500c29 | Michael Hanselmann | request.
|
86 | be500c29 | Michael Hanselmann |
|
87 | be500c29 | Michael Hanselmann | @type req: L{http.server._HttpServerRequest}
|
88 | be500c29 | Michael Hanselmann | @param req: HTTP request context
|
89 | be500c29 | Michael Hanselmann | @rtype: str or None
|
90 | be500c29 | Michael Hanselmann | @return: Authentication realm
|
91 | be500c29 | Michael Hanselmann |
|
92 | be500c29 | Michael Hanselmann | """
|
93 | be500c29 | Michael Hanselmann | return self.AUTH_REALM |
94 | be500c29 | Michael Hanselmann | |
95 | be500c29 | Michael Hanselmann | def PreHandleRequest(self, req): |
96 | be500c29 | Michael Hanselmann | """Called before a request is handled.
|
97 | be500c29 | Michael Hanselmann |
|
98 | be500c29 | Michael Hanselmann | @type req: L{http.server._HttpServerRequest}
|
99 | be500c29 | Michael Hanselmann | @param req: HTTP request context
|
100 | be500c29 | Michael Hanselmann |
|
101 | be500c29 | Michael Hanselmann | """
|
102 | be500c29 | Michael Hanselmann | realm = self.GetAuthRealm(req)
|
103 | be500c29 | Michael Hanselmann | |
104 | 81b59aaf | Iustin Pop | # Authentication not required, and no credentials given?
|
105 | 81b59aaf | Iustin Pop | if realm is None and http.HTTP_AUTHORIZATION not in req.request_headers: |
106 | be500c29 | Michael Hanselmann | return
|
107 | be500c29 | Michael Hanselmann | |
108 | 81b59aaf | Iustin Pop | if realm is None: # in case we don't require auth but someone |
109 | 81b59aaf | Iustin Pop | # passed the crendentials anyway
|
110 | 81b59aaf | Iustin Pop | realm = "Unspecified"
|
111 | 81b59aaf | Iustin Pop | |
112 | be500c29 | Michael Hanselmann | # Check "Authorization" header
|
113 | be500c29 | Michael Hanselmann | if self._CheckAuthorization(req): |
114 | be500c29 | Michael Hanselmann | # User successfully authenticated
|
115 | be500c29 | Michael Hanselmann | return
|
116 | be500c29 | Michael Hanselmann | |
117 | be500c29 | Michael Hanselmann | # Send 401 Unauthorized response
|
118 | be500c29 | Michael Hanselmann | params = { |
119 | be500c29 | Michael Hanselmann | "realm": realm,
|
120 | be500c29 | Michael Hanselmann | } |
121 | be500c29 | Michael Hanselmann | |
122 | be500c29 | Michael Hanselmann | # TODO: Support for Digest authentication (RFC2617, section 3).
|
123 | be500c29 | Michael Hanselmann | # TODO: Support for more than one WWW-Authenticate header with the same
|
124 | be500c29 | Michael Hanselmann | # response (RFC2617, section 4.6).
|
125 | be500c29 | Michael Hanselmann | headers = { |
126 | be500c29 | Michael Hanselmann | http.HTTP_WWW_AUTHENTICATE: _FormatAuthHeader(HTTP_BASIC_AUTH, params), |
127 | be500c29 | Michael Hanselmann | } |
128 | be500c29 | Michael Hanselmann | |
129 | be500c29 | Michael Hanselmann | raise http.HttpUnauthorized(headers=headers)
|
130 | be500c29 | Michael Hanselmann | |
131 | be500c29 | Michael Hanselmann | def _CheckAuthorization(self, req): |
132 | 25e7b43f | Iustin Pop | """Checks 'Authorization' header sent by client.
|
133 | be500c29 | Michael Hanselmann |
|
134 | be500c29 | Michael Hanselmann | @type req: L{http.server._HttpServerRequest}
|
135 | be500c29 | Michael Hanselmann | @param req: HTTP request context
|
136 | be500c29 | Michael Hanselmann | @rtype: bool
|
137 | be500c29 | Michael Hanselmann | @return: Whether user is allowed to execute request
|
138 | be500c29 | Michael Hanselmann |
|
139 | be500c29 | Michael Hanselmann | """
|
140 | be500c29 | Michael Hanselmann | credentials = req.request_headers.get(http.HTTP_AUTHORIZATION, None)
|
141 | be500c29 | Michael Hanselmann | if not credentials: |
142 | be500c29 | Michael Hanselmann | return False |
143 | be500c29 | Michael Hanselmann | |
144 | be500c29 | Michael Hanselmann | # Extract scheme
|
145 | be500c29 | Michael Hanselmann | parts = credentials.strip().split(None, 2) |
146 | be500c29 | Michael Hanselmann | if len(parts) < 1: |
147 | be500c29 | Michael Hanselmann | # Missing scheme
|
148 | be500c29 | Michael Hanselmann | return False |
149 | be500c29 | Michael Hanselmann | |
150 | be500c29 | Michael Hanselmann | # RFC2617, section 1.2: "[...] It uses an extensible, case-insensitive
|
151 | be500c29 | Michael Hanselmann | # token to identify the authentication scheme [...]"
|
152 | be500c29 | Michael Hanselmann | scheme = parts[0].lower()
|
153 | be500c29 | Michael Hanselmann | |
154 | be500c29 | Michael Hanselmann | if scheme == HTTP_BASIC_AUTH.lower():
|
155 | be500c29 | Michael Hanselmann | # Do basic authentication
|
156 | be500c29 | Michael Hanselmann | if len(parts) < 2: |
157 | be500c29 | Michael Hanselmann | raise http.HttpBadRequest(message=("Basic authentication requires" |
158 | be500c29 | Michael Hanselmann | " credentials"))
|
159 | be500c29 | Michael Hanselmann | return self._CheckBasicAuthorization(req, parts[1]) |
160 | be500c29 | Michael Hanselmann | |
161 | be500c29 | Michael Hanselmann | elif scheme == HTTP_DIGEST_AUTH.lower():
|
162 | be500c29 | Michael Hanselmann | # TODO: Implement digest authentication
|
163 | be500c29 | Michael Hanselmann | # RFC2617, section 3.3: "Note that the HTTP server does not actually need
|
164 | be500c29 | Michael Hanselmann | # to know the user's cleartext password. As long as H(A1) is available to
|
165 | be500c29 | Michael Hanselmann | # the server, the validity of an Authorization header may be verified."
|
166 | be500c29 | Michael Hanselmann | pass
|
167 | be500c29 | Michael Hanselmann | |
168 | be500c29 | Michael Hanselmann | # Unsupported authentication scheme
|
169 | be500c29 | Michael Hanselmann | return False |
170 | be500c29 | Michael Hanselmann | |
171 | e09fdcfa | Iustin Pop | def _CheckBasicAuthorization(self, req, in_data): |
172 | be500c29 | Michael Hanselmann | """Checks credentials sent for basic authentication.
|
173 | be500c29 | Michael Hanselmann |
|
174 | be500c29 | Michael Hanselmann | @type req: L{http.server._HttpServerRequest}
|
175 | be500c29 | Michael Hanselmann | @param req: HTTP request context
|
176 | e09fdcfa | Iustin Pop | @type in_data: str
|
177 | e09fdcfa | Iustin Pop | @param in_data: Username and password encoded as Base64
|
178 | be500c29 | Michael Hanselmann | @rtype: bool
|
179 | be500c29 | Michael Hanselmann | @return: Whether user is allowed to execute request
|
180 | be500c29 | Michael Hanselmann |
|
181 | be500c29 | Michael Hanselmann | """
|
182 | be500c29 | Michael Hanselmann | try:
|
183 | e09fdcfa | Iustin Pop | creds = base64.b64decode(in_data.encode('ascii')).decode('ascii') |
184 | be500c29 | Michael Hanselmann | except (TypeError, binascii.Error, UnicodeError): |
185 | be500c29 | Michael Hanselmann | logging.exception("Error when decoding Basic authentication credentials")
|
186 | be500c29 | Michael Hanselmann | return False |
187 | be500c29 | Michael Hanselmann | |
188 | be500c29 | Michael Hanselmann | if ":" not in creds: |
189 | be500c29 | Michael Hanselmann | return False |
190 | be500c29 | Michael Hanselmann | |
191 | be500c29 | Michael Hanselmann | (user, password) = creds.split(":", 1) |
192 | be500c29 | Michael Hanselmann | |
193 | be500c29 | Michael Hanselmann | return self.Authenticate(req, user, password) |
194 | be500c29 | Michael Hanselmann | |
195 | 85414b69 | Iustin Pop | def Authenticate(self, req, user, password): |
196 | be500c29 | Michael Hanselmann | """Checks the password for a user.
|
197 | be500c29 | Michael Hanselmann |
|
198 | be500c29 | Michael Hanselmann | This function MUST be overriden by a subclass.
|
199 | be500c29 | Michael Hanselmann |
|
200 | be500c29 | Michael Hanselmann | """
|
201 | be500c29 | Michael Hanselmann | raise NotImplementedError() |
202 | e6e94655 | Michael Hanselmann | |
203 | e6e94655 | Michael Hanselmann | |
204 | e6e94655 | Michael Hanselmann | class PasswordFileUser(object): |
205 | e6e94655 | Michael Hanselmann | """Data structure for users from password file.
|
206 | e6e94655 | Michael Hanselmann |
|
207 | e6e94655 | Michael Hanselmann | """
|
208 | e6e94655 | Michael Hanselmann | def __init__(self, name, password, options): |
209 | e6e94655 | Michael Hanselmann | self.name = name
|
210 | e6e94655 | Michael Hanselmann | self.password = password
|
211 | e6e94655 | Michael Hanselmann | self.options = options
|
212 | e6e94655 | Michael Hanselmann | |
213 | e6e94655 | Michael Hanselmann | |
214 | e6e94655 | Michael Hanselmann | def ReadPasswordFile(file_name): |
215 | e6e94655 | Michael Hanselmann | """Reads a password file.
|
216 | e6e94655 | Michael Hanselmann |
|
217 | 25e7b43f | Iustin Pop | Lines in the password file are of the following format::
|
218 | e6e94655 | Michael Hanselmann |
|
219 | 25e7b43f | Iustin Pop | <username> <password> [options]
|
220 | e6e94655 | Michael Hanselmann |
|
221 | e6e94655 | Michael Hanselmann | Fields are separated by whitespace. Username and password are mandatory,
|
222 | 25e7b43f | Iustin Pop | options are optional and separated by comma (','). Empty lines and comments
|
223 | 25e7b43f | Iustin Pop | ('#') are ignored.
|
224 | e6e94655 | Michael Hanselmann |
|
225 | e6e94655 | Michael Hanselmann | @type file_name: str
|
226 | e6e94655 | Michael Hanselmann | @param file_name: Path to password file
|
227 | e6e94655 | Michael Hanselmann | @rtype: dict
|
228 | e6e94655 | Michael Hanselmann | @return: Dictionary containing L{PasswordFileUser} instances
|
229 | e6e94655 | Michael Hanselmann |
|
230 | e6e94655 | Michael Hanselmann | """
|
231 | e6e94655 | Michael Hanselmann | users = {} |
232 | e6e94655 | Michael Hanselmann | |
233 | e6e94655 | Michael Hanselmann | for line in utils.ReadFile(file_name).splitlines(): |
234 | e6e94655 | Michael Hanselmann | line = line.strip() |
235 | e6e94655 | Michael Hanselmann | |
236 | e6e94655 | Michael Hanselmann | # Ignore empty lines and comments
|
237 | e6e94655 | Michael Hanselmann | if not line or line.startswith("#"): |
238 | e6e94655 | Michael Hanselmann | continue
|
239 | e6e94655 | Michael Hanselmann | |
240 | e6e94655 | Michael Hanselmann | parts = line.split(None, 2) |
241 | e6e94655 | Michael Hanselmann | if len(parts) < 2: |
242 | e6e94655 | Michael Hanselmann | # Invalid line
|
243 | e6e94655 | Michael Hanselmann | continue
|
244 | e6e94655 | Michael Hanselmann | |
245 | e6e94655 | Michael Hanselmann | name = parts[0]
|
246 | e6e94655 | Michael Hanselmann | password = parts[1]
|
247 | e6e94655 | Michael Hanselmann | |
248 | e6e94655 | Michael Hanselmann | # Extract options
|
249 | e6e94655 | Michael Hanselmann | options = [] |
250 | e6e94655 | Michael Hanselmann | if len(parts) >= 3: |
251 | e6e94655 | Michael Hanselmann | for part in parts[2].split(","): |
252 | e6e94655 | Michael Hanselmann | options.append(part.strip()) |
253 | e6e94655 | Michael Hanselmann | |
254 | e6e94655 | Michael Hanselmann | users[name] = PasswordFileUser(name, password, options) |
255 | e6e94655 | Michael Hanselmann | |
256 | e6e94655 | Michael Hanselmann | return users |