Statistics
| Branch: | Tag: | Revision:

root / lib / http / auth.py @ 7c4d6c7b

History | View | Annotate | Download (6.7 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 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 be500c29 Michael Hanselmann
  AUTH_REALM = None
77 be500c29 Michael Hanselmann
78 be500c29 Michael Hanselmann
  def GetAuthRealm(self, req):
79 be500c29 Michael Hanselmann
    """Returns the authentication realm for a request.
80 be500c29 Michael Hanselmann

81 5bbd3f7f Michael Hanselmann
    MAY be overridden by a subclass, which then can return different realms for
82 be500c29 Michael Hanselmann
    different paths. Returning "None" means no authentication is needed for a
83 be500c29 Michael Hanselmann
    request.
84 be500c29 Michael Hanselmann

85 be500c29 Michael Hanselmann
    @type req: L{http.server._HttpServerRequest}
86 be500c29 Michael Hanselmann
    @param req: HTTP request context
87 be500c29 Michael Hanselmann
    @rtype: str or None
88 be500c29 Michael Hanselmann
    @return: Authentication realm
89 be500c29 Michael Hanselmann

90 be500c29 Michael Hanselmann
    """
91 be500c29 Michael Hanselmann
    return self.AUTH_REALM
92 be500c29 Michael Hanselmann
93 be500c29 Michael Hanselmann
  def PreHandleRequest(self, req):
94 be500c29 Michael Hanselmann
    """Called before a request is handled.
95 be500c29 Michael Hanselmann

96 be500c29 Michael Hanselmann
    @type req: L{http.server._HttpServerRequest}
97 be500c29 Michael Hanselmann
    @param req: HTTP request context
98 be500c29 Michael Hanselmann

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

132 be500c29 Michael Hanselmann
    @type req: L{http.server._HttpServerRequest}
133 be500c29 Michael Hanselmann
    @param req: HTTP request context
134 be500c29 Michael Hanselmann
    @rtype: bool
135 be500c29 Michael Hanselmann
    @return: Whether user is allowed to execute request
136 be500c29 Michael Hanselmann

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

172 be500c29 Michael Hanselmann
    @type req: L{http.server._HttpServerRequest}
173 be500c29 Michael Hanselmann
    @param req: HTTP request context
174 e09fdcfa Iustin Pop
    @type in_data: str
175 e09fdcfa Iustin Pop
    @param in_data: Username and password encoded as Base64
176 be500c29 Michael Hanselmann
    @rtype: bool
177 be500c29 Michael Hanselmann
    @return: Whether user is allowed to execute request
178 be500c29 Michael Hanselmann

179 be500c29 Michael Hanselmann
    """
180 be500c29 Michael Hanselmann
    try:
181 e09fdcfa Iustin Pop
      creds = base64.b64decode(in_data.encode('ascii')).decode('ascii')
182 be500c29 Michael Hanselmann
    except (TypeError, binascii.Error, UnicodeError):
183 be500c29 Michael Hanselmann
      logging.exception("Error when decoding Basic authentication credentials")
184 be500c29 Michael Hanselmann
      return False
185 be500c29 Michael Hanselmann
186 be500c29 Michael Hanselmann
    if ":" not in creds:
187 be500c29 Michael Hanselmann
      return False
188 be500c29 Michael Hanselmann
189 be500c29 Michael Hanselmann
    (user, password) = creds.split(":", 1)
190 be500c29 Michael Hanselmann
191 be500c29 Michael Hanselmann
    return self.Authenticate(req, user, password)
192 be500c29 Michael Hanselmann
193 85414b69 Iustin Pop
  def Authenticate(self, req, user, password):
194 be500c29 Michael Hanselmann
    """Checks the password for a user.
195 be500c29 Michael Hanselmann

196 5bbd3f7f Michael Hanselmann
    This function MUST be overridden by a subclass.
197 be500c29 Michael Hanselmann

198 be500c29 Michael Hanselmann
    """
199 be500c29 Michael Hanselmann
    raise NotImplementedError()
200 e6e94655 Michael Hanselmann
201 e6e94655 Michael Hanselmann
202 e6e94655 Michael Hanselmann
class PasswordFileUser(object):
203 e6e94655 Michael Hanselmann
  """Data structure for users from password file.
204 e6e94655 Michael Hanselmann

205 e6e94655 Michael Hanselmann
  """
206 e6e94655 Michael Hanselmann
  def __init__(self, name, password, options):
207 e6e94655 Michael Hanselmann
    self.name = name
208 e6e94655 Michael Hanselmann
    self.password = password
209 e6e94655 Michael Hanselmann
    self.options = options
210 e6e94655 Michael Hanselmann
211 e6e94655 Michael Hanselmann
212 e6e94655 Michael Hanselmann
def ReadPasswordFile(file_name):
213 e6e94655 Michael Hanselmann
  """Reads a password file.
214 e6e94655 Michael Hanselmann

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

217 25e7b43f Iustin Pop
      <username> <password> [options]
218 e6e94655 Michael Hanselmann

219 e6e94655 Michael Hanselmann
  Fields are separated by whitespace. Username and password are mandatory,
220 25e7b43f Iustin Pop
  options are optional and separated by comma (','). Empty lines and comments
221 25e7b43f Iustin Pop
  ('#') are ignored.
222 e6e94655 Michael Hanselmann

223 e6e94655 Michael Hanselmann
  @type file_name: str
224 e6e94655 Michael Hanselmann
  @param file_name: Path to password file
225 e6e94655 Michael Hanselmann
  @rtype: dict
226 e6e94655 Michael Hanselmann
  @return: Dictionary containing L{PasswordFileUser} instances
227 e6e94655 Michael Hanselmann

228 e6e94655 Michael Hanselmann
  """
229 e6e94655 Michael Hanselmann
  users = {}
230 e6e94655 Michael Hanselmann
231 e6e94655 Michael Hanselmann
  for line in utils.ReadFile(file_name).splitlines():
232 e6e94655 Michael Hanselmann
    line = line.strip()
233 e6e94655 Michael Hanselmann
234 e6e94655 Michael Hanselmann
    # Ignore empty lines and comments
235 e6e94655 Michael Hanselmann
    if not line or line.startswith("#"):
236 e6e94655 Michael Hanselmann
      continue
237 e6e94655 Michael Hanselmann
238 e6e94655 Michael Hanselmann
    parts = line.split(None, 2)
239 e6e94655 Michael Hanselmann
    if len(parts) < 2:
240 e6e94655 Michael Hanselmann
      # Invalid line
241 e6e94655 Michael Hanselmann
      continue
242 e6e94655 Michael Hanselmann
243 e6e94655 Michael Hanselmann
    name = parts[0]
244 e6e94655 Michael Hanselmann
    password = parts[1]
245 e6e94655 Michael Hanselmann
246 e6e94655 Michael Hanselmann
    # Extract options
247 e6e94655 Michael Hanselmann
    options = []
248 e6e94655 Michael Hanselmann
    if len(parts) >= 3:
249 e6e94655 Michael Hanselmann
      for part in parts[2].split(","):
250 e6e94655 Michael Hanselmann
        options.append(part.strip())
251 e6e94655 Michael Hanselmann
252 e6e94655 Michael Hanselmann
    users[name] = PasswordFileUser(name, password, options)
253 e6e94655 Michael Hanselmann
254 e6e94655 Michael Hanselmann
  return users