root / lib / http / auth.py @ 6b405598
History | View | Annotate | Download (6.5 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 | be500c29 | Michael Hanselmann | _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 | be500c29 | Michael Hanselmann | # Authentication required?
|
105 | be500c29 | Michael Hanselmann | if realm is None: |
106 | be500c29 | Michael Hanselmann | return
|
107 | be500c29 | Michael Hanselmann | |
108 | be500c29 | Michael Hanselmann | # Check "Authorization" header
|
109 | be500c29 | Michael Hanselmann | if self._CheckAuthorization(req): |
110 | be500c29 | Michael Hanselmann | # User successfully authenticated
|
111 | be500c29 | Michael Hanselmann | return
|
112 | be500c29 | Michael Hanselmann | |
113 | be500c29 | Michael Hanselmann | # Send 401 Unauthorized response
|
114 | be500c29 | Michael Hanselmann | params = { |
115 | be500c29 | Michael Hanselmann | "realm": realm,
|
116 | be500c29 | Michael Hanselmann | } |
117 | be500c29 | Michael Hanselmann | |
118 | be500c29 | Michael Hanselmann | # TODO: Support for Digest authentication (RFC2617, section 3).
|
119 | be500c29 | Michael Hanselmann | # TODO: Support for more than one WWW-Authenticate header with the same
|
120 | be500c29 | Michael Hanselmann | # response (RFC2617, section 4.6).
|
121 | be500c29 | Michael Hanselmann | headers = { |
122 | be500c29 | Michael Hanselmann | http.HTTP_WWW_AUTHENTICATE: _FormatAuthHeader(HTTP_BASIC_AUTH, params), |
123 | be500c29 | Michael Hanselmann | } |
124 | be500c29 | Michael Hanselmann | |
125 | be500c29 | Michael Hanselmann | raise http.HttpUnauthorized(headers=headers)
|
126 | be500c29 | Michael Hanselmann | |
127 | be500c29 | Michael Hanselmann | def _CheckAuthorization(self, req): |
128 | 25e7b43f | Iustin Pop | """Checks 'Authorization' header sent by client.
|
129 | be500c29 | Michael Hanselmann |
|
130 | be500c29 | Michael Hanselmann | @type req: L{http.server._HttpServerRequest}
|
131 | be500c29 | Michael Hanselmann | @param req: HTTP request context
|
132 | be500c29 | Michael Hanselmann | @rtype: bool
|
133 | be500c29 | Michael Hanselmann | @return: Whether user is allowed to execute request
|
134 | be500c29 | Michael Hanselmann |
|
135 | be500c29 | Michael Hanselmann | """
|
136 | be500c29 | Michael Hanselmann | credentials = req.request_headers.get(http.HTTP_AUTHORIZATION, None)
|
137 | be500c29 | Michael Hanselmann | if not credentials: |
138 | be500c29 | Michael Hanselmann | return False |
139 | be500c29 | Michael Hanselmann | |
140 | be500c29 | Michael Hanselmann | # Extract scheme
|
141 | be500c29 | Michael Hanselmann | parts = credentials.strip().split(None, 2) |
142 | be500c29 | Michael Hanselmann | if len(parts) < 1: |
143 | be500c29 | Michael Hanselmann | # Missing scheme
|
144 | be500c29 | Michael Hanselmann | return False |
145 | be500c29 | Michael Hanselmann | |
146 | be500c29 | Michael Hanselmann | # RFC2617, section 1.2: "[...] It uses an extensible, case-insensitive
|
147 | be500c29 | Michael Hanselmann | # token to identify the authentication scheme [...]"
|
148 | be500c29 | Michael Hanselmann | scheme = parts[0].lower()
|
149 | be500c29 | Michael Hanselmann | |
150 | be500c29 | Michael Hanselmann | if scheme == HTTP_BASIC_AUTH.lower():
|
151 | be500c29 | Michael Hanselmann | # Do basic authentication
|
152 | be500c29 | Michael Hanselmann | if len(parts) < 2: |
153 | be500c29 | Michael Hanselmann | raise http.HttpBadRequest(message=("Basic authentication requires" |
154 | be500c29 | Michael Hanselmann | " credentials"))
|
155 | be500c29 | Michael Hanselmann | return self._CheckBasicAuthorization(req, parts[1]) |
156 | be500c29 | Michael Hanselmann | |
157 | be500c29 | Michael Hanselmann | elif scheme == HTTP_DIGEST_AUTH.lower():
|
158 | be500c29 | Michael Hanselmann | # TODO: Implement digest authentication
|
159 | be500c29 | Michael Hanselmann | # RFC2617, section 3.3: "Note that the HTTP server does not actually need
|
160 | be500c29 | Michael Hanselmann | # to know the user's cleartext password. As long as H(A1) is available to
|
161 | be500c29 | Michael Hanselmann | # the server, the validity of an Authorization header may be verified."
|
162 | be500c29 | Michael Hanselmann | pass
|
163 | be500c29 | Michael Hanselmann | |
164 | be500c29 | Michael Hanselmann | # Unsupported authentication scheme
|
165 | be500c29 | Michael Hanselmann | return False |
166 | be500c29 | Michael Hanselmann | |
167 | e09fdcfa | Iustin Pop | def _CheckBasicAuthorization(self, req, in_data): |
168 | be500c29 | Michael Hanselmann | """Checks credentials sent for basic authentication.
|
169 | be500c29 | Michael Hanselmann |
|
170 | be500c29 | Michael Hanselmann | @type req: L{http.server._HttpServerRequest}
|
171 | be500c29 | Michael Hanselmann | @param req: HTTP request context
|
172 | e09fdcfa | Iustin Pop | @type in_data: str
|
173 | e09fdcfa | Iustin Pop | @param in_data: Username and password encoded as Base64
|
174 | be500c29 | Michael Hanselmann | @rtype: bool
|
175 | be500c29 | Michael Hanselmann | @return: Whether user is allowed to execute request
|
176 | be500c29 | Michael Hanselmann |
|
177 | be500c29 | Michael Hanselmann | """
|
178 | be500c29 | Michael Hanselmann | try:
|
179 | e09fdcfa | Iustin Pop | creds = base64.b64decode(in_data.encode('ascii')).decode('ascii') |
180 | be500c29 | Michael Hanselmann | except (TypeError, binascii.Error, UnicodeError): |
181 | be500c29 | Michael Hanselmann | logging.exception("Error when decoding Basic authentication credentials")
|
182 | be500c29 | Michael Hanselmann | return False |
183 | be500c29 | Michael Hanselmann | |
184 | be500c29 | Michael Hanselmann | if ":" not in creds: |
185 | be500c29 | Michael Hanselmann | return False |
186 | be500c29 | Michael Hanselmann | |
187 | be500c29 | Michael Hanselmann | (user, password) = creds.split(":", 1) |
188 | be500c29 | Michael Hanselmann | |
189 | be500c29 | Michael Hanselmann | return self.Authenticate(req, user, password) |
190 | be500c29 | Michael Hanselmann | |
191 | 85414b69 | Iustin Pop | def Authenticate(self, req, user, password): |
192 | be500c29 | Michael Hanselmann | """Checks the password for a user.
|
193 | be500c29 | Michael Hanselmann |
|
194 | be500c29 | Michael Hanselmann | This function MUST be overriden by a subclass.
|
195 | be500c29 | Michael Hanselmann |
|
196 | be500c29 | Michael Hanselmann | """
|
197 | be500c29 | Michael Hanselmann | raise NotImplementedError() |
198 | e6e94655 | Michael Hanselmann | |
199 | e6e94655 | Michael Hanselmann | |
200 | e6e94655 | Michael Hanselmann | class PasswordFileUser(object): |
201 | e6e94655 | Michael Hanselmann | """Data structure for users from password file.
|
202 | e6e94655 | Michael Hanselmann |
|
203 | e6e94655 | Michael Hanselmann | """
|
204 | e6e94655 | Michael Hanselmann | def __init__(self, name, password, options): |
205 | e6e94655 | Michael Hanselmann | self.name = name
|
206 | e6e94655 | Michael Hanselmann | self.password = password
|
207 | e6e94655 | Michael Hanselmann | self.options = options
|
208 | e6e94655 | Michael Hanselmann | |
209 | e6e94655 | Michael Hanselmann | |
210 | e6e94655 | Michael Hanselmann | def ReadPasswordFile(file_name): |
211 | e6e94655 | Michael Hanselmann | """Reads a password file.
|
212 | e6e94655 | Michael Hanselmann |
|
213 | 25e7b43f | Iustin Pop | Lines in the password file are of the following format::
|
214 | e6e94655 | Michael Hanselmann |
|
215 | 25e7b43f | Iustin Pop | <username> <password> [options]
|
216 | e6e94655 | Michael Hanselmann |
|
217 | e6e94655 | Michael Hanselmann | Fields are separated by whitespace. Username and password are mandatory,
|
218 | 25e7b43f | Iustin Pop | options are optional and separated by comma (','). Empty lines and comments
|
219 | 25e7b43f | Iustin Pop | ('#') are ignored.
|
220 | e6e94655 | Michael Hanselmann |
|
221 | e6e94655 | Michael Hanselmann | @type file_name: str
|
222 | e6e94655 | Michael Hanselmann | @param file_name: Path to password file
|
223 | e6e94655 | Michael Hanselmann | @rtype: dict
|
224 | e6e94655 | Michael Hanselmann | @return: Dictionary containing L{PasswordFileUser} instances
|
225 | e6e94655 | Michael Hanselmann |
|
226 | e6e94655 | Michael Hanselmann | """
|
227 | e6e94655 | Michael Hanselmann | users = {} |
228 | e6e94655 | Michael Hanselmann | |
229 | e6e94655 | Michael Hanselmann | for line in utils.ReadFile(file_name).splitlines(): |
230 | e6e94655 | Michael Hanselmann | line = line.strip() |
231 | e6e94655 | Michael Hanselmann | |
232 | e6e94655 | Michael Hanselmann | # Ignore empty lines and comments
|
233 | e6e94655 | Michael Hanselmann | if not line or line.startswith("#"): |
234 | e6e94655 | Michael Hanselmann | continue
|
235 | e6e94655 | Michael Hanselmann | |
236 | e6e94655 | Michael Hanselmann | parts = line.split(None, 2) |
237 | e6e94655 | Michael Hanselmann | if len(parts) < 2: |
238 | e6e94655 | Michael Hanselmann | # Invalid line
|
239 | e6e94655 | Michael Hanselmann | continue
|
240 | e6e94655 | Michael Hanselmann | |
241 | e6e94655 | Michael Hanselmann | name = parts[0]
|
242 | e6e94655 | Michael Hanselmann | password = parts[1]
|
243 | e6e94655 | Michael Hanselmann | |
244 | e6e94655 | Michael Hanselmann | # Extract options
|
245 | e6e94655 | Michael Hanselmann | options = [] |
246 | e6e94655 | Michael Hanselmann | if len(parts) >= 3: |
247 | e6e94655 | Michael Hanselmann | for part in parts[2].split(","): |
248 | e6e94655 | Michael Hanselmann | options.append(part.strip()) |
249 | e6e94655 | Michael Hanselmann | |
250 | e6e94655 | Michael Hanselmann | users[name] = PasswordFileUser(name, password, options) |
251 | e6e94655 | Michael Hanselmann | |
252 | e6e94655 | Michael Hanselmann | return users |