Statistics
| Branch: | Tag: | Revision:

root / lib / http / auth.py @ 81b59aaf

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

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

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

83 be500c29 Michael Hanselmann
    MAY be overriden by a subclass, which then can return different realms for
84 be500c29 Michael Hanselmann
    different paths. Returning "None" means no authentication is needed for a
85 be500c29 Michael Hanselmann
    request.
86 be500c29 Michael Hanselmann

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

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

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

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

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

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

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

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

198 be500c29 Michael Hanselmann
    This function MUST be overriden by a subclass.
199 be500c29 Michael Hanselmann

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

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

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

219 25e7b43f Iustin Pop
      <username> <password> [options]
220 e6e94655 Michael Hanselmann

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

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

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