From 7bbd1318df28eb1fbc61de045b75b9462cdaeebd Mon Sep 17 00:00:00 2001
From: nicholasyang <nicholas.yang@suse.com>
Date: Mon, 26 Aug 2024 14:14:24 +0800
Subject: [PATCH 1/5] Fix: prun: allow pcopy_to_remote to use intercept
 (bsc#1228271)

---
 crmsh/prun/prun.py | 16 +++++++++-------
 1 file changed, 9 insertions(+), 7 deletions(-)

diff --git a/crmsh/prun/prun.py b/crmsh/prun/prun.py
index a6ecd86d0..23c6b9106 100644
--- a/crmsh/prun/prun.py
+++ b/crmsh/prun/prun.py
@@ -149,6 +149,7 @@ def pcopy_to_remote(
         *,
         timeout_seconds: int = -1,
         concurrency: int = _DEFAULT_CONCURRENCY,
+        interceptor: PRunInterceptor = PRunInterceptor(),
 ) -> typing.Dict[str, typing.Optional[PRunError]]:
     """Copy file or directory from local to remote hosts concurrently."""
     if src == dst:
@@ -176,13 +177,13 @@ def pcopy_to_remote(
             tasks = [_build_copy_task("-S '{}'".format(ssh.name), script, host) for host in hosts]
         runner = Runner(concurrency)
         for task in tasks:
-            runner.add_task(task)
+            runner.add_task(interceptor.task(task))
         runner.run(timeout_seconds)
     finally:
         if ssh is not None:
             os.unlink(ssh.name)
             ssh.close()
-    return {task.context['host']: _parse_copy_result(task) for task in tasks}
+    return {task.context['host']: _parse_copy_result(task, interceptor) for task in tasks}
 
 
 def _build_copy_task(ssh: str, script: str, host: str):
@@ -202,13 +203,13 @@ def _build_copy_task(ssh: str, script: str, host: str):
     )
 
 
-def _parse_copy_result(task: Task) -> typing.Optional[PRunError]:
+def _parse_copy_result(task: Task, interceptor: PRunInterceptor) -> typing.Optional[PRunError]:
     if task.returncode == 0:
         return None
     elif task.returncode == 255:
-        return SSHError(task.context['ssh_user'], task.context['host'], Utils.decode_str(task.stdout))
+        return interceptor.exception(SSHError(task.context['ssh_user'], task.context['host'], Utils.decode_str(task.stdout)))
     else:
-        return PRunError(task.context['ssh_user'], task.context['host'], Utils.decode_str(task.stdout))
+        return interceptor.exception(PRunError(task.context['ssh_user'], task.context['host'], Utils.decode_str(task.stdout)))
 
 
 def pfetch_from_remote(
@@ -217,6 +218,7 @@ def pfetch_from_remote(
         recursive=False,
         *,
         concurrency: int = _DEFAULT_CONCURRENCY,
+        interceptor: PRunInterceptor = PRunInterceptor(),
 ) -> typing.Dict[str, typing.Union[str, PRunError]]:
     """Copy files from remote hosts to local concurrently.
 
@@ -237,7 +239,7 @@ def pfetch_from_remote(
             tasks = [_build_fetch_task("-S '{}'".format(ssh.name), host, src, dst, flags) for host in hosts]
         runner = Runner(concurrency)
         for task in tasks:
-            runner.add_task(task)
+            runner.add_task(interceptor.task(task))
         runner.run()
     finally:
         if ssh is not None:
@@ -246,7 +248,7 @@ def pfetch_from_remote(
     basename = os.path.basename(src)
     return {
         host: v if v is not None else f"{dst}/{host}/{basename}"
-        for host, v in ((task.context['host'], _parse_copy_result(task)) for task in tasks)
+        for host, v in ((task.context['host'], _parse_copy_result(task, interceptor)) for task in tasks)
     }
 
 

From 88c4cde22fefb009c15debbe1ea1d85d90d1bae8 Mon Sep 17 00:00:00 2001
From: nicholasyang <nicholas.yang@suse.com>
Date: Tue, 27 Aug 2024 16:38:57 +0800
Subject: [PATCH 2/5] Fix: scripts: call commands as current user when running
 as hacluster (bsc#1228271)

use PRunInterceptor to modify cmdlines
---
 crmsh/scripts.py | 47 ++++++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 44 insertions(+), 3 deletions(-)

diff --git a/crmsh/scripts.py b/crmsh/scripts.py
index 3b49c90ed..2b500d711 100644
--- a/crmsh/scripts.py
+++ b/crmsh/scripts.py
@@ -13,12 +13,14 @@
 from glob import glob
 from lxml import etree
 
+from .prun.runner import Task
+
 try:
     import json
 except ImportError:
     import simplejson as json
 
-from . import config
+from . import config, constants
 from . import handles
 from . import options
 from . import userdir
@@ -1075,10 +1077,30 @@ def _check_control_persist():
     return "Bad configuration option" not in err
 
 
+class _PRunAsCurrentUserInterceptor(prun.PRunInterceptor):
+    _user = userdir.getuser()
+
+    def task(self, task: Task) -> Task:
+        if self._user != 'hacluster':
+            return task
+        elif any('ssh' in arg for arg in task.args):
+            task.args = ['ssh']
+            task.args .extend(constants.SSH_OPTION_ARGS)
+            task.args .extend([task.context['host'], '/bin/bash'])
+        else:
+            task.args = ['/bin/bash']
+        task.context['ssh_user'] = self._user
+        return task
+
+
 def _parallax_call(printer, hosts, cmd, timeout_seconds):
     "parallax.call with debug logging"
     printer.debug("parallax.call(%s, %s)" % (repr(hosts), cmd))
-    return prun.prun({host: cmd for host, _, _ in hosts}, timeout_seconds=timeout_seconds)
+    return prun.prun(
+        {host: cmd for host, _, _ in hosts},
+        interceptor=_PRunAsCurrentUserInterceptor(),
+        timeout_seconds=timeout_seconds,
+    )
 
 
 def _resolve_script(name):
@@ -1092,10 +1114,29 @@ def _resolve_script(name):
     return None
 
 
+class _PRunCopyAsCurrentUserInterceptor(prun.PRunInterceptor):
+    _user = userdir.getuser()
+
+    def task(self, task: Task) -> Task:
+        if self._user != 'hacluster':
+            return task
+        task.args = ['sftp']
+        task.args.extend(constants.SSH_OPTION_ARGS)
+        task.args.extend(['-o', 'BatchMode=yes', '-b', '-', task.context['host']])
+        task.context['remote_user'] = userdir.getuser()
+        return task
+
+
 def _parallax_copy(printer, hosts, src, dst, timeout_seconds):
     "parallax.copy with debug logging"
     printer.debug("parallax.copy(%s, %s, %s)" % (repr(hosts), src, dst))
-    return prun.pcopy_to_remote(src, [x[0] for x in hosts], dst, recursive=True, timeout_seconds=timeout_seconds)
+    return prun.pcopy_to_remote(
+        src,
+        [x[0] for x in hosts],
+        dst,
+        recursive=True, timeout_seconds=timeout_seconds,
+        interceptor=_PRunCopyAsCurrentUserInterceptor(),
+    )
 
 
 def _tempname(prefix):

From 462fa3968ccb47686b37cc106673a9930eb2df9f Mon Sep 17 00:00:00 2001
From: nicholasyang <nicholas.yang@suse.com>
Date: Mon, 26 Aug 2024 14:17:43 +0800
Subject: [PATCH 3/5] Fix: utils: allow haclient group to use cluster level
 commands (bsc#1228271)

This only allows those command at crmsh level and traditional unix DAC still applies.
---
 crmsh/utils.py | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/crmsh/utils.py b/crmsh/utils.py
index 14e6ca7c7..ec082f702 100644
--- a/crmsh/utils.py
+++ b/crmsh/utils.py
@@ -3021,9 +3021,7 @@ def check_user_access(level_name):
     Check current user's privilege and give hints to user
     """
     current_user = userdir.getuser()
-    if current_user == "root":
-        return
-    if level_name != "cluster" and in_haclient():
+    if current_user == "root" or in_haclient():
         return
 
     if not has_sudo_access():

From 2af4399046466f213fa5b8ee3a0cabb50ec8f3b6 Mon Sep 17 00:00:00 2001
From: nicholasyang <nicholas.yang@suse.com>
Date: Mon, 26 Aug 2024 14:19:48 +0800
Subject: [PATCH 4/5] Fix: completers: deferring calls to crm_mon until
 envsetup() is called (bsc#1228271)

---
 crmsh/completers.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/crmsh/completers.py b/crmsh/completers.py
index 1a4c1269a..a7b98833a 100644
--- a/crmsh/completers.py
+++ b/crmsh/completers.py
@@ -70,8 +70,8 @@ def primitives(args):
 
 
 nodes = call(xmlutil.listnodes)
-online_nodes = call(xmlutil.CrmMonXmlParser().get_node_list, "online")
-standby_nodes = call(xmlutil.CrmMonXmlParser().get_node_list, "standby")
+online_nodes = call(lambda x: xmlutil.CrmMonXmlParser().get_node_list(x), "online")
+standby_nodes = call(lambda x: xmlutil.CrmMonXmlParser().get_node_list(x), "standby")
 
 shadows = call(xmlutil.listshadows)
 

From c50a0c4b0a5f45f3c1fd58bfa77937335e0674f8 Mon Sep 17 00:00:00 2001
From: nicholasyang <nicholas.yang@suse.com>
Date: Tue, 27 Aug 2024 17:52:07 +0800
Subject: [PATCH 5/5] Dev: behave: update functional tests for previous changes

---
 test/features/user_access.feature | 17 ++---------------
 1 file changed, 2 insertions(+), 15 deletions(-)

diff --git a/test/features/user_access.feature b/test/features/user_access.feature
index 180dd3f41..d85a7c1bc 100644
--- a/test/features/user_access.feature
+++ b/test/features/user_access.feature
@@ -5,25 +5,12 @@ Feature: Functional test for user access
 
   Scenario: User in haclient group
     Given   Cluster service is "stopped" on "hanode1"
-    When    Run "useradd -m -s /bin/bash -N -g 90 xin1" on "hanode1"
+    When    Run "useradd -m -s /bin/bash -N -g haclient xin1" on "hanode1"
     When    Run "echo 'export PATH=$PATH:/usr/sbin/' >> ~xin1/.bashrc" on "hanode1"
-    When    Try "su - xin1 -c 'crm cluster init -y'"
-    Then    Except multiple lines
-      """
-      ERROR: Please run this command starting with "sudo".
-      Currently, this command needs to use sudo to escalate itself as root.
-      Please consider to add "xin1" as sudoer. For example:
-        sudo bash -c 'echo "xin1 ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/xin1'
-      """
     When    Run "echo "xin1 ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/xin1" on "hanode1"
-    When    Try "su - xin1 -c 'crm cluster init -y'"
-    Then    Except multiple lines
-      """
-      ERROR: Please run this command starting with "sudo"
-      """
     When    Run "su - xin1 -c 'sudo crm cluster init -y'" on "hanode1"
     Then    Cluster service is "started" on "hanode1"
-
+    And     Run "su - hacluster -c 'crm script run health'" OK on "hanode1"
     When    Run "su - xin1 -c 'crm node standby hanode1'" on "hanode1"
     Then    Node "hanode1" is standby
 
