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

Reply via email to