Statistics
| Branch: | Tag: | Revision:

root / lib / http / auth.py @ 98ae702b

History | View | Annotate | Download (9.4 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 716a32cb Guido Trotter
from ganeti import compat
31 be500c29 Michael Hanselmann
from ganeti import http
32 2cbe9af3 Michael Hanselmann
from ganeti import utils
33 be500c29 Michael Hanselmann
34 be500c29 Michael Hanselmann
from cStringIO import StringIO
35 be500c29 Michael Hanselmann
36 be500c29 Michael Hanselmann
# Digest types from RFC2617
37 be500c29 Michael Hanselmann
HTTP_BASIC_AUTH = "Basic"
38 be500c29 Michael Hanselmann
HTTP_DIGEST_AUTH = "Digest"
39 be500c29 Michael Hanselmann
40 be500c29 Michael Hanselmann
# Not exactly as described in RFC2616, section 2.2, but good enough
41 8a088b79 Guido Trotter
_NOQUOTE = re.compile(r"^[-_a-z0-9]+$", re.I)
42 be500c29 Michael Hanselmann
43 be500c29 Michael Hanselmann
44 be500c29 Michael Hanselmann
def _FormatAuthHeader(scheme, params):
45 be500c29 Michael Hanselmann
  """Formats WWW-Authentication header value as per RFC2617, section 1.2
46 be500c29 Michael Hanselmann

47 be500c29 Michael Hanselmann
  @type scheme: str
48 be500c29 Michael Hanselmann
  @param scheme: Authentication scheme
49 be500c29 Michael Hanselmann
  @type params: dict
50 be500c29 Michael Hanselmann
  @param params: Additional parameters
51 be500c29 Michael Hanselmann
  @rtype: str
52 be500c29 Michael Hanselmann
  @return: Formatted header value
53 be500c29 Michael Hanselmann

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

85 23ccba04 Michael Hanselmann
    May be overridden by a subclass, which then can return different realms for
86 23ccba04 Michael Hanselmann
    different paths.
87 be500c29 Michael Hanselmann

88 be500c29 Michael Hanselmann
    @type req: L{http.server._HttpServerRequest}
89 be500c29 Michael Hanselmann
    @param req: HTTP request context
90 23ccba04 Michael Hanselmann
    @rtype: string
91 be500c29 Michael Hanselmann
    @return: Authentication realm
92 be500c29 Michael Hanselmann

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

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

105 23ccba04 Michael Hanselmann
    @type req: L{http.server._HttpServerRequest}
106 23ccba04 Michael Hanselmann
    @param req: HTTP request context
107 23ccba04 Michael Hanselmann

108 23ccba04 Michael Hanselmann
    """
109 6873a52a Michael Hanselmann
    # Unused argument, method could be a function
110 b459a848 Andrea Spadaccini
    # pylint: disable=W0613,R0201
111 23ccba04 Michael Hanselmann
    return False
112 23ccba04 Michael Hanselmann
113 be500c29 Michael Hanselmann
  def PreHandleRequest(self, req):
114 be500c29 Michael Hanselmann
    """Called before a request is handled.
115 be500c29 Michael Hanselmann

116 be500c29 Michael Hanselmann
    @type req: L{http.server._HttpServerRequest}
117 be500c29 Michael Hanselmann
    @param req: HTTP request context
118 be500c29 Michael Hanselmann

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

153 be500c29 Michael Hanselmann
    @type req: L{http.server._HttpServerRequest}
154 be500c29 Michael Hanselmann
    @param req: HTTP request context
155 be500c29 Michael Hanselmann
    @rtype: bool
156 be500c29 Michael Hanselmann
    @return: Whether user is allowed to execute request
157 be500c29 Michael Hanselmann

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

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

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

217 5bbd3f7f Michael Hanselmann
    This function MUST be overridden by a subclass.
218 be500c29 Michael Hanselmann

219 be500c29 Michael Hanselmann
    """
220 be500c29 Michael Hanselmann
    raise NotImplementedError()
221 e6e94655 Michael Hanselmann
222 bf9bd8dd Michael Hanselmann
  def VerifyBasicAuthPassword(self, req, username, password, expected):
223 bf9bd8dd Michael Hanselmann
    """Checks the password for basic authentication.
224 bf9bd8dd Michael Hanselmann

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

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

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

283 e6e94655 Michael Hanselmann
  """
284 e6e94655 Michael Hanselmann
  def __init__(self, name, password, options):
285 e6e94655 Michael Hanselmann
    self.name = name
286 e6e94655 Michael Hanselmann
    self.password = password
287 e6e94655 Michael Hanselmann
    self.options = options
288 e6e94655 Michael Hanselmann
289 e6e94655 Michael Hanselmann
290 2287b920 Michael Hanselmann
def ParsePasswordFile(contents):
291 2287b920 Michael Hanselmann
  """Parses the contents of a password file.
292 e6e94655 Michael Hanselmann

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

295 25e7b43f Iustin Pop
      <username> <password> [options]
296 e6e94655 Michael Hanselmann

297 e6e94655 Michael Hanselmann
  Fields are separated by whitespace. Username and password are mandatory,
298 25e7b43f Iustin Pop
  options are optional and separated by comma (','). Empty lines and comments
299 25e7b43f Iustin Pop
  ('#') are ignored.
300 e6e94655 Michael Hanselmann

301 c6e7edb8 Michael Hanselmann
  @type contents: str
302 c6e7edb8 Michael Hanselmann
  @param contents: Contents of password file
303 e6e94655 Michael Hanselmann
  @rtype: dict
304 e6e94655 Michael Hanselmann
  @return: Dictionary containing L{PasswordFileUser} instances
305 e6e94655 Michael Hanselmann

306 e6e94655 Michael Hanselmann
  """
307 e6e94655 Michael Hanselmann
  users = {}
308 e6e94655 Michael Hanselmann
309 2cbe9af3 Michael Hanselmann
  for line in utils.FilterEmptyLinesAndComments(contents):
310 e6e94655 Michael Hanselmann
    parts = line.split(None, 2)
311 e6e94655 Michael Hanselmann
    if len(parts) < 2:
312 e6e94655 Michael Hanselmann
      # Invalid line
313 bfbd12f7 Michael Hanselmann
      # TODO: Return line number from FilterEmptyLinesAndComments
314 bfbd12f7 Michael Hanselmann
      logging.warning("Ignoring non-comment line with less than two fields")
315 e6e94655 Michael Hanselmann
      continue
316 e6e94655 Michael Hanselmann
317 e6e94655 Michael Hanselmann
    name = parts[0]
318 e6e94655 Michael Hanselmann
    password = parts[1]
319 e6e94655 Michael Hanselmann
320 e6e94655 Michael Hanselmann
    # Extract options
321 e6e94655 Michael Hanselmann
    options = []
322 e6e94655 Michael Hanselmann
    if len(parts) >= 3:
323 e6e94655 Michael Hanselmann
      for part in parts[2].split(","):
324 e6e94655 Michael Hanselmann
        options.append(part.strip())
325 bfbd12f7 Michael Hanselmann
    else:
326 bfbd12f7 Michael Hanselmann
      logging.warning("Ignoring values for user '%s': %s", name, parts[3:])
327 e6e94655 Michael Hanselmann
328 e6e94655 Michael Hanselmann
    users[name] = PasswordFileUser(name, password, options)
329 e6e94655 Michael Hanselmann
330 e6e94655 Michael Hanselmann
  return users