Statistics
| Branch: | Tag: | Revision:

root / lib / http / auth.py @ 04cdf663

History | View | Annotate | Download (9.3 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 re
27 be500c29 Michael Hanselmann
import base64
28 be500c29 Michael Hanselmann
import binascii
29 be500c29 Michael Hanselmann
30 be500c29 Michael Hanselmann
from ganeti import utils
31 be500c29 Michael Hanselmann
from ganeti import http
32 be500c29 Michael Hanselmann
33 be500c29 Michael Hanselmann
from cStringIO import StringIO
34 be500c29 Michael Hanselmann
35 bf9bd8dd Michael Hanselmann
try:
36 bf9bd8dd Michael Hanselmann
  from hashlib import md5
37 bf9bd8dd Michael Hanselmann
except ImportError:
38 bf9bd8dd Michael Hanselmann
  from md5 import new as md5
39 bf9bd8dd Michael Hanselmann
40 be500c29 Michael Hanselmann
41 be500c29 Michael Hanselmann
# Digest types from RFC2617
42 be500c29 Michael Hanselmann
HTTP_BASIC_AUTH = "Basic"
43 be500c29 Michael Hanselmann
HTTP_DIGEST_AUTH = "Digest"
44 be500c29 Michael Hanselmann
45 be500c29 Michael Hanselmann
# Not exactly as described in RFC2616, section 2.2, but good enough
46 8a088b79 Guido Trotter
_NOQUOTE = re.compile(r"^[-_a-z0-9]+$", re.I)
47 be500c29 Michael Hanselmann
48 be500c29 Michael Hanselmann
49 be500c29 Michael Hanselmann
def _FormatAuthHeader(scheme, params):
50 be500c29 Michael Hanselmann
  """Formats WWW-Authentication header value as per RFC2617, section 1.2
51 be500c29 Michael Hanselmann

52 be500c29 Michael Hanselmann
  @type scheme: str
53 be500c29 Michael Hanselmann
  @param scheme: Authentication scheme
54 be500c29 Michael Hanselmann
  @type params: dict
55 be500c29 Michael Hanselmann
  @param params: Additional parameters
56 be500c29 Michael Hanselmann
  @rtype: str
57 be500c29 Michael Hanselmann
  @return: Formatted header value
58 be500c29 Michael Hanselmann

59 be500c29 Michael Hanselmann
  """
60 be500c29 Michael Hanselmann
  buf = StringIO()
61 be500c29 Michael Hanselmann
62 be500c29 Michael Hanselmann
  buf.write(scheme)
63 be500c29 Michael Hanselmann
64 be500c29 Michael Hanselmann
  for name, value in params.iteritems():
65 be500c29 Michael Hanselmann
    buf.write(" ")
66 be500c29 Michael Hanselmann
    buf.write(name)
67 be500c29 Michael Hanselmann
    buf.write("=")
68 be500c29 Michael Hanselmann
    if _NOQUOTE.match(value):
69 be500c29 Michael Hanselmann
      buf.write(value)
70 be500c29 Michael Hanselmann
    else:
71 be500c29 Michael Hanselmann
      buf.write("\"")
72 be500c29 Michael Hanselmann
      # TODO: Better quoting
73 be500c29 Michael Hanselmann
      buf.write(value.replace("\"", "\\\""))
74 be500c29 Michael Hanselmann
      buf.write("\"")
75 be500c29 Michael Hanselmann
76 be500c29 Michael Hanselmann
  return buf.getvalue()
77 be500c29 Michael Hanselmann
78 be500c29 Michael Hanselmann
79 be500c29 Michael Hanselmann
class HttpServerRequestAuthentication(object):
80 be500c29 Michael Hanselmann
  # Default authentication realm
81 23ccba04 Michael Hanselmann
  AUTH_REALM = "Unspecified"
82 be500c29 Michael Hanselmann
83 bf9bd8dd Michael Hanselmann
  # Schemes for passwords
84 bf9bd8dd Michael Hanselmann
  _CLEARTEXT_SCHEME = "{CLEARTEXT}"
85 bf9bd8dd Michael Hanselmann
  _HA1_SCHEME = "{HA1}"
86 bf9bd8dd Michael Hanselmann
87 be500c29 Michael Hanselmann
  def GetAuthRealm(self, req):
88 be500c29 Michael Hanselmann
    """Returns the authentication realm for a request.
89 be500c29 Michael Hanselmann

90 23ccba04 Michael Hanselmann
    May be overridden by a subclass, which then can return different realms for
91 23ccba04 Michael Hanselmann
    different paths.
92 be500c29 Michael Hanselmann

93 be500c29 Michael Hanselmann
    @type req: L{http.server._HttpServerRequest}
94 be500c29 Michael Hanselmann
    @param req: HTTP request context
95 23ccba04 Michael Hanselmann
    @rtype: string
96 be500c29 Michael Hanselmann
    @return: Authentication realm
97 be500c29 Michael Hanselmann

98 be500c29 Michael Hanselmann
    """
99 2d54e29c Iustin Pop
    # today we don't have per-request filtering, but we might want to
100 2d54e29c Iustin Pop
    # add it in the future
101 2d54e29c Iustin Pop
    # pylint: disable-msg=W0613
102 be500c29 Michael Hanselmann
    return self.AUTH_REALM
103 be500c29 Michael Hanselmann
104 23ccba04 Michael Hanselmann
  def AuthenticationRequired(self, req):
105 23ccba04 Michael Hanselmann
    """Determines whether authentication is required for a request.
106 23ccba04 Michael Hanselmann

107 23ccba04 Michael Hanselmann
    To enable authentication, override this function in a subclass and return
108 23ccba04 Michael Hanselmann
    C{True}. L{AUTH_REALM} must be set.
109 23ccba04 Michael Hanselmann

110 23ccba04 Michael Hanselmann
    @type req: L{http.server._HttpServerRequest}
111 23ccba04 Michael Hanselmann
    @param req: HTTP request context
112 23ccba04 Michael Hanselmann

113 23ccba04 Michael Hanselmann
    """
114 6873a52a Michael Hanselmann
    # Unused argument, method could be a function
115 6873a52a Michael Hanselmann
    # pylint: disable-msg=W0613,R0201
116 23ccba04 Michael Hanselmann
    return False
117 23ccba04 Michael Hanselmann
118 be500c29 Michael Hanselmann
  def PreHandleRequest(self, req):
119 be500c29 Michael Hanselmann
    """Called before a request is handled.
120 be500c29 Michael Hanselmann

121 be500c29 Michael Hanselmann
    @type req: L{http.server._HttpServerRequest}
122 be500c29 Michael Hanselmann
    @param req: HTTP request context
123 be500c29 Michael Hanselmann

124 be500c29 Michael Hanselmann
    """
125 81b59aaf Iustin Pop
    # Authentication not required, and no credentials given?
126 23ccba04 Michael Hanselmann
    if not (self.AuthenticationRequired(req) or
127 23ccba04 Michael Hanselmann
            (req.request_headers and
128 23ccba04 Michael Hanselmann
             http.HTTP_AUTHORIZATION in req.request_headers)):
129 be500c29 Michael Hanselmann
      return
130 be500c29 Michael Hanselmann
131 23ccba04 Michael Hanselmann
    realm = self.GetAuthRealm(req)
132 23ccba04 Michael Hanselmann
133 23ccba04 Michael Hanselmann
    if not realm:
134 23ccba04 Michael Hanselmann
      raise AssertionError("No authentication realm")
135 81b59aaf Iustin Pop
136 be500c29 Michael Hanselmann
    # Check "Authorization" header
137 be500c29 Michael Hanselmann
    if self._CheckAuthorization(req):
138 be500c29 Michael Hanselmann
      # User successfully authenticated
139 be500c29 Michael Hanselmann
      return
140 be500c29 Michael Hanselmann
141 be500c29 Michael Hanselmann
    # Send 401 Unauthorized response
142 be500c29 Michael Hanselmann
    params = {
143 be500c29 Michael Hanselmann
      "realm": realm,
144 be500c29 Michael Hanselmann
      }
145 be500c29 Michael Hanselmann
146 be500c29 Michael Hanselmann
    # TODO: Support for Digest authentication (RFC2617, section 3).
147 be500c29 Michael Hanselmann
    # TODO: Support for more than one WWW-Authenticate header with the same
148 be500c29 Michael Hanselmann
    # response (RFC2617, section 4.6).
149 be500c29 Michael Hanselmann
    headers = {
150 be500c29 Michael Hanselmann
      http.HTTP_WWW_AUTHENTICATE: _FormatAuthHeader(HTTP_BASIC_AUTH, params),
151 be500c29 Michael Hanselmann
      }
152 be500c29 Michael Hanselmann
153 be500c29 Michael Hanselmann
    raise http.HttpUnauthorized(headers=headers)
154 be500c29 Michael Hanselmann
155 be500c29 Michael Hanselmann
  def _CheckAuthorization(self, req):
156 25e7b43f Iustin Pop
    """Checks 'Authorization' header sent by client.
157 be500c29 Michael Hanselmann

158 be500c29 Michael Hanselmann
    @type req: L{http.server._HttpServerRequest}
159 be500c29 Michael Hanselmann
    @param req: HTTP request context
160 be500c29 Michael Hanselmann
    @rtype: bool
161 be500c29 Michael Hanselmann
    @return: Whether user is allowed to execute request
162 be500c29 Michael Hanselmann

163 be500c29 Michael Hanselmann
    """
164 be500c29 Michael Hanselmann
    credentials = req.request_headers.get(http.HTTP_AUTHORIZATION, None)
165 be500c29 Michael Hanselmann
    if not credentials:
166 be500c29 Michael Hanselmann
      return False
167 be500c29 Michael Hanselmann
168 be500c29 Michael Hanselmann
    # Extract scheme
169 be500c29 Michael Hanselmann
    parts = credentials.strip().split(None, 2)
170 be500c29 Michael Hanselmann
    if len(parts) < 1:
171 be500c29 Michael Hanselmann
      # Missing scheme
172 be500c29 Michael Hanselmann
      return False
173 be500c29 Michael Hanselmann
174 be500c29 Michael Hanselmann
    # RFC2617, section 1.2: "[...] It uses an extensible, case-insensitive
175 be500c29 Michael Hanselmann
    # token to identify the authentication scheme [...]"
176 be500c29 Michael Hanselmann
    scheme = parts[0].lower()
177 be500c29 Michael Hanselmann
178 be500c29 Michael Hanselmann
    if scheme == HTTP_BASIC_AUTH.lower():
179 be500c29 Michael Hanselmann
      # Do basic authentication
180 be500c29 Michael Hanselmann
      if len(parts) < 2:
181 be500c29 Michael Hanselmann
        raise http.HttpBadRequest(message=("Basic authentication requires"
182 be500c29 Michael Hanselmann
                                           " credentials"))
183 be500c29 Michael Hanselmann
      return self._CheckBasicAuthorization(req, parts[1])
184 be500c29 Michael Hanselmann
185 be500c29 Michael Hanselmann
    elif scheme == HTTP_DIGEST_AUTH.lower():
186 be500c29 Michael Hanselmann
      # TODO: Implement digest authentication
187 be500c29 Michael Hanselmann
      # RFC2617, section 3.3: "Note that the HTTP server does not actually need
188 be500c29 Michael Hanselmann
      # to know the user's cleartext password. As long as H(A1) is available to
189 be500c29 Michael Hanselmann
      # the server, the validity of an Authorization header may be verified."
190 be500c29 Michael Hanselmann
      pass
191 be500c29 Michael Hanselmann
192 be500c29 Michael Hanselmann
    # Unsupported authentication scheme
193 be500c29 Michael Hanselmann
    return False
194 be500c29 Michael Hanselmann
195 e09fdcfa Iustin Pop
  def _CheckBasicAuthorization(self, req, in_data):
196 be500c29 Michael Hanselmann
    """Checks credentials sent for basic authentication.
197 be500c29 Michael Hanselmann

198 be500c29 Michael Hanselmann
    @type req: L{http.server._HttpServerRequest}
199 be500c29 Michael Hanselmann
    @param req: HTTP request context
200 e09fdcfa Iustin Pop
    @type in_data: str
201 e09fdcfa Iustin Pop
    @param in_data: Username and password encoded as Base64
202 be500c29 Michael Hanselmann
    @rtype: bool
203 be500c29 Michael Hanselmann
    @return: Whether user is allowed to execute request
204 be500c29 Michael Hanselmann

205 be500c29 Michael Hanselmann
    """
206 be500c29 Michael Hanselmann
    try:
207 e09fdcfa Iustin Pop
      creds = base64.b64decode(in_data.encode('ascii')).decode('ascii')
208 be500c29 Michael Hanselmann
    except (TypeError, binascii.Error, UnicodeError):
209 be500c29 Michael Hanselmann
      logging.exception("Error when decoding Basic authentication credentials")
210 be500c29 Michael Hanselmann
      return False
211 be500c29 Michael Hanselmann
212 be500c29 Michael Hanselmann
    if ":" not in creds:
213 be500c29 Michael Hanselmann
      return False
214 be500c29 Michael Hanselmann
215 be500c29 Michael Hanselmann
    (user, password) = creds.split(":", 1)
216 be500c29 Michael Hanselmann
217 be500c29 Michael Hanselmann
    return self.Authenticate(req, user, password)
218 be500c29 Michael Hanselmann
219 85414b69 Iustin Pop
  def Authenticate(self, req, user, password):
220 be500c29 Michael Hanselmann
    """Checks the password for a user.
221 be500c29 Michael Hanselmann

222 5bbd3f7f Michael Hanselmann
    This function MUST be overridden by a subclass.
223 be500c29 Michael Hanselmann

224 be500c29 Michael Hanselmann
    """
225 be500c29 Michael Hanselmann
    raise NotImplementedError()
226 e6e94655 Michael Hanselmann
227 bf9bd8dd Michael Hanselmann
  def VerifyBasicAuthPassword(self, req, username, password, expected):
228 bf9bd8dd Michael Hanselmann
    """Checks the password for basic authentication.
229 bf9bd8dd Michael Hanselmann

230 23057d29 Michael Hanselmann
    As long as they don't start with an opening brace ("E{lb}"), old passwords
231 23057d29 Michael Hanselmann
    are supported. A new scheme uses H(A1) from RFC2617, where H is MD5 and A1
232 bf9bd8dd Michael Hanselmann
    consists of the username, the authentication realm and the actual password.
233 bf9bd8dd Michael Hanselmann

234 bf9bd8dd Michael Hanselmann
    @type req: L{http.server._HttpServerRequest}
235 bf9bd8dd Michael Hanselmann
    @param req: HTTP request context
236 bf9bd8dd Michael Hanselmann
    @type username: string
237 bf9bd8dd Michael Hanselmann
    @param username: Username from HTTP headers
238 bf9bd8dd Michael Hanselmann
    @type password: string
239 bf9bd8dd Michael Hanselmann
    @param password: Password from HTTP headers
240 bf9bd8dd Michael Hanselmann
    @type expected: string
241 bf9bd8dd Michael Hanselmann
    @param expected: Expected password with optional scheme prefix (e.g. from
242 bf9bd8dd Michael Hanselmann
                     users file)
243 bf9bd8dd Michael Hanselmann

244 bf9bd8dd Michael Hanselmann
    """
245 bf9bd8dd Michael Hanselmann
    # Backwards compatibility for old-style passwords without a scheme
246 bf9bd8dd Michael Hanselmann
    if not expected.startswith("{"):
247 bf9bd8dd Michael Hanselmann
      expected = self._CLEARTEXT_SCHEME + expected
248 bf9bd8dd Michael Hanselmann
249 bf9bd8dd Michael Hanselmann
    # Check again, just to be sure
250 bf9bd8dd Michael Hanselmann
    if not expected.startswith("{"):
251 bf9bd8dd Michael Hanselmann
      raise AssertionError("Invalid scheme")
252 bf9bd8dd Michael Hanselmann
253 bf9bd8dd Michael Hanselmann
    scheme_end_idx = expected.find("}", 1)
254 bf9bd8dd Michael Hanselmann
255 bf9bd8dd Michael Hanselmann
    # Ensure scheme has a length of at least one character
256 bf9bd8dd Michael Hanselmann
    if scheme_end_idx <= 1:
257 bf9bd8dd Michael Hanselmann
      logging.warning("Invalid scheme in password for user '%s'", username)
258 bf9bd8dd Michael Hanselmann
      return False
259 bf9bd8dd Michael Hanselmann
260 bf9bd8dd Michael Hanselmann
    scheme = expected[:scheme_end_idx + 1].upper()
261 bf9bd8dd Michael Hanselmann
    expected_password = expected[scheme_end_idx + 1:]
262 bf9bd8dd Michael Hanselmann
263 bf9bd8dd Michael Hanselmann
    # Good old plain text password
264 bf9bd8dd Michael Hanselmann
    if scheme == self._CLEARTEXT_SCHEME:
265 bf9bd8dd Michael Hanselmann
      return password == expected_password
266 bf9bd8dd Michael Hanselmann
267 bf9bd8dd Michael Hanselmann
    # H(A1) as described in RFC2617
268 bf9bd8dd Michael Hanselmann
    if scheme == self._HA1_SCHEME:
269 bf9bd8dd Michael Hanselmann
      realm = self.GetAuthRealm(req)
270 bf9bd8dd Michael Hanselmann
      if not realm:
271 bf9bd8dd Michael Hanselmann
        # There can not be a valid password for this case
272 23ccba04 Michael Hanselmann
        raise AssertionError("No authentication realm")
273 bf9bd8dd Michael Hanselmann
274 bf9bd8dd Michael Hanselmann
      expha1 = md5()
275 bf9bd8dd Michael Hanselmann
      expha1.update("%s:%s:%s" % (username, realm, password))
276 bf9bd8dd Michael Hanselmann
277 bf9bd8dd Michael Hanselmann
      return (expected_password.lower() == expha1.hexdigest().lower())
278 bf9bd8dd Michael Hanselmann
279 bf9bd8dd Michael Hanselmann
    logging.warning("Unknown scheme '%s' in password for user '%s'",
280 bf9bd8dd Michael Hanselmann
                    scheme, username)
281 bf9bd8dd Michael Hanselmann
282 bf9bd8dd Michael Hanselmann
    return False
283 bf9bd8dd Michael Hanselmann
284 e6e94655 Michael Hanselmann
285 e6e94655 Michael Hanselmann
class PasswordFileUser(object):
286 e6e94655 Michael Hanselmann
  """Data structure for users from password file.
287 e6e94655 Michael Hanselmann

288 e6e94655 Michael Hanselmann
  """
289 e6e94655 Michael Hanselmann
  def __init__(self, name, password, options):
290 e6e94655 Michael Hanselmann
    self.name = name
291 e6e94655 Michael Hanselmann
    self.password = password
292 e6e94655 Michael Hanselmann
    self.options = options
293 e6e94655 Michael Hanselmann
294 e6e94655 Michael Hanselmann
295 e6e94655 Michael Hanselmann
def ReadPasswordFile(file_name):
296 e6e94655 Michael Hanselmann
  """Reads a password file.
297 e6e94655 Michael Hanselmann

298 25e7b43f Iustin Pop
  Lines in the password file are of the following format::
299 e6e94655 Michael Hanselmann

300 25e7b43f Iustin Pop
      <username> <password> [options]
301 e6e94655 Michael Hanselmann

302 e6e94655 Michael Hanselmann
  Fields are separated by whitespace. Username and password are mandatory,
303 25e7b43f Iustin Pop
  options are optional and separated by comma (','). Empty lines and comments
304 25e7b43f Iustin Pop
  ('#') are ignored.
305 e6e94655 Michael Hanselmann

306 e6e94655 Michael Hanselmann
  @type file_name: str
307 e6e94655 Michael Hanselmann
  @param file_name: Path to password file
308 e6e94655 Michael Hanselmann
  @rtype: dict
309 e6e94655 Michael Hanselmann
  @return: Dictionary containing L{PasswordFileUser} instances
310 e6e94655 Michael Hanselmann

311 e6e94655 Michael Hanselmann
  """
312 e6e94655 Michael Hanselmann
  users = {}
313 e6e94655 Michael Hanselmann
314 e6e94655 Michael Hanselmann
  for line in utils.ReadFile(file_name).splitlines():
315 e6e94655 Michael Hanselmann
    line = line.strip()
316 e6e94655 Michael Hanselmann
317 e6e94655 Michael Hanselmann
    # Ignore empty lines and comments
318 e6e94655 Michael Hanselmann
    if not line or line.startswith("#"):
319 e6e94655 Michael Hanselmann
      continue
320 e6e94655 Michael Hanselmann
321 e6e94655 Michael Hanselmann
    parts = line.split(None, 2)
322 e6e94655 Michael Hanselmann
    if len(parts) < 2:
323 e6e94655 Michael Hanselmann
      # Invalid line
324 e6e94655 Michael Hanselmann
      continue
325 e6e94655 Michael Hanselmann
326 e6e94655 Michael Hanselmann
    name = parts[0]
327 e6e94655 Michael Hanselmann
    password = parts[1]
328 e6e94655 Michael Hanselmann
329 e6e94655 Michael Hanselmann
    # Extract options
330 e6e94655 Michael Hanselmann
    options = []
331 e6e94655 Michael Hanselmann
    if len(parts) >= 3:
332 e6e94655 Michael Hanselmann
      for part in parts[2].split(","):
333 e6e94655 Michael Hanselmann
        options.append(part.strip())
334 e6e94655 Michael Hanselmann
335 e6e94655 Michael Hanselmann
    users[name] = PasswordFileUser(name, password, options)
336 e6e94655 Michael Hanselmann
337 e6e94655 Michael Hanselmann
  return users