The document propose the way of integration PAM into Ganeti RAPI in order to support flexible authentification and authorization.
Signed-off-by: Oleg Ponomarev <[email protected]> --- Makefile.am | 1 + doc/design-optables.rst | 2 + doc/design-rapi-pam.rst | 165 ++++++++++++++++++++++++++++++++++++++++++++++++ doc/index.rst | 1 + doc/rapi.rst | 1 + lib/rapi/pam.py | 119 ++++++++++++++++++++++++++++++++++ 6 files changed, 289 insertions(+) create mode 100644 doc/design-rapi-pam.rst create mode 100644 lib/rapi/pam.py diff --git a/Makefile.am b/Makefile.am index a721089..835abbd 100644 --- a/Makefile.am +++ b/Makefile.am @@ -723,6 +723,7 @@ docinput = \ doc/design-query-splitting.rst \ doc/design-query2.rst \ doc/design-query-splitting.rst \ + doc/design-rapi-pam.rst \ doc/design-reason-trail.rst \ doc/design-repaird.rst \ doc/design-reservations.rst \ diff --git a/doc/design-optables.rst b/doc/design-optables.rst index 6c0c1e0..4c6599f 100644 --- a/doc/design-optables.rst +++ b/doc/design-optables.rst @@ -115,6 +115,8 @@ CONTINUE. Filter chains are processed in increasing order of priority (lowest number means highest priority), then watermark, then UUID. +.. _filter-predicates: + Predicates available for the filter rules ----------------------------------------- diff --git a/doc/design-rapi-pam.rst b/doc/design-rapi-pam.rst new file mode 100644 index 0000000..791a317 --- /dev/null +++ b/doc/design-rapi-pam.rst @@ -0,0 +1,165 @@ +=============================================== +RAPI authentication and authorization using PAM +=============================================== + +.. contents:: :depth: 4 + +This document describes the proposed way of :doc:`rapi` authentication +and authorization refactoring by using pluggable authentication modules +(PAM). + +Current State +============= + +Currently :doc:`rapi` supports authentication using *basic* https +protocol. The users are stored in a file (usually +``/var/lib/ganeti/rapi/users``) and have either read or write rights. +Please read :ref:`rapi-users` for more details. + +.. _motivation: + +Motivation +========== + +During the GanetiCon 2015 the following features were requested by the +community: + +- Support for different authentication methods (e.g. X.509); +- Granular access to different RAPI command subsets; +- Granular access to different target instances. + +The last two statements may be desired when an administrator wants to +provide some restricted cluster or instance management rights for users. + +Extra Brief Overview of PAM +=========================== +Below a small PAM glossary is provided: + +- *applicant* - end-user/management app/daemon; +- *agent* - an app/daemon asking the *applicant* for the + password/security key or just providing its SSH key…; +- *client/server* - entities that interact with *libpamc* and *libpam* + and implement *conversation function* (function that is able to + transfer bytestrings between a *module* and a *client*). An + interaction between *client* and *server* can be performed via HTTPS; +- *module* - service performs user authentication by asking the client + *agents* for password/security key/SSH key via the *conversation + function*; +- *libpam* - authenticate the applicant by calling *modules* + admin-configured sequence. +- *libpamc* - *client*-side PAM helper library containing several + standard agents (e.g. agent asking user for the passowrd) + +In our typical use-case *curl* plays prole of both *agent* and *client*. +Despite the glossary above, *authentication* is only one of four +possible module use-cases. Below there is a list containing all of them: + +- *authentication* - performs user authentication; +- *account* - checks whether an authenticated user still has an access; +- *password* - updates user password/any other credentials; +- *session* - allocates the resources that a user might need during a + session; + +Each application on the server side that uses PAM is recognized as a +PAM *service*. The sequence of PAM modules used for each use-case for +a *service* is defined in the ``/etc/pam.d/service_name`` file. + +Proposed Implementation +======================= + +Ganeti RAPI will use PAM for *authentication* and *authorization* +purposes. In order to preserve backwards compability, ``ganeti-basic`` +PAM module performing *authentication* and *authorization* based on +the contents of ``ganeti/rapi/users`` file. Ganeti rapi will interact +with PAM using ``ganeti-rapi`` service name. The default configuration +for ``ganeti-rapi`` PAM service will just use ``ganeti-basic`` module. + +Authentication Specific Details +------------------------------- + +In case of *basic* http authentication, the username and password will +be extracted as they are presented in the +:ref:`standard form <basic-protocol>`. + +In case of other authentication method ``Authorization`` header field +should be filled with ``Ganeti-RAPI username`` and the user's +credentials should be provided in ``Ganeti-RAPI-Credential`` field. Both +username and credentials should be encoded using base64 algorithm as for +the base authentication. + +Ganeti will copy the username to ``PAM_USER`` field of a ``pam_handler`` +and the credentials to ``PAM_AUTHTOK`` field of a ``pam_handler``. +Such actions will be performed per each request receiced via the +*conversation function* with ``PAM_PROMPT_ECHO_OFF`` message constant. +Other requests will be just ignored. + +Authorization Specific Details +------------------------------ + +Ganeti will pass several parameters that might be useful for the +*authorization* phase to the modules via the private PAM environmental +variables (using ``pam_setenv``) + +GANETI_RAPI_URI + The requested URI. +GANETI_REQUEST_BODY + The body of a request. + +One More Time About the Goals +============================= + +Below the way in which proposed solution helps to reach the goals +specified in the :ref:`motivation section <motivation>` is described. + +Support for Different Authentication Methods +-------------------------------------------- + +Any 1 phase authentication method can be implemented using the proposed +implementation. For most cases it'll be even not necessary to write a +new PAM module because there are already lots of PAM modules that +support different authentication methods. The RAPI *applicant* will +only have to include the corresponding credentials into a http header. + +2 phase authentication doesn't make sense for a rest API because it's +supposed to be used by different automatization tools, not by humans. + +Granular Access to Different Command Subsets +-------------------------------------------- + +This functionality can be implemented just by writing more complex +authorization module that will permit or deny execution of some command +based on the environment variables passed and some additional config +file. + +Granular Access to Different Target Instances +--------------------------------------------- + +For such kind of authorization, a PAM module may be implemented as +well. The main difference is that for complex access rights maintaining +the module will have to store users rights and lists of owned objects +on some kind of dynamic database instead of simple static config file. + +Switching Between the Old and the New Implementations +----------------------------------------------------- + +As the changes introduced should be backwards compatible, new configure +flag ``--enable_pam_rapi`` will be introduced. + +Other Changes +============= + +As writing PAM module can be the most universal solution for the +authorization, sometimes such flexibility is not necessary or not +available because of disabled PAM. In that case there is still +possible to provide granular access to the RAPI. + +For that purpose ``RAPI-Auth:username`` will be added to the reason +trail just before sending job for further processing. That will allow +to configure a filter that will reject all the jobs of some kind +initiated by some specific user i.e. add a user to a blacklist. +See :doc:`design-optables` for more information about filters. + +Other proposal is to introduce a new +:ref:`filter predicate <filter-predicates>`, ``username`` that will be +equal to the authenticated user name and thus will make it possible to +define a set of allowed users for each operation. diff --git a/doc/index.rst b/doc/index.rst index a8b3fba..e5535eb 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -140,6 +140,7 @@ Draft designs design-plain-redundancy.rst design-query2.rst design-query-splitting.rst + design-rapi-pam.rst design-reason-trail.rst design-repaird.rst design-restricted-commands.rst diff --git a/doc/rapi.rst b/doc/rapi.rst index d6cab78..13533e5 100644 --- a/doc/rapi.rst +++ b/doc/rapi.rst @@ -107,6 +107,7 @@ Alternatively, the appropriate parameter of your HTTP client In the current version ``ganeti-rapi``'s realm, ``Ganeti Remote API``, can only be changed by modifying the source code. +.. _basic-protocol: Protocol -------- diff --git a/lib/rapi/pam.py b/lib/rapi/pam.py new file mode 100644 index 0000000..75d82cf --- /dev/null +++ b/lib/rapi/pam.py @@ -0,0 +1,119 @@ +# (c) 2007 Chris AtLee <[email protected]> +# Licensed under the MIT license: +# http://www.opensource.org/licenses/mit-license.php +""" +PAM module for python +Provides an authenticate function that will allow the caller to authenticate +a user against the Pluggable Authentication Modules (PAM) on the system. +Implemented using ctypes, so no compilation is necessary. +""" +__all__ = ['authenticate'] + +from ctypes import CDLL, POINTER, Structure, CFUNCTYPE, cast, pointer, sizeof +from ctypes import c_void_p, c_uint, c_char_p, c_char, c_int +from ctypes.util import find_library + +LIBPAM = CDLL(find_library("pam")) +LIBC = CDLL(find_library("c")) + +CALLOC = LIBC.calloc +CALLOC.restype = c_void_p +CALLOC.argtypes = [c_uint, c_uint] + +STRDUP = LIBC.strdup +STRDUP.argstypes = [c_char_p] +STRDUP.restype = POINTER(c_char) # NOT c_char_p !!!! + +# Various constants +PAM_PROMPT_ECHO_OFF = 1 +PAM_PROMPT_ECHO_ON = 2 +PAM_ERROR_MSG = 3 +PAM_TEXT_INFO = 4 + + +class PamHandle(Structure): + """wrapper class for pam_handle_t""" + _fields_ = [ + ("handle", c_void_p) + ] + + def __init__(self): + Structure.__init__(self) + self.handle = 0 + + +class PamMessage(Structure): + """wrapper class for pam_message structure""" + _fields_ = [ + ("msg_style", c_int), + ("msg", c_char_p), + ] + + def __repr__(self): + return "<PamMessage %i '%s'>" % (self.msg_style, self.msg) + + +class PamResponse(Structure): + """wrapper class for pam_response structure""" + _fields_ = [ + ("resp", c_char_p), + ("resp_retcode", c_int), + ] + + def __repr__(self): + return "<PamResponse %i '%s'>" % (self.resp_retcode, self.resp) + +CONV_FUNC = CFUNCTYPE(c_int, + c_int, POINTER(POINTER(PamMessage)), + POINTER(POINTER(PamResponse)), c_void_p) + + +class PamConv(Structure): + """wrapper class for pam_conv structure""" + _fields_ = [ + ("conv", CONV_FUNC), + ("appdata_ptr", c_void_p) + ] + +PAM_START = LIBPAM.pam_start +PAM_START.restype = c_int +PAM_START.argtypes = [c_char_p, c_char_p, POINTER(PamConv), + POINTER(PamHandle)] + +PAM_AUTHENTICATE = LIBPAM.pam_authenticate +PAM_AUTHENTICATE.restype = c_int +PAM_AUTHENTICATE.argtypes = [PamHandle, c_int] + + +def authenticate(username, password, service='ganeti'): + """Returns True if the given username and password authenticate for the + given service. Returns False otherwise + ``username``: the username to authenticate + ``password``: the password in plain text + ``service``: the PAM service to authenticate against. + Defaults to 'login'""" + @CONV_FUNC + def my_conv(n_messages, messages, p_response, app_data): + """Simple conversation function that responds to any + prompt where the echo is off with the supplied password""" + # Create an array of n_messages response objects + addr = CALLOC(n_messages, sizeof(PamResponse)) + p_response[0] = cast(addr, POINTER(PamResponse)) + for i in range(n_messages): + if messages[i].contents.msg_style == PAM_PROMPT_ECHO_OFF: + pw_copy = STRDUP(str(password)) + p_response.contents[i].resp = cast(pw_copy, c_char_p) + p_response.contents[i].resp_retcode = 0 + return 0 + + handle = PamHandle() + conv = PamConv(my_conv, 0) + retval = PAM_START(service, username, pointer(conv), pointer(handle)) + + if retval != 0: + # TODO: This is not an authentication error, something + # has gone wrong starting up PAM + return False + + retval = PAM_AUTHENTICATE(handle, 0) + return retval == 0 -- 1.9.1
