Skip to content

Commit 5945ef1

Browse files
feature: Implement provider interface in client role
Allow Client to be used as provider by implementing user, group, sudorule interfaces. Implement LocalSudoRule class. Add version comparison method to BaseLinuxHost.
1 parent 5aad53d commit 5945ef1

File tree

4 files changed

+292
-1
lines changed

4 files changed

+292
-1
lines changed

sssd_test_framework/hosts/base.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,3 +314,34 @@ def get_package_version(self, package: str = "sssd", raise_on_error: bool = True
314314
vers["update"] = int(v_match.group(5)) if v_match.group(5) else 0
315315
vers["release"] = v_match.group(6) if v_match.group(6) else ""
316316
return vers
317+
318+
def compare_package_version(self, other_version: dict, package: str = "sssd") -> int:
319+
"""
320+
Compare installed package version with other version.
321+
322+
:param other_version: Version dictionary to compare
323+
keys: major, minor, patch, prerelease, update, release
324+
:param package: Package name (default: sssd)
325+
:return: -1 if installed < other, 0 if equal, 1 if installed > other
326+
"""
327+
328+
def version_tuple(ver):
329+
# Compose a tuple for comparable versioning, keeping prerelease (str) sortable
330+
return (
331+
ver.get("major", 0),
332+
ver.get("minor", 0),
333+
ver.get("patch", 0),
334+
ver.get("prerelease", ""),
335+
ver.get("update", 0),
336+
ver.get("release", ""),
337+
)
338+
339+
installed_ver = self.get_package_version(package)
340+
t_installed = version_tuple(installed_ver)
341+
t_other = version_tuple(other_version)
342+
if t_installed < t_other:
343+
return -1
344+
elif t_installed > t_other:
345+
return 1
346+
else:
347+
return 0

sssd_test_framework/roles/client.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from ..utils.automount import AutomountUtils
1111
from ..utils.gdm import GDM
1212
from ..utils.ldb import LDBUtils
13-
from ..utils.local_users import LocalUsersUtils
13+
from ..utils.local_users import LocalGroup, LocalSudoRule, LocalUser, LocalUsersUtils
1414
from ..utils.realmd import RealmUtils
1515
from ..utils.sbus import DBUSDestination, DBUSKnownBus
1616
from ..utils.smartcard import SmartCardUtils
@@ -156,3 +156,37 @@ def sss_ssh_authorizedkeys(self, *args: str) -> ProcessResult:
156156
:rtype: ProcessResult
157157
"""
158158
return self.host.conn.exec(["sss_ssh_authorizedkeys", *args], raise_on_error=False)
159+
160+
def user(self, name: str) -> LocalUser:
161+
"""
162+
Get user object.
163+
164+
:param name: User name.
165+
:type name: str
166+
:return: New user object.
167+
:rtype: LocalUser
168+
"""
169+
170+
return LocalUser(self.local, name)
171+
172+
def group(self, name: str) -> LocalGroup:
173+
"""
174+
Get group object.
175+
:param name: Group name.
176+
:type name: str
177+
:return: New group object.
178+
:rtype: LocalGroup
179+
"""
180+
181+
return LocalGroup(self.local, name)
182+
183+
def sudorule(self, name: str) -> LocalSudoRule:
184+
"""
185+
Get sudo rule object.
186+
:param name: Sudo rule name.
187+
:type name: str
188+
:return: New sudo rule object.
189+
:rtype: LocalSudoRule
190+
"""
191+
192+
return LocalSudoRule(self.local, name)

sssd_test_framework/utils/authentication.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1100,6 +1100,38 @@ def run(self, username: str, password: str | None = None, *, command: str) -> bo
11001100

11011101
return result.rc == 0
11021102

1103+
def run_advanced(
1104+
self, username: str, password: str | None = None, *, parameters: list[str] | None = None, command: str
1105+
) -> ProcessResult:
1106+
"""
1107+
Execute sudo command with parameters.
1108+
1109+
:param username: Username that calls sudo.
1110+
:type username: str
1111+
:param password: User password, defaults to None
1112+
:type password: str | None, optional
1113+
:param parameters: List of parameters to sudo.
1114+
:type parameters: list[str] | None
1115+
:param command: Command to execute (make sure to properly escape any quotes).
1116+
:type command: str
1117+
:return: Command result.
1118+
:rtype: ProcessResult
1119+
"""
1120+
if parameters is None:
1121+
parameters = []
1122+
if password is not None:
1123+
parameters.append("--stdin")
1124+
if password is not None:
1125+
result = self.host.conn.run(
1126+
f'su - "{username}" -c "sudo {" ".join(parameters)} {command}"', input=password, raise_on_error=False
1127+
)
1128+
else:
1129+
result = self.host.conn.run(
1130+
f'su - "{username}" -c "sudo {" ".join(parameters)} {command}"', raise_on_error=False
1131+
)
1132+
1133+
return result
1134+
11031135
def list(self, username: str, password: str | None = None, *, expected: list[str] | None = None) -> bool:
11041136
"""
11051137
List commands that the user can run under sudo.

sssd_test_framework/utils/local_users.py

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
from __future__ import annotations
44

5+
from typing import Any
6+
57
import jc
68
from pytest_mh import MultihostHost, MultihostUtility
79
from pytest_mh.cli import CLIBuilder, CLIBuilderArgs
@@ -12,6 +14,7 @@
1214
"LocalGroup",
1315
"LocalUser",
1416
"LocalUsersUtils",
17+
"LocalSudoRule",
1518
]
1619

1720

@@ -35,6 +38,7 @@ def __init__(self, host: MultihostHost, fs: LinuxFileSystem) -> None:
3538
self.fs: LinuxFileSystem = fs
3639
self._users: list[str] = []
3740
self._groups: list[str] = []
41+
self._sudorules: list[LocalSudoRule] = []
3842

3943
def teardown(self) -> None:
4044
"""
@@ -53,6 +57,9 @@ def teardown(self) -> None:
5357
if cmd:
5458
self.host.conn.run("set -e\n\n" + cmd)
5559

60+
for rule in self._sudorules[:]:
61+
rule.delete()
62+
5663
super().teardown()
5764

5865
def user(self, name: str) -> LocalUser:
@@ -129,6 +136,12 @@ def __init__(self, util: LocalUsersUtils, name: str) -> None:
129136
self.util = util
130137
self.name = name
131138

139+
def __str__(self):
140+
"""
141+
Returns a string representation of the LocalUser.
142+
"""
143+
return self.name
144+
132145
def add(
133146
self,
134147
*,
@@ -276,6 +289,12 @@ def __init__(self, util: LocalUsersUtils, name: str) -> None:
276289
self.util = util
277290
self.name = name
278291

292+
def __str__(self):
293+
"""
294+
Returns a string representation of the LocalGroup.
295+
"""
296+
return self.name
297+
279298
def add(
280299
self,
281300
*,
@@ -421,3 +440,178 @@ def remove_members(self, members: list[LocalUser]) -> LocalGroup:
421440
self.util.host.conn.run("set -ex\n" + cmd, log_level=ProcessLogLevel.Error)
422441

423442
return self
443+
444+
445+
class LocalSudoRule(object):
446+
"""
447+
Local sudo rule management.
448+
"""
449+
450+
default_user: str = "ALL"
451+
default_host: str = "ALL"
452+
default_command: str = "ALL"
453+
454+
def __init__(self, util: LocalUsersUtils, name: str) -> None:
455+
"""
456+
:param util: LocalUsersUtils util object.
457+
:param name: Sudo rule name.
458+
:type name: str
459+
"""
460+
self.name = name
461+
self.util = util
462+
self.__rule: dict[str, Any] = dict()
463+
self.filename: str | None = None
464+
self.rule_str: str | None = None
465+
466+
def __str__(self):
467+
"""
468+
Returns a string representation of the LocalSudoRule.
469+
"""
470+
if self.rule_str:
471+
return self.rule_str
472+
else:
473+
return self.name
474+
475+
@staticmethod
476+
def _format_list(item: str | Any | list[str | Any], add_percent: bool = False) -> str:
477+
"""
478+
Format the item as a string.
479+
480+
:param item: object to be formatted
481+
:type item: str | Any| list[str | Any]
482+
:param add_percent: If true, prepend % to the item, defaults to False
483+
:type add_percent: bool, optional
484+
:return: Formatted string.
485+
:rtype: str
486+
"""
487+
if isinstance(item, list):
488+
result = ", ".join(f"%{str(x)}" if isinstance(x, LocalGroup) and add_percent else str(x) for x in item)
489+
else:
490+
if isinstance(item, LocalGroup) and add_percent:
491+
result = f"%{str(item)}"
492+
else:
493+
result = str(item)
494+
return result
495+
496+
def add(
497+
self,
498+
*,
499+
user: str | LocalUser | LocalGroup | list[str | LocalUser | LocalGroup] | Any | None = default_user,
500+
host: str | list[str] | Any | None = default_host,
501+
command: str | list[str] | Any | None = default_command,
502+
option: str | list[str] | None = None,
503+
runasuser: str | LocalUser | list[str | LocalUser] | None = None,
504+
runasgroup: str | LocalGroup | list[str | LocalGroup] | None = None,
505+
order: int | None = None,
506+
nopasswd: bool | None = None,
507+
) -> LocalSudoRule:
508+
"""
509+
Create new sudo rule.
510+
511+
:param user: sudoUser attribute, defaults to ALL
512+
:type user: str | LocalUser | LocalGroup | list[str | LocalUser | LocalGroup]
513+
:param host: sudoHost attribute, defaults to ALL
514+
:type host: str | list[str],
515+
:param command: sudoCommand attribute, defaults to ALL
516+
:type command: str | list[str],
517+
:param option: sudoOption attribute, defaults to None
518+
:type option: str | list[str] | None, optional
519+
:param runasuser: sudoRunAsUser attribute, defaults to None
520+
:type runasuser: str | LocalUser | list[str | LocalUser] | None, optional
521+
:param runasgroup: sudoRunAsGroup attribute, defaults to None
522+
:type runasgroup: str | LocalGroup | list[str | LocalGroup] | None, optional
523+
:param order: sudoOrder attribute, defaults to None
524+
:type order: int | None, optional
525+
:param nopasswd: If true, no authentication is required (NOPASSWD), defaults to None (no change)
526+
:type nopasswd: bool | None, optional
527+
:return: New sudo rule object.
528+
:rtype: LocalSudoRule
529+
"""
530+
orderstr = f"{order:02d}" if order is not None else str(len(self.util._sudorules))
531+
if self.filename is None:
532+
self.filename = f"{orderstr}_{self.name}"
533+
534+
# Remember arguments so we can use them in modify if needed
535+
self.__rule = dict[str, Any](
536+
user=user,
537+
host=host,
538+
command=command,
539+
option=option,
540+
runasuser=runasuser,
541+
runasgroup=runasgroup,
542+
order=order,
543+
nopasswd=nopasswd,
544+
)
545+
run_as_str = ""
546+
if runasuser or runasgroup:
547+
run_as_str += "("
548+
if runasuser:
549+
run_as_str += LocalSudoRule._format_list(runasuser)
550+
if runasgroup:
551+
run_as_str += f":{LocalSudoRule._format_list(runasgroup)}"
552+
run_as_str += ")"
553+
user_str = LocalSudoRule._format_list(user, add_percent=True)
554+
host_str = LocalSudoRule._format_list(host)
555+
tagspec_str = "NOPASSWD:" if nopasswd else ""
556+
command_str = LocalSudoRule._format_list(command)
557+
rule_str = f"{user_str} {host_str}={run_as_str} {tagspec_str} {command_str}\n"
558+
self.rule_str = rule_str
559+
self.util.fs.write(f"/etc/sudoers.d/{self.filename}", self.rule_str)
560+
self.util._sudorules.append(self)
561+
return self
562+
563+
def modify(
564+
self,
565+
*,
566+
user: str | LocalUser | LocalGroup | list[str | LocalUser | LocalGroup] | None = None,
567+
host: str | list[str] | None = None,
568+
command: str | list[str] | None = None,
569+
option: str | list[str] | None = None,
570+
runasuser: str | LocalUser | list[str | LocalUser] | None = None,
571+
runasgroup: str | LocalGroup | list[str | LocalGroup] | None = None,
572+
order: int | None = None,
573+
nopasswd: bool | None = None,
574+
) -> LocalSudoRule:
575+
"""
576+
Modify existing Local sudo rule.
577+
578+
:param user: sudoUser attribute, defaults to None
579+
:type user: str | LocalUser | LocalGroup | list[str | LocalUser | LocalGroup] | None, optional
580+
:param host: sudoHost attribute, defaults to None
581+
:type host: str | list[str] | None, optional
582+
:param command: sudoCommand attribute defaults to None
583+
:type command: str | list[str] | None, optional
584+
:param option: sudoOption attribute, defaults to None
585+
:type option: str | list[str] | None, optional
586+
:param runasuser: sudoRunAsUser attribute, defaults to None
587+
:type runasuser: str | LocalUser | list[str | LocalUser] | None, optional
588+
:param runasgroup: sudoRunAsGroup attribute, defaults to None
589+
:type runasgroup: str | LocalGroup | list[str | LocalGroup] | None, optional
590+
:param order: sudoOrder attribute, defaults to None
591+
:type order: int | None, optional
592+
:param nopasswd: If true, no authentication is required (NOPASSWD), defaults to None (no change)
593+
:type nopasswd: bool | None, optional
594+
:return: New sudo rule object.
595+
:rtype: LocalSudoRule
596+
"""
597+
self.delete()
598+
self.add(
599+
user=user if user is not None else self.__rule.get("user"),
600+
host=host if host is not None else self.__rule.get("host"),
601+
command=command if command is not None else self.__rule.get("command"),
602+
option=option if option is not None else self.__rule.get("option"),
603+
runasuser=runasuser if runasuser is not None else self.__rule.get("runasuser"),
604+
runasgroup=runasgroup if runasgroup is not None else self.__rule.get("runasgroup"),
605+
order=order if order is not None else self.__rule.get("order"),
606+
nopasswd=nopasswd if nopasswd is not None else self.__rule.get("nopasswd"),
607+
)
608+
return self
609+
610+
def delete(self) -> None:
611+
"""
612+
Delete local sudo rule.
613+
"""
614+
if self.filename:
615+
self.util.fs.rm(f"/etc/sudoers.d/{self.filename}")
616+
if self in self.util._sudorules:
617+
self.util._sudorules.remove(self)

0 commit comments

Comments
 (0)