Statistics
| Branch: | Tag: | Revision:

root / lib / http / auth.py @ 56452af7

History | View | Annotate | Download (8.9 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 be500c29 Michael Hanselmann
  AUTH_REALM = None
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 5bbd3f7f Michael Hanselmann
    MAY be overridden by a subclass, which then can return different realms for
91 be500c29 Michael Hanselmann
    different paths. Returning "None" means no authentication is needed for a
92 be500c29 Michael Hanselmann
    request.
93 be500c29 Michael Hanselmann

94 be500c29 Michael Hanselmann
    @type req: L{http.server._HttpServerRequest}
95 be500c29 Michael Hanselmann
    @param req: HTTP request context
96 be500c29 Michael Hanselmann
    @rtype: str or None
97 be500c29 Michael Hanselmann
    @return: Authentication realm
98 be500c29 Michael Hanselmann

99 be500c29 Michael Hanselmann
    """
100 2d54e29c Iustin Pop
    # today we don't have per-request filtering, but we might want to
101 2d54e29c Iustin Pop
    # add it in the future
102 2d54e29c Iustin Pop
    # pylint: disable-msg=W0613
103 be500c29 Michael Hanselmann
    return self.AUTH_REALM
104 be500c29 Michael Hanselmann
105 be500c29 Michael Hanselmann
  def PreHandleRequest(self, req):
106 be500c29 Michael Hanselmann
    """Called before a request is handled.
107 be500c29 Michael Hanselmann

108 be500c29 Michael Hanselmann
    @type req: L{http.server._HttpServerRequest}
109 be500c29 Michael Hanselmann
    @param req: HTTP request context
110 be500c29 Michael Hanselmann

111 be500c29 Michael Hanselmann
    """
112 be500c29 Michael Hanselmann
    realm = self.GetAuthRealm(req)
113 be500c29 Michael Hanselmann
114 81b59aaf Iustin Pop
    # Authentication not required, and no credentials given?
115 81b59aaf Iustin Pop
    if realm is None and http.HTTP_AUTHORIZATION not in req.request_headers:
116 be500c29 Michael Hanselmann
      return
117 be500c29 Michael Hanselmann
118 81b59aaf Iustin Pop
    if realm is None: # in case we don't require auth but someone
119 81b59aaf Iustin Pop
                      # passed the crendentials anyway
120 81b59aaf Iustin Pop
      realm = "Unspecified"
121 81b59aaf Iustin Pop
122 be500c29 Michael Hanselmann
    # Check "Authorization" header
123 be500c29 Michael Hanselmann
    if self._CheckAuthorization(req):
124 be500c29 Michael Hanselmann
      # User successfully authenticated
125 be500c29 Michael Hanselmann
      return
126 be500c29 Michael Hanselmann
127 be500c29 Michael Hanselmann
    # Send 401 Unauthorized response
128 be500c29 Michael Hanselmann
    params = {
129 be500c29 Michael Hanselmann
      "realm": realm,
130 be500c29 Michael Hanselmann
      }
131 be500c29 Michael Hanselmann
132 be500c29 Michael Hanselmann
    # TODO: Support for Digest authentication (RFC2617, section 3).
133 be500c29 Michael Hanselmann
    # TODO: Support for more than one WWW-Authenticate header with the same
134 be500c29 Michael Hanselmann
    # response (RFC2617, section 4.6).
135 be500c29 Michael Hanselmann
    headers = {
136 be500c29 Michael Hanselmann
      http.HTTP_WWW_AUTHENTICATE: _FormatAuthHeader(HTTP_BASIC_AUTH, params),
137 be500c29 Michael Hanselmann
      }
138 be500c29 Michael Hanselmann
139 be500c29 Michael Hanselmann
    raise http.HttpUnauthorized(headers=headers)
140 be500c29 Michael Hanselmann
141 be500c29 Michael Hanselmann
  def _CheckAuthorization(self, req):
142 25e7b43f Iustin Pop
    """Checks 'Authorization' header sent by client.
143 be500c29 Michael Hanselmann

144 be500c29 Michael Hanselmann
    @type req: L{http.server._HttpServerRequest}
145 be500c29 Michael Hanselmann
    @param req: HTTP request context
146 be500c29 Michael Hanselmann
    @rtype: bool
147 be500c29 Michael Hanselmann
    @return: Whether user is allowed to execute request
148 be500c29 Michael Hanselmann

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

184 be500c29 Michael Hanselmann
    @type req: L{http.server._HttpServerRequest}
185 be500c29 Michael Hanselmann
    @param req: HTTP request context
186 e09fdcfa Iustin Pop
    @type in_data: str
187 e09fdcfa Iustin Pop
    @param in_data: Username and password encoded as Base64
188 be500c29 Michael Hanselmann
    @rtype: bool
189 be500c29 Michael Hanselmann
    @return: Whether user is allowed to execute request
190 be500c29 Michael Hanselmann

191 be500c29 Michael Hanselmann
    """
192 be500c29 Michael Hanselmann
    try:
193 e09fdcfa Iustin Pop
      creds = base64.b64decode(in_data.encode('ascii')).decode('ascii')
194 be500c29 Michael Hanselmann
    except (TypeError, binascii.Error, UnicodeError):
195 be500c29 Michael Hanselmann
      logging.exception("Error when decoding Basic authentication credentials")
196 be500c29 Michael Hanselmann
      return False
197 be500c29 Michael Hanselmann
198 be500c29 Michael Hanselmann
    if ":" not in creds:
199 be500c29 Michael Hanselmann
      return False
200 be500c29 Michael Hanselmann
201 be500c29 Michael Hanselmann
    (user, password) = creds.split(":", 1)
202 be500c29 Michael Hanselmann
203 be500c29 Michael Hanselmann
    return self.Authenticate(req, user, password)
204 be500c29 Michael Hanselmann
205 85414b69 Iustin Pop
  def Authenticate(self, req, user, password):
206 be500c29 Michael Hanselmann
    """Checks the password for a user.
207 be500c29 Michael Hanselmann

208 5bbd3f7f Michael Hanselmann
    This function MUST be overridden by a subclass.
209 be500c29 Michael Hanselmann

210 be500c29 Michael Hanselmann
    """
211 be500c29 Michael Hanselmann
    raise NotImplementedError()
212 e6e94655 Michael Hanselmann
213 bf9bd8dd Michael Hanselmann
  def VerifyBasicAuthPassword(self, req, username, password, expected):
214 bf9bd8dd Michael Hanselmann
    """Checks the password for basic authentication.
215 bf9bd8dd Michael Hanselmann

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

220 bf9bd8dd Michael Hanselmann
    @type req: L{http.server._HttpServerRequest}
221 bf9bd8dd Michael Hanselmann
    @param req: HTTP request context
222 bf9bd8dd Michael Hanselmann
    @type username: string
223 bf9bd8dd Michael Hanselmann
    @param username: Username from HTTP headers
224 bf9bd8dd Michael Hanselmann
    @type password: string
225 bf9bd8dd Michael Hanselmann
    @param password: Password from HTTP headers
226 bf9bd8dd Michael Hanselmann
    @type expected: string
227 bf9bd8dd Michael Hanselmann
    @param expected: Expected password with optional scheme prefix (e.g. from
228 bf9bd8dd Michael Hanselmann
                     users file)
229 bf9bd8dd Michael Hanselmann

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

274 e6e94655 Michael Hanselmann
  """
275 e6e94655 Michael Hanselmann
  def __init__(self, name, password, options):
276 e6e94655 Michael Hanselmann
    self.name = name
277 e6e94655 Michael Hanselmann
    self.password = password
278 e6e94655 Michael Hanselmann
    self.options = options
279 e6e94655 Michael Hanselmann
280 e6e94655 Michael Hanselmann
281 e6e94655 Michael Hanselmann
def ReadPasswordFile(file_name):
282 e6e94655 Michael Hanselmann
  """Reads a password file.
283 e6e94655 Michael Hanselmann

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

286 25e7b43f Iustin Pop
      <username> <password> [options]
287 e6e94655 Michael Hanselmann

288 e6e94655 Michael Hanselmann
  Fields are separated by whitespace. Username and password are mandatory,
289 25e7b43f Iustin Pop
  options are optional and separated by comma (','). Empty lines and comments
290 25e7b43f Iustin Pop
  ('#') are ignored.
291 e6e94655 Michael Hanselmann

292 e6e94655 Michael Hanselmann
  @type file_name: str
293 e6e94655 Michael Hanselmann
  @param file_name: Path to password file
294 e6e94655 Michael Hanselmann
  @rtype: dict
295 e6e94655 Michael Hanselmann
  @return: Dictionary containing L{PasswordFileUser} instances
296 e6e94655 Michael Hanselmann

297 e6e94655 Michael Hanselmann
  """
298 e6e94655 Michael Hanselmann
  users = {}
299 e6e94655 Michael Hanselmann
300 e6e94655 Michael Hanselmann
  for line in utils.ReadFile(file_name).splitlines():
301 e6e94655 Michael Hanselmann
    line = line.strip()
302 e6e94655 Michael Hanselmann
303 e6e94655 Michael Hanselmann
    # Ignore empty lines and comments
304 e6e94655 Michael Hanselmann
    if not line or line.startswith("#"):
305 e6e94655 Michael Hanselmann
      continue
306 e6e94655 Michael Hanselmann
307 e6e94655 Michael Hanselmann
    parts = line.split(None, 2)
308 e6e94655 Michael Hanselmann
    if len(parts) < 2:
309 e6e94655 Michael Hanselmann
      # Invalid line
310 e6e94655 Michael Hanselmann
      continue
311 e6e94655 Michael Hanselmann
312 e6e94655 Michael Hanselmann
    name = parts[0]
313 e6e94655 Michael Hanselmann
    password = parts[1]
314 e6e94655 Michael Hanselmann
315 e6e94655 Michael Hanselmann
    # Extract options
316 e6e94655 Michael Hanselmann
    options = []
317 e6e94655 Michael Hanselmann
    if len(parts) >= 3:
318 e6e94655 Michael Hanselmann
      for part in parts[2].split(","):
319 e6e94655 Michael Hanselmann
        options.append(part.strip())
320 e6e94655 Michael Hanselmann
321 e6e94655 Michael Hanselmann
    users[name] = PasswordFileUser(name, password, options)
322 e6e94655 Michael Hanselmann
323 e6e94655 Michael Hanselmann
  return users