Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (6.7 kB)

1
#
2
#
3

    
4
# Copyright (C) 2007, 2008 Google Inc.
5
#
6
# This program is free software; you can redistribute it and/or modify
7
# it under the terms of the GNU General Public License as published by
8
# the Free Software Foundation; either version 2 of the License, or
9
# (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful, but
12
# WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14
# General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19
# 02110-1301, USA.
20

    
21
"""HTTP authentication module.
22

23
"""
24

    
25
import logging
26
import re
27
import base64
28
import binascii
29

    
30
from ganeti import utils
31
from ganeti import http
32

    
33
from cStringIO import StringIO
34

    
35

    
36
# Digest types from RFC2617
37
HTTP_BASIC_AUTH = "Basic"
38
HTTP_DIGEST_AUTH = "Digest"
39

    
40
# Not exactly as described in RFC2616, section 2.2, but good enough
41
_NOQUOTE = re.compile(r"^[-_a-z0-9]+$", re.I)
42

    
43

    
44
def _FormatAuthHeader(scheme, params):
45
  """Formats WWW-Authentication header value as per RFC2617, section 1.2
46

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

54
  """
55
  buf = StringIO()
56

    
57
  buf.write(scheme)
58

    
59
  for name, value in params.iteritems():
60
    buf.write(" ")
61
    buf.write(name)
62
    buf.write("=")
63
    if _NOQUOTE.match(value):
64
      buf.write(value)
65
    else:
66
      buf.write("\"")
67
      # TODO: Better quoting
68
      buf.write(value.replace("\"", "\\\""))
69
      buf.write("\"")
70

    
71
  return buf.getvalue()
72

    
73

    
74
class HttpServerRequestAuthentication(object):
75
  # Default authentication realm
76
  AUTH_REALM = None
77

    
78
  def GetAuthRealm(self, req):
79
    """Returns the authentication realm for a request.
80

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

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

90
    """
91
    return self.AUTH_REALM
92

    
93
  def PreHandleRequest(self, req):
94
    """Called before a request is handled.
95

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

99
    """
100
    realm = self.GetAuthRealm(req)
101

    
102
    # Authentication not required, and no credentials given?
103
    if realm is None and http.HTTP_AUTHORIZATION not in req.request_headers:
104
      return
105

    
106
    if realm is None: # in case we don't require auth but someone
107
                      # passed the crendentials anyway
108
      realm = "Unspecified"
109

    
110
    # Check "Authorization" header
111
    if self._CheckAuthorization(req):
112
      # User successfully authenticated
113
      return
114

    
115
    # Send 401 Unauthorized response
116
    params = {
117
      "realm": realm,
118
      }
119

    
120
    # TODO: Support for Digest authentication (RFC2617, section 3).
121
    # TODO: Support for more than one WWW-Authenticate header with the same
122
    # response (RFC2617, section 4.6).
123
    headers = {
124
      http.HTTP_WWW_AUTHENTICATE: _FormatAuthHeader(HTTP_BASIC_AUTH, params),
125
      }
126

    
127
    raise http.HttpUnauthorized(headers=headers)
128

    
129
  def _CheckAuthorization(self, req):
130
    """Checks 'Authorization' header sent by client.
131

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

137
    """
138
    credentials = req.request_headers.get(http.HTTP_AUTHORIZATION, None)
139
    if not credentials:
140
      return False
141

    
142
    # Extract scheme
143
    parts = credentials.strip().split(None, 2)
144
    if len(parts) < 1:
145
      # Missing scheme
146
      return False
147

    
148
    # RFC2617, section 1.2: "[...] It uses an extensible, case-insensitive
149
    # token to identify the authentication scheme [...]"
150
    scheme = parts[0].lower()
151

    
152
    if scheme == HTTP_BASIC_AUTH.lower():
153
      # Do basic authentication
154
      if len(parts) < 2:
155
        raise http.HttpBadRequest(message=("Basic authentication requires"
156
                                           " credentials"))
157
      return self._CheckBasicAuthorization(req, parts[1])
158

    
159
    elif scheme == HTTP_DIGEST_AUTH.lower():
160
      # TODO: Implement digest authentication
161
      # RFC2617, section 3.3: "Note that the HTTP server does not actually need
162
      # to know the user's cleartext password. As long as H(A1) is available to
163
      # the server, the validity of an Authorization header may be verified."
164
      pass
165

    
166
    # Unsupported authentication scheme
167
    return False
168

    
169
  def _CheckBasicAuthorization(self, req, in_data):
170
    """Checks credentials sent for basic authentication.
171

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

179
    """
180
    try:
181
      creds = base64.b64decode(in_data.encode('ascii')).decode('ascii')
182
    except (TypeError, binascii.Error, UnicodeError):
183
      logging.exception("Error when decoding Basic authentication credentials")
184
      return False
185

    
186
    if ":" not in creds:
187
      return False
188

    
189
    (user, password) = creds.split(":", 1)
190

    
191
    return self.Authenticate(req, user, password)
192

    
193
  def Authenticate(self, req, user, password):
194
    """Checks the password for a user.
195

196
    This function MUST be overridden by a subclass.
197

198
    """
199
    raise NotImplementedError()
200

    
201

    
202
class PasswordFileUser(object):
203
  """Data structure for users from password file.
204

205
  """
206
  def __init__(self, name, password, options):
207
    self.name = name
208
    self.password = password
209
    self.options = options
210

    
211

    
212
def ReadPasswordFile(file_name):
213
  """Reads a password file.
214

215
  Lines in the password file are of the following format::
216

217
      <username> <password> [options]
218

219
  Fields are separated by whitespace. Username and password are mandatory,
220
  options are optional and separated by comma (','). Empty lines and comments
221
  ('#') are ignored.
222

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

228
  """
229
  users = {}
230

    
231
  for line in utils.ReadFile(file_name).splitlines():
232
    line = line.strip()
233

    
234
    # Ignore empty lines and comments
235
    if not line or line.startswith("#"):
236
      continue
237

    
238
    parts = line.split(None, 2)
239
    if len(parts) < 2:
240
      # Invalid line
241
      continue
242

    
243
    name = parts[0]
244
    password = parts[1]
245

    
246
    # Extract options
247
    options = []
248
    if len(parts) >= 3:
249
      for part in parts[2].split(","):
250
        options.append(part.strip())
251

    
252
    users[name] = PasswordFileUser(name, password, options)
253

    
254
  return users