Repository: ansible/ansible

Issue:
pamd broke /etc/pam.d/su file after task
<!---
Verify first that your issue/request is not already reported on GitHub.
Also test if the latest release, and master branch are affected too.
-->

##### ISSUE TYPE
<!--- Pick one below and delete the rest: -->
 - Bug Report

##### COMPONENT NAME
<!--- Name of the module/plugin/task/feature -->
pamd

##### ANSIBLE VERSION
<!--- Paste verbatim output from “ansible --version” between quotes below -->
```
ansible 2.3.0.0
  config file = /etc/ansible/ansible.cfg
  configured module search path = Default w/o overrides
  python version = 2.7.5 (default, Aug  2 2016, 04:20:16) [GCC 4.8.5 20150623 (Red Hat 4.8.5-4)]
```
But I use latest version from https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/system/pamd.py
##### CONFIGURATION
<!---
Mention any settings you have changed/added/removed in ansible.cfg
(or using the ANSIBLE_* environment variables).
-->
default
##### OS / ENVIRONMENT
<!---
Mention the OS you are running Ansible from, and the OS you are
managing, or say “N/A” for anything that is not platform-specific.
Also mention the specific version of what you are trying to control,
e.g. if this is a network bug the version of firmware on the network device.
-->
Red Hat Enterprise Linux 7.3
##### SUMMARY
<!--- Explain the problem briefly -->
pamd broke /etc/pam.d/su file after task
##### STEPS TO REPRODUCE
<!---
For bugs, show exactly how to reproduce the problem, using a minimal test-case.
For new features, show how the feature would be used.
-->

<!--- Paste example playbooks or commands between quotes below -->
```yaml
  - name: Disable switch for domain users
    pamd:
      name: su
      type: auth
      control: sufficient
      module_path: pam_rootok.so
      new_type: auth
      new_control: required
      new_module_path: pam_succeed_if.so
      module_arguments: 'uid < 40000'
      state: before
```

<!--- You can also paste gist.github.com links for larger files -->

##### EXPECTED RESULTS
<!--- What did you expect to happen when running the steps above? -->
Good /etc/pam.d/su file:
```
#%PAM-1.0
auth            required        pam_succeed_if.so uid < 40000
auth            sufficient      pam_rootok.so
# Uncomment the following line to implicitly trust users in the "wheel" group.
#auth           sufficient      pam_wheel.so trust use_uid
# Uncomment the following line to require a user to be in the "wheel" group.
#auth           required        pam_wheel.so use_uid
auth            substack        system-auth
auth            include         postlogin
account         sufficient      pam_succeed_if.so uid = 0 use_uid quiet
account         include         system-auth
password        include         system-auth
session         include         system-auth
session         include         postlogin
session         optional        pam_xauth.so
```
##### ACTUAL RESULTS
<!--- What actually happened? If possible run with extra verbosity (-vvvv) -->
Bad /etc/pam.d/su file:
<!--- Paste verbatim command output between quotes below -->
```
#%PAM-1.0
# Uncomment the following line to implicitly trust users in the "wheel" group.
#auth           sufficient      pam_wheel.so trust use_uid
# Uncomment the following line to require a user to be in the "wheel" group.
#auth           required        pam_wheel.so use_uid
auth            required        pam_succeed_if.so uid < 40000
auth            sufficient      pam_rootok.so
auth            substack        system -auth
auth            include postlogin

account         sufficient      pam_succeed_if.so uid = 0 use_uid quiet
account         include system -auth

password        include system -auth

session         include system -auth
session         include postlogin
session         optional        pam_xauth.so
```
Spaces in system-auth module name.
pamd eats trailing comma in module_arguments
##### ISSUE TYPE
 - Bug Report

##### COMPONENT NAME
pamd

##### ANSIBLE VERSION
```
ansible 2.3.1.0
config file =
configured module search path = Default w/o overrides
python version = 2.7.13 (default, March 13 2017, 20:56:15) [GCC 5.4.0]
```

##### CONFIGURATION
Default configuration

##### OS / ENVIRONMENT
N/A

##### SUMMARY
If module_arguments has a trailing comma, the resulting line in the module stack has an empty string.

##### STEPS TO REPRODUCE
pam_access.so requires the argument "listsep=," in order to parse Active Directory groups properly, as they often contain spaces (e.g., "Domain Users").  pamd is unable to add this argument correctly; quoted strings and escape sequences have no effect. 

For example, attempting this task on Ubuntu fails:

<!--- Paste example playbooks or commands between quotes below -->
```yaml
- name: Ensure specific arguments are present in a rule
  pamd:
    name: common-account
    type: account
    control: required
    module_path: pam_access.so
    module_arguments: "listsep=,"
    state: args_present
```

##### EXPECTED RESULTS
The task should add "listsep=," to the pam_access.so line.

##### ACTUAL RESULTS
pamd adds "listsep= ".

Sorry, verbatim output not available; source is on a closed network.


Discussion:
cc @kevensen
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
May be "include" and "substack" control does not taken into account at this line:

https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/system/pamd.py#L272

because it can have any filename.

http://www.linux-pam.org/Linux-PAM-html/sag-configuration-file.html

@ITD27M01 I had not considered that case.  I'll take a look and submit a fix within the next day.
cc @kevensen
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
@kevensen I tested your fixes and they didn't work for me.  See my comment on the pull req.

Resolving patch:
diff --git a/lib/ansible/modules/system/pamd.py b/lib/ansible/modules/system/pamd.py
--- a/lib/ansible/modules/system/pamd.py
+++ b/lib/ansible/modules/system/pamd.py
@@ -1,9 +1,10 @@
 #!/usr/bin/python
 # -*- coding: utf-8 -*-
-# (c) 2016, Kenneth D. Evensen <kevensen@redhat.com>
+# (c) 2017, Kenneth D. Evensen <kevensen@redhat.com>
+
 # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
 
-from __future__ import absolute_import, division, print_function
+from __future__ import (absolute_import, division, print_function)
 __metaclass__ = type
 
 
@@ -62,7 +63,8 @@
       'args_present' any args listed in module_arguments are added if
       missing from the existing rule.  Furthermore, if the module argument
       takes a value denoted by '=', the value will be changed to that specified
-      in module_arguments.
+      in module_arguments.  Note that module_arguments is a list.  Please see
+      the examples for usage.
   state:
     default: updated
     choices:
@@ -157,7 +159,7 @@
     name: system-auth
     type: session control='[success=1 default=ignore]'
     module_path: pam_succeed_if.so
-    module_arguments: 'crond quiet'
+    module_arguments: crond,quiet
     state: args_absent
 
 - name: Ensure specific arguments are present in a rule
@@ -166,7 +168,28 @@
     type: session
     control: '[success=1 default=ignore]'
     module_path: pam_succeed_if.so
-    module_arguments: 'crond quiet'
+    module_arguments: crond,quiet
+    state: args_present
+
+- name: Ensure specific arguments are present in a rule (alternative)
+  pamd:
+    name: system-auth
+    type: session
+    control: '[success=1 default=ignore]'
+    module_path: pam_succeed_if.so
+    module_arguments:
+    - crond
+    - quiet
+    state: args_present
+
+- name: Module arguments requiring commas must be listed as a Yaml list
+  pamd:
+    name: special-module
+    type: account
+    control: required
+    module_path: pam_access.so
+    module_arguments:
+    - listsep=,
     state: args_present
 
 - name: Update specific argument value in a rule
@@ -219,6 +242,7 @@
 ...
 '''
 
+
 from ansible.module_utils.basic import AnsibleModule
 from ansible.module_utils.pycompat24 import get_exception
 import os
@@ -259,18 +283,18 @@ def rulefromstring(cls, stringline):
 
         if '[' in stringline:
             pattern = re.compile(
-                r"""([\-A-Za-z0-9_]+)\s*        # Rule Type
-                    \[([A-Za-z0-9_=\s]+)\]\s*   # Rule Control
-                    ([A-Za-z0-9_\.]+)\s*        # Rule Path
-                    ([A-Za-z0-9_=<>\-\s]*)""",  # Rule Args
+                r"""([\-A-Za-z0-9_]+)\s*         # Rule Type
+                    \[([A-Za-z0-9_=\s]+)\]\s*    # Rule Control
+                    ([A-Za-z0-9_\-\.]+)\s*         # Rule Path
+                    ([A-Za-z0-9,_=<>\-\s]*)""",  # Rule Args
                 re.X)
             complicated = True
         else:
             pattern = re.compile(
                 r"""([\-A-Za-z0-9_]+)\s*        # Rule Type
                     ([A-Za-z0-9_]+)\s*          # Rule Control
-                    ([A-Za-z0-9_\.]+)\s*        # Rule Path
-                    ([A-Za-z0-9_=<>\-\s]*)""",  # Rule Args
+                    ([A-Za-z0-9_\-\.]+)\s*        # Rule Path
+                    ([A-Za-z0-9,_=<>\-\s]*)""",  # Rule Args
                 re.X)
 
         result = pattern.match(stringline)


Test patch:
diff --git a/test/units/modules/system/test_pamd.py b/test/units/modules/system/test_pamd.py
--- a/test/units/modules/system/test_pamd.py
+++ b/test/units/modules/system/test_pamd.py
@@ -52,39 +52,47 @@ def test_less_than_in_args(self):
         self.assertEqual(rule, module_string.rstrip())
         self.assertEqual('uid >= 1025 quiet_success', module.get_module_args_as_string())
 
+    def test_trailing_comma(self):
+        rule = "account required pam_access.so listsep=,"
+        module = PamdRule.rulefromstring(stringline=rule)
+        module_string = re.sub(' +', ' ', str(module).replace('\t', ' '))
+        self.assertEqual(rule, module_string.rstrip())
+
 
 class PamdServiceTestCase(unittest.TestCase):
     def setUp(self):
         self.system_auth_string = """#%PAM-1.0
 # This file is auto-generated.
 # User changes will be destroyed the next time authconfig is run.
-auth      	required	pam_env.so
-auth      	sufficient	pam_unix.so nullok try_first_pass
-auth      	requisite	pam_succeed_if.so uid
-auth      	required	pam_deny.so
-
-account   	required	pam_unix.so
-account   	sufficient	pam_localuser.so
-account   	sufficient	pam_succeed_if.so uid
-account   	required	pam_permit.so
-
-password  	requisite	pam_pwquality.so try_first_pass local_users_only retry=3 authtok_type=
-password  	sufficient	pam_unix.so sha512 shadow nullok try_first_pass use_authtok
-password  	required	pam_deny.so
-
-session   	optional	pam_keyinit.so revoke
-session   	required	pam_limits.so
--session  	optional	pam_systemd.so
-session   	[success=1 default=ignore]	pam_succeed_if.so service in crond quiet use_uid
-session   	[success=1 test=me default=ignore]	pam_succeed_if.so service in crond quiet use_uid
-session   	required	pam_unix.so"""
+auth      \trequired\tpam_env.so
+auth      \tsufficient\tpam_unix.so nullok try_first_pass
+auth      \trequisite\tpam_succeed_if.so uid
+auth      \trequired\tpam_deny.so
+
+account   \trequired\tpam_unix.so
+account   \tsufficient\tpam_localuser.so
+account   \tsufficient\tpam_succeed_if.so uid
+account   \trequired\tpam_permit.so
+account   \trequired\tpam_access.so listsep=,
+session   \tinclude\tsystem-auth
+
+password  \trequisite\tpam_pwquality.so try_first_pass local_users_only retry=3 authtok_type=
+password  \tsufficient\tpam_unix.so sha512 shadow nullok try_first_pass use_authtok
+password  \trequired\tpam_deny.so
+
+session   \toptional\tpam_keyinit.so revoke
+session   \trequired\tpam_limits.so
+-session  \toptional\tpam_systemd.so
+session   \t[success=1 default=ignore]\tpam_succeed_if.so service in crond quiet use_uid
+session   \t[success=1 test=me default=ignore]\tpam_succeed_if.so service in crond quiet use_uid
+session   \trequired\tpam_unix.so"""
 
         self.pamd = PamdService()
         self.pamd.load_rules_from_string(self.system_auth_string)
 
     def test_load_rule_from_string(self):
 
-        self.assertEqual(self.system_auth_string, str(self.pamd))
+        self.assertEqual(self.system_auth_string.rstrip().replace("\n\n", "\n"), str(self.pamd).rstrip().replace("\n\n", "\n"))
 
     def test_update_rule_type(self):
         old_rule = PamdRule.rulefromstring('auth      	required	pam_env.so')
@@ -201,3 +209,10 @@ def test_remove_rule(self):
         old_rule = PamdRule.rulefromstring('account   	required	pam_unix.so')
         remove_rule(self.pamd, old_rule)
         self.assertNotIn(str(old_rule).rstrip(), str(self.pamd))
+
+    def test_add_module_args_with_string(self):
+        old_rule = PamdRule.rulefromstring("account     required    pam_access.so")
+        new_rule = PamdRule.rulefromstring("account     required    pam_access.so listsep=,")
+        args_to_add = ['listsep=,']
+        add_module_arguments(self.pamd, old_rule, args_to_add)
+        self.assertIn(str(new_rule).rstrip(), str(self.pamd))