Statistics
| Branch: | Tag: | Revision:

root / lib / http / auth.py @ 25e7b43f

History | View | Annotate | Download (6.5 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 time
27
import re
28
import base64
29
import binascii
30

    
31
from ganeti import constants
32
from ganeti import utils
33
from ganeti import http
34

    
35
from cStringIO import StringIO
36

    
37

    
38
# Digest types from RFC2617
39
HTTP_BASIC_AUTH = "Basic"
40
HTTP_DIGEST_AUTH = "Digest"
41

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

    
45

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

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

56
  """
57
  buf = StringIO()
58

    
59
  buf.write(scheme)
60

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

    
73
  return buf.getvalue()
74

    
75

    
76
class HttpServerRequestAuthentication(object):
77
  # Default authentication realm
78
  AUTH_REALM = None
79

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

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

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

92
    """
93
    return self.AUTH_REALM
94

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

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

101
    """
102
    realm = self.GetAuthRealm(req)
103

    
104
    # Authentication required?
105
    if realm is None:
106
      return
107

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

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

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

    
125
    raise http.HttpUnauthorized(headers=headers)
126

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

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

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

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

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

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

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

    
164
    # Unsupported authentication scheme
165
    return False
166

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

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

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

    
184
    if ":" not in creds:
185
      return False
186

    
187
    (user, password) = creds.split(":", 1)
188

    
189
    return self.Authenticate(req, user, password)
190

    
191
  def AuthenticateBasic(self, req, user, password):
192
    """Checks the password for a user.
193

194
    This function MUST be overriden by a subclass.
195

196
    """
197
    raise NotImplementedError()
198

    
199

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

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

    
209

    
210
def ReadPasswordFile(file_name):
211
  """Reads a password file.
212

213
  Lines in the password file are of the following format::
214

215
      <username> <password> [options]
216

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

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

226
  """
227
  users = {}
228

    
229
  for line in utils.ReadFile(file_name).splitlines():
230
    line = line.strip()
231

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

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

    
241
    name = parts[0]
242
    password = parts[1]
243

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

    
250
    users[name] = PasswordFileUser(name, password, options)
251

    
252
  return users