As proposed in doc/design-rapi-pam.rst, implement ValidateRequest function that interacts with PAM in order to perform authentication and then authorization.
Signed-off-by: Oleg Ponomarev <[email protected]> --- Makefile.am | 1 + lib/rapi/auth/pam.py | 351 ++++++++++++++++++++++++++++++++++++++++++++++++ src/Ganeti/Constants.hs | 7 + 3 files changed, 359 insertions(+) create mode 100644 lib/rapi/auth/pam.py diff --git a/Makefile.am b/Makefile.am index 1f50bf2..a7d06a2 100644 --- a/Makefile.am +++ b/Makefile.am @@ -575,6 +575,7 @@ rapi_PYTHON = \ rapi_auth_PYTHON = \ lib/rapi/auth/__init__.py \ lib/rapi/auth/basic_auth.py \ + lib/rapi/auth/pam.py \ lib/rapi/auth/users_file.py http_PYTHON = \ diff --git a/lib/rapi/auth/pam.py b/lib/rapi/auth/pam.py new file mode 100644 index 0000000..6235fb8 --- /dev/null +++ b/lib/rapi/auth/pam.py @@ -0,0 +1,351 @@ +# +# + +# Copyright (C) 2015 Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +"""Module interacting with PAM performing authorization and authentication + +This module authenticates and authorizes RAPI users based on their credintials. +Both actions are performed by interaction with PAM as a 'ganeti-rapi' service. + +""" + +import logging +try: + import ctypes as c # pylint: disable=F0401 +except ImportError: + c = None + +from ganeti import constants +from ganeti.http import HttpInternalServerError, HttpForbidden, HttpBadRequest +from ganeti.http.auth import HttpServerRequestAuthentication +from ganeti.rapi import auth + + +__all__ = ['PamAuthenticator'] + +DEFAULT_SERVICE_NAME = 'ganeti-rapi' +MAX_STR_LENGTH = 100000 +MAX_MSG_COUNT = 100 +PAM_ENV_URI = 'GANETI_RAPI_URI' +PAM_ENV_BODY = 'GANETI_REQUEST_BODY' +PAM_ENV_METHOD = 'GANETI_REQUEST_METHOD' +PAM_ENV_ACCESS = 'GANETI_RESOURCE_ACCESS' + +LIBPAM = c.CDLL(c.util.find_library("pam")) +LIBC = c.CDLL(c.util.find_library("c")) + +PAM_ABORT = 26 +PAM_BUF_ERR = 5 +PAM_CONV_ERR = 19 +PAM_SILENT = 32768 +PAM_SUCCESS = 0 + +PAM_PROMPT_ECHO_OFF = 1 + +PAM_AUTHTOK = 6 + + +class PamHandleT(c.Structure): + """Wrapper for PamHandleT + + """ + _fields_ = [("hidden", c.c_void_p)] + + def __init__(self): + c.Structure.__init__(self) + self.handle = 0 + + +class PamMessage(c.Structure): + """Wrapper for PamMessage + + """ + _fields_ = [ + ("msg_style", c.c_int), + ("msg", c.c_char_p), + ] + + +class PamResponse(c.Structure): + """Wrapper for PamResponse + + """ + _fields_ = [ + ("resp", c.c_char_p), + ("resp_retcode", c.c_int), + ] + + +CONV_FUNC = c.CFUNCTYPE(c.c_int, c.c_int, c.POINTER(c.POINTER(PamMessage)), + c.POINTER(c.POINTER(PamResponse)), c.c_void_p) + + +class PamConv(c.Structure): + """Wrapper for PamConv + + """ + _fields_ = [ + ("conv", CONV_FUNC), + ("appdata_ptr", c.c_void_p), + ] + + +PAM_ACCT_MGMT = LIBPAM.pam_acct_mgmt +PAM_ACCT_MGMT.argtypes = [PamHandleT, c.c_int] +PAM_ACCT_MGMT.restype = c.c_int + +PAM_AUTHENTICATE = LIBPAM.pam_authenticate +PAM_AUTHENTICATE.argtypes = [PamHandleT, c.c_int] +PAM_AUTHENTICATE.restype = c.c_int + +PAM_END = LIBPAM.pam_end +PAM_END.argtypes = [PamHandleT, c.c_int] +PAM_END.restype = c.c_int + +PAM_PUTENV = LIBPAM.pam_putenv +PAM_PUTENV.argtypes = [PamHandleT, c.c_char_p] +PAM_PUTENV.restype = c.c_int + +PAM_SET_ITEM = LIBPAM.pam_set_item +PAM_SET_ITEM.argtypes = [PamHandleT, c.c_int, c.c_void_p] +PAM_SET_ITEM.restype = c.c_int + +PAM_START = LIBPAM.pam_start +PAM_START.argtypes = [ + c.c_char_p, + c.c_char_p, + c.POINTER(PamConv), + c.POINTER(PamHandleT), + ] +PAM_START.restype = c.c_int + + +CALLOC = LIBC.calloc +CALLOC.argtypes = [c.c_uint, c.c_uint] +CALLOC.restype = c.c_void_p + +FREE = LIBC.free +FREE.argstypes = [c.c_void_p] +FREE.restype = None + +STRNDUP = LIBC.strndup +STRNDUP.argstypes = [c.c_char_p, c.c_uint] +STRNDUP.restype = c.c_char_p + + +def Authenticate(pam_handle, authtok=None): + """Performs authentication via PAM. + + Perfroms two steps: + - if authtok is provided then set it with pam_set_item + - call pam_authenticate + + """ + try: + authtok_copy = None + if authtok: + authtok_copy = STRNDUP(authtok, len(authtok)) + if not authtok_copy: + raise HttpInternalServerError("Not enough memory for PAM") + ret = PAM_SET_ITEM(c.pointer(pam_handle), PAM_AUTHTOK, authtok_copy) + if ret != PAM_SUCCESS: + raise HttpInternalServerError("pam_set_item call failed [%d]" % ret) + + ret = PAM_AUTHENTICATE(pam_handle, 0) + if ret == PAM_ABORT: + raise HttpInternalServerError("pam_authenticate requested abort") + if ret != PAM_SUCCESS: + raise HttpUnauthorized("Authentication failed") + except: + PAM_END(pam_handle, ret) + raise + finally: + if authtok_copy: + FREE(authtok_copy) + + +def PutPamEnvVariable(pam_handle, name, value): + """Wrapper over pam_setenv. + + """ + setenv = "%s=" % name + if value: + setenv += value + ret = PAM_PUTENV(pam_handle, setenv) + if ret != PAM_SUCCESS: + raise HttpInternalServerError("pam_putenv call failed [%d]" % ret) + + +def Authorize(pam_handle, uri_access_rights, uri=None, method=None, body=None): + """Performs authorization via PAM. + + Performs two steps: + - initialize environmental variables + - call pam_acct_mgmt + + """ + try: + PutPamEnvVariable(pam_handle, PAM_ENV_ACCESS, uri_access_rights) + PutPamEnvVariable(pam_handle, PAM_ENV_URI, uri) + PutPamEnvVariable(pam_handle, PAM_ENV_METHOD, method) + PutPamEnvVariable(pam_handle, PAM_ENV_BODY, body) + + ret = PAM_ACCT_MGMT(pam_handle, PAM_SILENT) + if ret != PAM_SUCCESS: + raise HttpUnauthorized("Authorization failed") + except: + PAM_END(pam_handle, ret) + raise + + +def ValidateParams(username, _uri_access_rights, password, service, authtok, + _uri, _method, _body): + """Checks whether ValidateRequest has been called with a correct params. + + These checks includes: + - username is an obligatory parameter + - either password or authtok is an obligatory parameter + + """ + if not username: + raise HttpUnauthorized("Username should be provided") + if not service: + raise HttpBadRequest("Service should be proivded") + if not password and not authtok: + raise HttpUnauthorized("At least password or authtok should be provided") + + +def ValidateRequest(username, uri_access_rights, password=None, + service=DEFAULT_SERVICE_NAME, authtok=None, uri=None, + method=None, body=None): + """Checks whether it's permitted to execute an rapi request. + + Calls pam_authenticate and then pam_acct_mgmt in order to check whether a + request should be executed. + + @param username: username + @param uri_access_rights: handler access rights + @param password: password + @param service: a service name that will be used for the interaction with PAM + @param authtok: user's authentication token (e.g. some kind of signature) + @param uri: an uri of a target resource obtained from an http header + @param method: http method trying to access the uri + @param body: a body of an RAPI request + + """ + ValidateParams(username, uri_access_rights, password, service, authtok, uri, + method, body) + + def ConversationFunction(num_msg, msg, resp, _app_data_ptr): + """Conversation function that will be provided to PAM modules. + + The function replies with a password for each message with + PAM_PROMPT_ECHO_OFF style and just ignores the others. + + """ + if num_msg > MAX_MSG_COUNT: + logging.info("Too many messages passed to conv function: [%d]", num_msg) + return PAM_BUF_ERR + response = CALLOC(num_msg, c.sizeof(PamResponse)) + if not response: + logging.info("Calloc failed in conv function") + return PAM_BUF_ERR + resp[0] = c.cast(response, c.POINTER(PamResponse)) + for i in range(num_msg): + if msg[i].contents.msg_style != PAM_PROMPT_ECHO_OFF: + continue + resp.contents[i].resp = STRNDUP(password, len(password)) + if not resp.contents[i].resp: + logging.info("Strndup failed in conv function") + for j in range(i): + FREE(c.cast(resp.contents[j].resp, c.c_void_p)) + FREE(response) + return PAM_BUF_ERR + resp.contents[i].resp_retcode = 0 + return PAM_SUCCESS + + pam_handle = PamHandleT() + conv = PamConv(CONV_FUNC(ConversationFunction), 0) + ret = PAM_START(service, username, c.pointer(conv), c.pointer(pam_handle)) + if ret != PAM_SUCCESS: + PAM_END(pam_handle, ret) + raise HttpInternalServerError("pam_start call failed [%d]" % ret) + + Authenticate(pam_handle, authtok) + Authorize(pam_handle, uri_access_rights, uri, method, body) + + PAM_END(pam_handle, PAM_SUCCESS) + + +def MakeStringC(string): + """Converts a string to a valid C string. + + As a C side treats non-unicode strings, encode unicode string with 'ascii'. + Also ensure that C string will not be longer than MAX_STR_LENGTH in order to + prevent attacs based on too long buffers. + + """ + if string is None: + return None + if isinstance(string, unicode): + string = string.encode("ascii") + if not isinstance(string, str): + return None + if len(string) <= MAX_STR_LENGTH: + return string + return string[:MAX_STR_LENGTH] + + +class PamAuthenticator(auth.RapiAuthenticator): + """Class providing an Authenticate method based on interaction with PAM. + + """ + + def __init__(self): + """Checks whether ctypes has been imported. + + """ + assert c is not None, "ctypes Python package not found but it's required" + + def ValidateRequest(self, req, handler_access): + """Checks whether a user can access a resource. + + """ + username, password = HttpServerRequestAuthentication \ + .ExtractUserPassword(req) + authtok = req.request_headers.get(constants.HTTP_RAPI_PAM_CREDENTIAL, None) + if handler_access is not None: + handler_access_ = ','.join(handler_access) + ValidateRequest(MakeStringC(username), MakeStringC(handler_access_), + MakeStringC(password), + MakeStringC(DEFAULT_SERVICE_NAME), + MakeStringC(authtok), MakeStringC(req.request_path), + MakeStringC(req.request_method), + MakeStringC(req.request_body)) + return True diff --git a/src/Ganeti/Constants.hs b/src/Ganeti/Constants.hs index ae33b7b..0220e66 100644 --- a/src/Ganeti/Constants.hs +++ b/src/Ganeti/Constants.hs @@ -5547,3 +5547,10 @@ maintdSuccessTagPrefix = maintdPrefix ++ "repairready:" maintdFailureTagPrefix :: String maintdFailureTagPrefix = maintdPrefix ++ "repairfailed:" + +-- * RAPI PAM auth related constants + +-- | The name of ganeti rapi specific http header containing additional user +-- credentials +httpRapiPamCredential :: String +httpRapiPamCredential = "Ganeti-RAPI-Credential" -- 2.6.0.rc2.230.g3dd15c0
