Statistics
| Branch: | Tag: | Revision:

root / lib / http / auth.py @ 377ae13e

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

305 e6e94655 Michael Hanselmann
  """
306 e6e94655 Michael Hanselmann
  users = {}
307 e6e94655 Michael Hanselmann
308 2287b920 Michael Hanselmann
  for line in contents.splitlines():
309 e6e94655 Michael Hanselmann
    line = line.strip()
310 e6e94655 Michael Hanselmann
311 e6e94655 Michael Hanselmann
    # Ignore empty lines and comments
312 e6e94655 Michael Hanselmann
    if not line or line.startswith("#"):
313 e6e94655 Michael Hanselmann
      continue
314 e6e94655 Michael Hanselmann
315 e6e94655 Michael Hanselmann
    parts = line.split(None, 2)
316 e6e94655 Michael Hanselmann
    if len(parts) < 2:
317 e6e94655 Michael Hanselmann
      # Invalid line
318 e6e94655 Michael Hanselmann
      continue
319 e6e94655 Michael Hanselmann
320 e6e94655 Michael Hanselmann
    name = parts[0]
321 e6e94655 Michael Hanselmann
    password = parts[1]
322 e6e94655 Michael Hanselmann
323 e6e94655 Michael Hanselmann
    # Extract options
324 e6e94655 Michael Hanselmann
    options = []
325 e6e94655 Michael Hanselmann
    if len(parts) >= 3:
326 e6e94655 Michael Hanselmann
      for part in parts[2].split(","):
327 e6e94655 Michael Hanselmann
        options.append(part.strip())
328 e6e94655 Michael Hanselmann
329 e6e94655 Michael Hanselmann
    users[name] = PasswordFileUser(name, password, options)
330 e6e94655 Michael Hanselmann
331 e6e94655 Michael Hanselmann
  return users