Revision be500c29

b/Makefile.am
102 102

  
103 103
http_PYTHON = \
104 104
	lib/http/__init__.py \
105
	lib/http/auth.py \
105 106
	lib/http/client.py \
106 107
	lib/http/server.py
107 108

  
b/lib/http/auth.py
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
    @type credentials: str
133
    @param credentials: Credentials sent
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, input):
170
    """Checks credentials sent for basic authentication.
171

  
172
    @type req: L{http.server._HttpServerRequest}
173
    @param req: HTTP request context
174
    @type input: str
175
    @param input: 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(input.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 AuthenticateBasic(self, req, user, password):
194
    """Checks the password for a user.
195

  
196
    This function MUST be overriden by a subclass.
197

  
198
    """
199
    raise NotImplementedError()

Also available in: Unified diff