From be500c29b32a1e644840338dfd667273b197e1b9 Mon Sep 17 00:00:00 2001 From: Michael Hanselmann <hansmi@google.com> Date: Fri, 19 Dec 2008 12:57:22 +0000 Subject: [PATCH] ganeti.http: Add support for basic HTTP authentication As per RFC2617. Reviewed-by: amishchenko --- Makefile.am | 1 + lib/http/auth.py | 199 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 200 insertions(+) create mode 100644 lib/http/auth.py diff --git a/Makefile.am b/Makefile.am index ac2fdc6d0..b1c409064 100644 --- a/Makefile.am +++ b/Makefile.am @@ -102,6 +102,7 @@ rapi_PYTHON = \ http_PYTHON = \ lib/http/__init__.py \ + lib/http/auth.py \ lib/http/client.py \ lib/http/server.py diff --git a/lib/http/auth.py b/lib/http/auth.py new file mode 100644 index 000000000..a85baa8b9 --- /dev/null +++ b/lib/http/auth.py @@ -0,0 +1,199 @@ +# +# + +# Copyright (C) 2007, 2008 Google Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. + +"""HTTP authentication module. + +""" + +import logging +import time +import re +import base64 +import binascii + +from ganeti import constants +from ganeti import utils +from ganeti import http + +from cStringIO import StringIO + + +# Digest types from RFC2617 +HTTP_BASIC_AUTH = "Basic" +HTTP_DIGEST_AUTH = "Digest" + +# Not exactly as described in RFC2616, section 2.2, but good enough +_NOQUOTE = re.compile(r"^[-_a-z0-9]$", re.I) + + +def _FormatAuthHeader(scheme, params): + """Formats WWW-Authentication header value as per RFC2617, section 1.2 + + @type scheme: str + @param scheme: Authentication scheme + @type params: dict + @param params: Additional parameters + @rtype: str + @return: Formatted header value + + """ + buf = StringIO() + + buf.write(scheme) + + for name, value in params.iteritems(): + buf.write(" ") + buf.write(name) + buf.write("=") + if _NOQUOTE.match(value): + buf.write(value) + else: + buf.write("\"") + # TODO: Better quoting + buf.write(value.replace("\"", "\\\"")) + buf.write("\"") + + return buf.getvalue() + + +class HttpServerRequestAuthentication(object): + # Default authentication realm + AUTH_REALM = None + + def GetAuthRealm(self, req): + """Returns the authentication realm for a request. + + MAY be overriden by a subclass, which then can return different realms for + different paths. Returning "None" means no authentication is needed for a + request. + + @type req: L{http.server._HttpServerRequest} + @param req: HTTP request context + @rtype: str or None + @return: Authentication realm + + """ + return self.AUTH_REALM + + def PreHandleRequest(self, req): + """Called before a request is handled. + + @type req: L{http.server._HttpServerRequest} + @param req: HTTP request context + + """ + realm = self.GetAuthRealm(req) + + # Authentication required? + if realm is None: + return + + # Check "Authorization" header + if self._CheckAuthorization(req): + # User successfully authenticated + return + + # Send 401 Unauthorized response + params = { + "realm": realm, + } + + # TODO: Support for Digest authentication (RFC2617, section 3). + # TODO: Support for more than one WWW-Authenticate header with the same + # response (RFC2617, section 4.6). + headers = { + http.HTTP_WWW_AUTHENTICATE: _FormatAuthHeader(HTTP_BASIC_AUTH, params), + } + + raise http.HttpUnauthorized(headers=headers) + + def _CheckAuthorization(self, req): + """Checks "Authorization" header sent by client. + + @type req: L{http.server._HttpServerRequest} + @param req: HTTP request context + @type credentials: str + @param credentials: Credentials sent + @rtype: bool + @return: Whether user is allowed to execute request + + """ + credentials = req.request_headers.get(http.HTTP_AUTHORIZATION, None) + if not credentials: + return False + + # Extract scheme + parts = credentials.strip().split(None, 2) + if len(parts) < 1: + # Missing scheme + return False + + # RFC2617, section 1.2: "[...] It uses an extensible, case-insensitive + # token to identify the authentication scheme [...]" + scheme = parts[0].lower() + + if scheme == HTTP_BASIC_AUTH.lower(): + # Do basic authentication + if len(parts) < 2: + raise http.HttpBadRequest(message=("Basic authentication requires" + " credentials")) + return self._CheckBasicAuthorization(req, parts[1]) + + elif scheme == HTTP_DIGEST_AUTH.lower(): + # TODO: Implement digest authentication + # RFC2617, section 3.3: "Note that the HTTP server does not actually need + # to know the user's cleartext password. As long as H(A1) is available to + # the server, the validity of an Authorization header may be verified." + pass + + # Unsupported authentication scheme + return False + + def _CheckBasicAuthorization(self, req, input): + """Checks credentials sent for basic authentication. + + @type req: L{http.server._HttpServerRequest} + @param req: HTTP request context + @type input: str + @param input: Username and password encoded as Base64 + @rtype: bool + @return: Whether user is allowed to execute request + + """ + try: + creds = base64.b64decode(input.encode('ascii')).decode('ascii') + except (TypeError, binascii.Error, UnicodeError): + logging.exception("Error when decoding Basic authentication credentials") + return False + + if ":" not in creds: + return False + + (user, password) = creds.split(":", 1) + + return self.Authenticate(req, user, password) + + def AuthenticateBasic(self, req, user, password): + """Checks the password for a user. + + This function MUST be overriden by a subclass. + + """ + raise NotImplementedError() -- GitLab