Statistics
| Branch: | Tag: | Revision:

root / lib / http / auth.py @ 85414b69

History | View | Annotate | Download (6.5 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 be500c29 Michael Hanselmann
_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 be500c29 Michael Hanselmann
    # Authentication required?
105 be500c29 Michael Hanselmann
    if realm is None:
106 be500c29 Michael Hanselmann
      return
107 be500c29 Michael Hanselmann
108 be500c29 Michael Hanselmann
    # Check "Authorization" header
109 be500c29 Michael Hanselmann
    if self._CheckAuthorization(req):
110 be500c29 Michael Hanselmann
      # User successfully authenticated
111 be500c29 Michael Hanselmann
      return
112 be500c29 Michael Hanselmann
113 be500c29 Michael Hanselmann
    # Send 401 Unauthorized response
114 be500c29 Michael Hanselmann
    params = {
115 be500c29 Michael Hanselmann
      "realm": realm,
116 be500c29 Michael Hanselmann
      }
117 be500c29 Michael Hanselmann
118 be500c29 Michael Hanselmann
    # TODO: Support for Digest authentication (RFC2617, section 3).
119 be500c29 Michael Hanselmann
    # TODO: Support for more than one WWW-Authenticate header with the same
120 be500c29 Michael Hanselmann
    # response (RFC2617, section 4.6).
121 be500c29 Michael Hanselmann
    headers = {
122 be500c29 Michael Hanselmann
      http.HTTP_WWW_AUTHENTICATE: _FormatAuthHeader(HTTP_BASIC_AUTH, params),
123 be500c29 Michael Hanselmann
      }
124 be500c29 Michael Hanselmann
125 be500c29 Michael Hanselmann
    raise http.HttpUnauthorized(headers=headers)
126 be500c29 Michael Hanselmann
127 be500c29 Michael Hanselmann
  def _CheckAuthorization(self, req):
128 25e7b43f Iustin Pop
    """Checks 'Authorization' header sent by client.
129 be500c29 Michael Hanselmann

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

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

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

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

194 be500c29 Michael Hanselmann
    This function MUST be overriden by a subclass.
195 be500c29 Michael Hanselmann

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

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

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

215 25e7b43f Iustin Pop
      <username> <password> [options]
216 e6e94655 Michael Hanselmann

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

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

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