Xen: use utils.Readfile to read the VNC password
[ganeti-local] / 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     @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