Repository: cobbler/cobbler

Issue:
PAM authentication failure
### Describe the bug

<!--- A clear and concise description of what the bug is. -->

<!--- HINT: You can paste gist.github.com links for long logs or larger files -->
Enabling authentication.pam module results in TypeError exception and failure to authenticate.

### To Reproduce

1. Change authentication module to "authentication.pam" in modules.com
2. Restart cobbler service
3. Attempt to login via web UI
4. See error in cobbler.log

````
DEBUG | authn_pam: PAM service is login
INFO | Exception occured: <class 'ctypes.ArgumentError'>
INFO | Exception value: argument 1: <class 'TypeError'>: wrong type
INFO | Exception Info:
  File "/usr/lib/python3.6/site-packages/cobbler/remote.py", line 3477, in _dispatch
    return method_handle(*params)

  File "/usr/lib/python3.6/site-packages/cobbler/remote.py", line 3233, in login
    if self.__validate_user(login_user, login_password):

  File "/usr/lib/python3.6/site-packages/cobbler/remote.py", line 3113, in __validate_user
    return self.api.authenticate(input_user, input_password)

  File "/usr/lib/python3.6/site-packages/cobbler/api.py", line 1599, in authenticate
    rc = self.authn.authenticate(self, user, password)

  File "/usr/lib/python3.6/site-packages/cobbler/modules/authentication/pam.py", line 141, in authenticate
    retval = PAM_START(str(service), str(username), pointer(conv), pointer(handle))

````

### Expected behavior

<!--- A clear and concise description of what you expected to happen. -->
PAM authentication should succeed.

### Version

<!--- Paste output from `cobbler version` -->
````
Cobbler 3.1.2
  source: ?, ?
  build time: Sun May 31 02:32:34 2020
````

### Screenshots

<!--- If applicable, add screenshots to help explain your problem. -->

### Additional context

<!--- Add any other context about the problem here. -->



Discussion:
The following patch seems to have fixed the issue for me:
```diff
--- /usr/lib/python3.6/site-packages/cobbler/modules/authentication/pam.py.orig	2020-08-24 22:12:16.178380466 +0000
+++ /usr/lib/python3.6/site-packages/cobbler/modules/authentication/pam.py	2020-08-24 22:19:34.634209255 +0000
@@ -124,7 +124,7 @@
         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))
+                pw_copy = STRDUP(password.encode())
                 p_response.contents[i].resp = cast(pw_copy, c_char_p)
                 p_response.contents[i].resp_retcode = 0
         return 0
@@ -138,7 +138,7 @@
 
     handle = PamHandle()
     conv = PamConv(my_conv, 0)
-    retval = PAM_START(service, username, pointer(conv), pointer(handle))
+    retval = PAM_START(service.encode(), username.encode(), pointer(conv), pointer(handle))
 
     if retval != 0:
         # TODO: This is not an authentication error, something
```
@fonzinc Does the module work now to your expectations or is there anything else which is broken in your eyes. If everything is fine I will create a PR for this out of your patch.
Yes, PAM authentication seems to work as expected after applying the patch. I have not encountered any additional issues, but I did not conduct a full audit of the module for additional type issues.

I would love to see the patch included. Thanks!
Okay when I have time at the weekend or in the evening I will include it in master. The next release will happen along with the new signatures library (which needs testing before integrating it), thus the next version will need some time until it will get released.
Looks like this got forgotten about and never applied?
Yes it was forgotten. Damit. I will create a PR now.

Resolving patch:
diff --git a/cobbler/modules/authentication/pam.py b/cobbler/modules/authentication/pam.py
--- a/cobbler/modules/authentication/pam.py
+++ b/cobbler/modules/authentication/pam.py
@@ -36,7 +36,6 @@
 Implemented using ctypes, so no compilation is necessary.
 """
 
-from builtins import str
 from builtins import range
 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
@@ -68,7 +67,9 @@ def register():
 
 
 class PamHandle(Structure):
-    """wrapper class for pam_handle_t"""
+    """
+    wrapper class for pam_handle_t
+    """
     _fields_ = [("handle", c_void_p)]
 
     def __init__(self):
@@ -77,7 +78,9 @@ def __init__(self):
 
 
 class PamMessage(Structure):
-    """wrapper class for pam_message structure"""
+    """
+    wrapper class for pam_message structure
+    """
     _fields_ = [("msg_style", c_int), ("msg", c_char_p)]
 
     def __repr__(self):
@@ -85,7 +88,9 @@ def __repr__(self):
 
 
 class PamResponse(Structure):
-    """wrapper class for pam_response structure"""
+    """
+    wrapper class for pam_response structure
+    """
     _fields_ = [("resp", c_char_p), ("resp_retcode", c_int)]
 
     def __repr__(self):
@@ -96,7 +101,9 @@ def __repr__(self):
 
 
 class PamConv(Structure):
-    """wrapper class for pam_conv structure"""
+    """
+    wrapper class for pam_conv structure
+    """
     _fields_ = [("conv", CONV_FUNC), ("appdata_ptr", c_void_p)]
 
 
@@ -111,20 +118,26 @@ class PamConv(Structure):
 
 def authenticate(api_handle, username, password):
     """
-    Returns True if the given username and password authenticate for the
-    given service.  Returns False otherwise
+    :param api_handle: Used for resolving the the pam service name and getting the Logger.
+    :param username:The username to log in with.
+    :type username: str
+    :param password: The password to log in with.
+    :type password: str
+    :returns: True if the given username and password authenticate for the given service. Otherwise False
+    :rtype: bool
     """
 
     @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"""
+        """
+        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))
+                pw_copy = STRDUP(password.encode())
                 p_response.contents[i].resp = cast(pw_copy, c_char_p)
                 p_response.contents[i].resp_retcode = 0
         return 0
@@ -138,11 +151,10 @@ def my_conv(n_messages, messages, p_response, app_data):
 
     handle = PamHandle()
     conv = PamConv(my_conv, 0)
-    retval = PAM_START(service, username, pointer(conv), pointer(handle))
+    retval = PAM_START(service.encode(), username.encode(), pointer(conv), pointer(handle))
 
     if retval != 0:
-        # TODO: This is not an authentication error, something
-        # has gone wrong starting up PAM
+        # TODO: This is not an authentication error, something has gone wrong starting up PAM
         api_handle.logger.error("authn_pam: error initializing PAM library")
         return False
 


Test patch:
diff --git a/tests/modules/authentication/pam_test.py b/tests/modules/authentication/pam_test.py
new file mode 100644
--- /dev/null
+++ b/tests/modules/authentication/pam_test.py
@@ -0,0 +1,16 @@
+from cobbler.api import CobblerAPI
+from cobbler.modules.authentication import pam
+
+
+class TestPam:
+    def test_authenticate(self):
+        # Arrange
+        test_username = "test"
+        test_password = "test"
+        test_api = CobblerAPI()
+
+        # Act
+        result = pam.authenticate(test_api, test_username, test_password)
+
+        # Assert
+        assert result
diff --git a/tests/setup-test-docker.sh b/tests/setup-test-docker.sh
--- a/tests/setup-test-docker.sh
+++ b/tests/setup-test-docker.sh
@@ -23,6 +23,23 @@ pip3 install pykickstart
 # Packages for building & installing cobbler from source
 zypper -n in make gzip sed git hg
 
+# Add Testuser for the PAM tests
+password=$(perl -e 'print crypt("test", "password")')
+useradd -p "$password" test
+
+# Manipulate /etc/pam.d/login
+cat > /etc/pam.d/login << EOF
+#%PAM-1.0
+auth      include    common-auth
+account   include    common-account
+password  include    common-password
+session   required   pam_loginuid.so
+session   optional   pam_keyinit.so force revoke
+session   include    common-session
+#session  optional   pam_lastlog.so nowtmp showfailed
+session   optional   pam_mail.so standard
+EOF
+
 # Set tftpboot location correctly for SUSE distributions
 sed -e "s|/var/lib/tftpboot|/srv/tftpboot|g" -i cobbler/settings.py config/cobbler/settings
 
@@ -33,7 +50,7 @@ pip3 install pytest-pythonpath
 # set SECRET_KEY for django tests
 sed -i s/SECRET_KEY.*/'SECRET_KEY\ =\ "qwertyuiopasdfghl;"'/ cobbler/web/settings.py
 
-# Install and upgrade all dependecys
+# Install and upgrade all dependencies
 pip3 install --upgrade pip
 pip3 install .[lint,test]