Commit be500c29 authored by Michael Hanselmann's avatar Michael Hanselmann

ganeti.http: Add support for basic HTTP authentication

As per RFC2617.

Reviewed-by: amishchenko
parent f8bd7df3
......@@ -102,6 +102,7 @@ rapi_PYTHON = \
http_PYTHON = \
lib/http/__init__.py \
lib/http/auth.py \
lib/http/client.py \
lib/http/server.py
......
#
#
# 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()
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment