Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (6.8 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 not required, and no credentials given?
105
    if realm is None and http.HTTP_AUTHORIZATION not in req.request_headers:
106
      return
107

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

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

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

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

    
129
    raise http.HttpUnauthorized(headers=headers)
130

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

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

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

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

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

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

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

    
168
    # Unsupported authentication scheme
169
    return False
170

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

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

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

    
188
    if ":" not in creds:
189
      return False
190

    
191
    (user, password) = creds.split(":", 1)
192

    
193
    return self.Authenticate(req, user, password)
194

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

198
    This function MUST be overriden by a subclass.
199

200
    """
201
    raise NotImplementedError()
202

    
203

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

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

    
213

    
214
def ReadPasswordFile(file_name):
215
  """Reads a password file.
216

217
  Lines in the password file are of the following format::
218

219
      <username> <password> [options]
220

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

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

230
  """
231
  users = {}
232

    
233
  for line in utils.ReadFile(file_name).splitlines():
234
    line = line.strip()
235

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

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

    
245
    name = parts[0]
246
    password = parts[1]
247

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

    
254
    users[name] = PasswordFileUser(name, password, options)
255

    
256
  return users