From 22f31d02f28a7eb384a589bfa99dbf3a2279ead0 Mon Sep 17 00:00:00 2001
From: Rafael Guterres Jeffman <rjeffman@redhat.com>
Date: Wed, 20 Oct 2021 19:47:43 -0300
Subject: [PATCH] sudorule: Fix runas with external users and groups.

When setting 'runasuser' or 'runasgroup' for a sudorule, either IPA or
external users and groups can be used, but only IPA users and groups
were being searched for when modifying the attributes, making this task
not idempotent if an external group or user was used..

This patch fixes this issue by comparing users and groups to the IPA
and external setting.

The IPA CLI commands are slightly confusing, as the sudorule-add and
sudorule-mod display separate options for internal and external users
and groups, but these options are deprecated and do not work anymore,
in favor of sudorule-add-runasuser and sudorule-add-runasgroup, which
don't diferentiate between internal and external users, from the CLI
user perspective.
---
 plugins/modules/ipasudorule.py   |  96 +++++++++++++++++++----
 tests/sudorule/test_sudorule.yml | 130 +++++++++++++++++++++++++++++++
 2 files changed, 213 insertions(+), 13 deletions(-)

diff --git a/plugins/modules/ipasudorule.py b/plugins/modules/ipasudorule.py
index 8d72d893..d5a459a8 100644
--- a/plugins/modules/ipasudorule.py
+++ b/plugins/modules/ipasudorule.py
@@ -456,11 +456,31 @@ def main():
                     sudooption_add, sudooption_del = gen_add_del_lists(
                         sudooption, res_find.get('ipasudoopt', []))
 
+                    # runasuser attribute can be used with both IPA and
+                    # non-IPA (external) users. IPA will handle the correct
+                    # attribute to properly store data, so we need to compare
+                    # the provided list against both users and external
+                    # users list.
                     runasuser_add, runasuser_del = gen_add_del_lists(
-                        runasuser, res_find.get('ipasudorunas_user', []))
-
+                        runasuser,
+                        (
+                            res_find.get('ipasudorunas_user', [])
+                            + res_find.get('ipasudorunasextuser', [])
+                        )
+                    )
+
+                    # runasgroup attribute can be used with both IPA and
+                    # non-IPA (external) groups. IPA will handle the correct
+                    # attribute to properly store data, so we need to compare
+                    # the provided list against both groups and external
+                    # groups list.
                     runasgroup_add, runasgroup_del = gen_add_del_lists(
-                        runasgroup, res_find.get('ipasudorunas_group', []))
+                        runasgroup,
+                        (
+                            res_find.get('ipasudorunas_group', [])
+                            + res_find.get('ipasudorunasextgroup', [])
+                        )
+                    )
 
                     # Add hosts and hostgroups
                     if len(host_add) > 0 or len(hostgroup_add) > 0:
@@ -593,14 +613,38 @@ def main():
                        "ipasudoopt" in res_find:
                         sudooption = gen_add_list(
                             sudooption, res_find["ipasudoopt"])
-                    if runasuser is not None and \
-                       "ipasudorunas_user" in res_find:
+                    # runasuser attribute can be used with both IPA and
+                    # non-IPA (external) users, so we need to compare
+                    # the provided list against both users and external
+                    # users list.
+                    if (
+                        runasuser is not None
+                        and (
+                            "ipasudorunas_user" in res_find
+                            or "ipasudorunasextuser" in res_find
+                        )
+                    ):
                         runasuser = gen_add_list(
-                            runasuser, res_find["ipasudorunas_user"])
-                    if runasgroup is not None and \
-                       "ipasudorunasgroup_group" in res_find:
+                            runasuser,
+                            (list(res_find.get('ipasudorunas_user', []))
+                             + list(res_find.get('ipasudorunasextuser', [])))
+                        )
+                    # runasgroup attribute can be used with both IPA and
+                    # non-IPA (external) groups, so we need to compare
+                    # the provided list against both users and external
+                    # groups list.
+                    if (
+                        runasgroup is not None
+                        and (
+                            "ipasudorunasgroup_group" in res_find
+                            or "ipasudorunasextgroup" in res_find
+                        )
+                    ):
                         runasgroup = gen_add_list(
-                            runasgroup, res_find["ipasudorunasgroup_group"])
+                            runasgroup,
+                            (list(res_find.get("ipasudorunasgroup_group", []))
+                             + list(res_find.get("ipasudorunasextgroup", [])))
+                        )
 
                     # Add hosts and hostgroups
                     if host is not None or hostgroup is not None:
@@ -724,17 +768,43 @@ def main():
                                 sudooption, res_find["ipasudoopt"])
                         else:
                             sudooption = None
+                    # runasuser attribute can be used with both IPA and
+                    # non-IPA (external) users, so we need to compare
+                    # the provided list against both users and external
+                    # users list.
                     if runasuser is not None:
-                        if "ipasudorunas_user" in res_find:
+                        if (
+                            "ipasudorunas_user" in res_find
+                            or "ipasudorunasextuser" in res_find
+                        ):
                             runasuser = gen_intersection_list(
-                                runasuser, res_find["ipasudorunas_user"])
+                                runasuser,
+                                (
+                                    list(res_find.get('ipasudorunas_user', []))
+                                    + list(res_find.get(
+                                        'ipasudorunasextuser', []))
+                                )
+                            )
                         else:
                             runasuser = None
+                    # runasgroup attribute can be used with both IPA and
+                    # non-IPA (external) groups, so we need to compare
+                    # the provided list against both groups and external
+                    # groups list.
                     if runasgroup is not None:
-                        if "ipasudorunasgroup_group" in res_find:
+                        if (
+                            "ipasudorunasgroup_group" in res_find
+                            or "ipasudorunasextgroup" in res_find
+                        ):
                             runasgroup = gen_intersection_list(
                                 runasgroup,
-                                res_find["ipasudorunasgroup_group"])
+                                (
+                                    list(res_find.get(
+                                        "ipasudorunasgroup_group", []))
+                                    + list(res_find.get(
+                                        "ipasudorunasextgroup", []))
+                                )
+                            )
                         else:
                             runasgroup = None
 
diff --git a/tests/sudorule/test_sudorule.yml b/tests/sudorule/test_sudorule.yml
index 92ea773f..918ab5bf 100644
--- a/tests/sudorule/test_sudorule.yml
+++ b/tests/sudorule/test_sudorule.yml
@@ -73,6 +73,7 @@
       ipaadmin_password: SomeADMINpassword
       ipaapi_context: "{{ ipa_context | default(omit) }}"
       name:
+      - test_upstream_issue_664
       - testrule1
       - allusers
       - allhosts
@@ -755,6 +756,134 @@
     register: result
     failed_when: result.changed or result.failed
 
+  - name: Ensure sudorule with external user in 'runasuser' is present
+    ipasudorule:
+      ipaadmin_password: SomeADMINpassword
+      name: test_upstream_issue_664
+      runasuser:
+        - apache
+    register: result
+    failed_when: not result.changed or result.failed
+
+  - name: Ensure sudorule with external user in 'runasuser' is present, again
+    ipasudorule:
+      ipaadmin_password: SomeADMINpassword
+      name: test_upstream_issue_664
+      runasuser:
+        - apache
+    register: result
+    failed_when: result.changed or result.failed
+
+  - name: Ensure sudorule member external user in 'runasuser' is absent
+    ipasudorule:
+      ipaadmin_password: SomeADMINpassword
+      name: test_upstream_issue_664
+      runasuser:
+        - apache
+      action: member
+      state: absent
+    register: result
+    failed_when: not result.changed or result.failed
+
+  - name: Ensure sudorule member external user in 'runasuser' is absent, again
+    ipasudorule:
+      ipaadmin_password: SomeADMINpassword
+      name: test_upstream_issue_664
+      runasuser:
+        - apache
+      action: member
+      state: absent
+    register: result
+    failed_when: result.changed or result.failed
+
+  - name: Ensure sudorule member external user in 'runasuser' is present
+    ipasudorule:
+      ipaadmin_password: SomeADMINpassword
+      name: test_upstream_issue_664
+      runasuser:
+        - apache
+      action: member
+    register: result
+    failed_when: not result.changed or result.failed
+
+  - name: Ensure sudorule member external user in 'runasuser' is present, again
+    ipasudorule:
+      ipaadmin_password: SomeADMINpassword
+      name: test_upstream_issue_664
+      runasuser:
+        - apache
+      action: member
+    register: result
+    failed_when: result.changed or result.failed
+
+  - name: Ensure sudorule with external group in 'runasgroup' is present
+    ipasudorule:
+      ipaadmin_password: SomeADMINpassword
+      name: test_upstream_issue_664
+      runasgroup:
+        - wheel
+    register: result
+    failed_when: not result.changed or result.failed
+
+  - name: Ensure sudorule with external group in 'runasgroup' user is present, again
+    ipasudorule:
+      ipaadmin_password: SomeADMINpassword
+      name: test_upstream_issue_664
+      runasgroup:
+        - wheel
+    register: result
+    failed_when: result.changed or result.failed
+
+  - name: Ensure sudorule member external group in 'runasgroup' is absent
+    ipasudorule:
+      ipaadmin_password: SomeADMINpassword
+      name: test_upstream_issue_664
+      runasgroup:
+        - wheel
+      action: member
+      state: absent
+    register: result
+    failed_when: not result.changed or result.failed
+
+  - name: Ensure sudorule member external group in 'runasgroup' is absent, again
+    ipasudorule:
+      ipaadmin_password: SomeADMINpassword
+      name: test_upstream_issue_664
+      runasgroup:
+        - wheel
+      action: member
+      state: absent
+    register: result
+    failed_when: result.changed or result.failed
+
+  - name: Ensure sudorule member external group in 'runasgroup' is present
+    ipasudorule:
+      ipaadmin_password: SomeADMINpassword
+      name: test_upstream_issue_664
+      runasgroup:
+        - wheel
+      action: member
+    register: result
+    failed_when: not result.changed or result.failed
+
+  - name: Ensure sudorule member external group in 'runasgroup' is present, again
+    ipasudorule:
+      ipaadmin_password: SomeADMINpassword
+      name: test_upstream_issue_664
+      runasgroup:
+        - wheel
+      action: member
+    register: result
+    failed_when: result.changed or result.failed
+
+  - name: Ensure sudorule 'test_upstream_issue_664' is absent
+    ipasudorule:
+      ipaadmin_password: SomeADMINpassword
+      name: test_upstream_issue_664
+      state: absent
+    register: result
+    failed_when: not result.changed or result.failed
+
   # cleanup
   - name: Ensure sudocmdgroup is absent
     ipasudocmdgroup:
@@ -777,6 +906,7 @@
       ipaadmin_password: SomeADMINpassword
       ipaapi_context: "{{ ipa_context | default(omit) }}"
       name:
+      - test_upstream_issue_664
       - testrule1
       - allusers
       - allhosts
-- 
GitLab