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

Reply via email to