Skip to content

Commit 85f9c21

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. Extend BareClient with BareClientTopologyController that setups local domain using either files-provider or proxy files.
1 parent 9c026fb commit 85f9c21

File tree

4 files changed

+213
-2
lines changed

4 files changed

+213
-2
lines changed

sssd_test_framework/roles/client.py

Lines changed: 32 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 LocalUsersUtils, LocalUser, LocalGroup, LocalSudoRule
1414
from ..utils.realmd import RealmUtils
1515
from ..utils.sbus import DBUSDestination, DBUSKnownBus
1616
from ..utils.smartcard import SmartCardUtils
@@ -156,3 +156,34 @@ 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+
return LocalUser(self.local, name)
170+
171+
def group(self, name: str) -> LocalGroup:
172+
"""
173+
Get group object.
174+
:param name: Group name.
175+
:type name: str
176+
:return: New group object.
177+
:rtype: LocalGroup
178+
"""
179+
return LocalGroup(self.local, name)
180+
181+
def sudorule(self, name: str) -> LocalSudoRule:
182+
"""
183+
Get sudo rule object.
184+
:param name: Sudo rule name.
185+
:type name: str
186+
:return: New sudo rule object.
187+
:rtype: LocalSudoRule
188+
"""
189+
return LocalSudoRule(self.local, name)

sssd_test_framework/topology.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from .config import SSSDTopologyMark
1111
from .topology_controllers import (
1212
ADTopologyController,
13+
BareClientTopologyController,
1314
ClientTopologyController,
1415
GDMTopologyController,
1516
IPATopologyController,
@@ -45,7 +46,7 @@ def test_ldap(client: Client, ldap: LDAP):
4546
BareClient = SSSDTopologyMark(
4647
name="bare_client",
4748
topology=Topology(TopologyDomain("sssd", client=1)),
48-
controller=ClientTopologyController(),
49+
controller=BareClientTopologyController(),
4950
fixtures=dict(client="sssd.client[0]", provider="sssd.client[0]"),
5051
)
5152
"""

sssd_test_framework/topology_controllers.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,11 @@
1010
from .hosts.keycloak import KeycloakHost
1111
from .hosts.samba import SambaHost
1212
from .misc.ssh import retry_command
13+
from .utils.authselect import AuthselectUtils
14+
from .utils.sssd import SSSDUtils
1315

1416
__all__ = [
17+
"BareClientTopologyController",
1518
"LDAPTopologyController",
1619
"IPATopologyController",
1720
"ADTopologyController",
@@ -58,6 +61,22 @@ class ClientTopologyController(ProvisionedBackupTopologyController):
5861
pass
5962

6063

64+
class BareClientTopologyController(BackupTopologyController[SSSDMultihostConfig]):
65+
"""
66+
Bare Client Topology Controller.
67+
"""
68+
69+
def topology_setup(self, client: ClientHost) -> None:
70+
sssd = SSSDUtils(client, client.fs, client.svc, AuthselectUtils(client), load_config=False)
71+
if client.features["files-provider"]:
72+
# Use sssd with files provider when available
73+
sssd.sssd["enable_files_domain"] = "true"
74+
else:
75+
# Use sssd with proxy-files provider
76+
sssd.common.local()
77+
sssd.start()
78+
79+
6180
class LDAPTopologyController(ProvisionedBackupTopologyController):
6281
"""
6382
LDAP Topology Controller.

sssd_test_framework/utils/local_users.py

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"Managing local users and groups."
22

33
from __future__ import annotations
4+
from typing import Any
45

56
import jc
67
from pytest_mh import MultihostHost, MultihostUtility
@@ -12,6 +13,7 @@
1213
"LocalGroup",
1314
"LocalUser",
1415
"LocalUsersUtils",
16+
"LocalSudoRule",
1517
]
1618

1719

@@ -35,6 +37,7 @@ def __init__(self, host: MultihostHost, fs: LinuxFileSystem) -> None:
3537
self.fs: LinuxFileSystem = fs
3638
self._users: list[str] = []
3739
self._groups: list[str] = []
40+
self._sudorules: list[LocalSudoRule] = []
3841

3942
def teardown(self) -> None:
4043
"""
@@ -53,6 +56,9 @@ def teardown(self) -> None:
5356
if cmd:
5457
self.host.conn.run("set -e\n\n" + cmd)
5558

59+
for rule in self._sudorules:
60+
rule.delete()
61+
5662
super().teardown()
5763

5864
def user(self, name: str) -> LocalUser:
@@ -421,3 +427,157 @@ def remove_members(self, members: list[LocalUser]) -> LocalGroup:
421427
self.util.host.conn.run("set -ex\n" + cmd, log_level=ProcessLogLevel.Error)
422428

423429
return self
430+
431+
432+
class LocalSudoRule(object):
433+
"""
434+
Local sudo rule management.
435+
"""
436+
437+
def __init__(self, util: LocalUsersUtils, name: str) -> None:
438+
"""
439+
:param util: LocalUsersUtils util object.
440+
:param name: Sudo rule name.
441+
:type name: str
442+
"""
443+
self.name = name
444+
self.util = util
445+
self.__rule: dict[str, Any] = dict()
446+
self.filename: str | None = None
447+
448+
def add(
449+
self,
450+
*,
451+
user: str | LocalUser | LocalGroup | list[str | LocalUser | LocalGroup] | None = None,
452+
host: str | list[str] | None = None,
453+
command: str | list[str] | None = None,
454+
option: str | list[str] | None = None,
455+
runasuser: str | LocalUser | LocalGroup | list[str | LocalUser | LocalGroup] | None = None,
456+
runasgroup: str | LocalGroup | list[str | LocalGroup] | None = None,
457+
order: int | None = None,
458+
nopasswd: bool | None = None,
459+
) -> LocalSudoRule:
460+
"""
461+
Create new sudo rule.
462+
463+
:param user: sudoUser attribute, defaults to None
464+
:type user: str | LocalUser | LocalGroup | list[str | LocalUser | LocalGroup] | None, optional
465+
:param host: sudoHost attribute, defaults to None
466+
:type host: str | list[str] | None, optional
467+
:param command: sudoCommand attribute, defaults to None
468+
:type command: str | list[str] | None, optional
469+
:param option: sudoOption attribute, defaults to None
470+
:type option: str | list[str] | None, optional
471+
:param runasuser: sudoRunAsUser attribute, defaults to None
472+
:type runasuser: str | LocalUser | list[str | LocalUser] | None, optional
473+
:param runasgroup: sudoRunAsGroup attribute, defaults to None
474+
:type runasgroup: str | LocalGroup | list[str | LocalGroup] | None, optional
475+
:param order: sudoOrder attribute, defaults to None
476+
:type order: int | None, optional
477+
:param nopasswd: If true, no authentication is required (NOPASSWD), defaults to None (no change)
478+
:type nopasswd: bool | None, optional
479+
:return: _description_
480+
:rtype: LocalSudoRule
481+
"""
482+
orderstr = f"{order:02d}" if order else str(len(self.util._sudorules))
483+
self.filename = f"{orderstr}_{self.name}"
484+
# Remember arguments so we can use them in modify if needed
485+
self.__rule = dict(
486+
user=user,
487+
host=host,
488+
command=command,
489+
option=option,
490+
runasuser=runasuser,
491+
runasgroup=runasgroup,
492+
order=order,
493+
nopasswd=nopasswd,
494+
)
495+
run_as_str = ""
496+
if runasuser or runasgroup:
497+
run_as_str += "("
498+
if runasuser:
499+
if isinstance(runasuser, list):
500+
runasuser_str = ", ".join(runasuser)
501+
else:
502+
runasuser_str = runasuser
503+
run_as_str += f"{runasuser_str}"
504+
if runasgroup:
505+
if isinstance(runasgroup, list):
506+
runasgroup_str = ", ".join(runasgroup)
507+
else:
508+
runasgroup_str = runasgroup
509+
run_as_str += f":{runasgroup_str}"
510+
run_as_str += ")"
511+
if isinstance(user, list):
512+
user_str = ", ".join(user)
513+
else:
514+
user_str = user if user else ""
515+
if isinstance(host, list):
516+
host_str = ", ".join(host)
517+
else:
518+
host_str = f"{host}=" if host else "="
519+
tagspec_str = ""
520+
if nopasswd:
521+
tagspec_str = "NOPASSWD:"
522+
if isinstance(command, list):
523+
command_str = ", ".join(command)
524+
else:
525+
command_str = command if command else ""
526+
rule_str = f"{user_str} {host_str}{run_as_str} {tagspec_str} {command_str} \n"
527+
self.util.fs.write(f"/etc/sudoers.d/{self.filename}", rule_str)
528+
self.util._sudorules.append(self)
529+
return self
530+
531+
def modify(
532+
self,
533+
*,
534+
user: str | LocalUser | LocalGroup | list[str | LocalUser | LocalGroup] | None = None,
535+
host: str | list[str] | None = None,
536+
command: str | list[str] | None = None,
537+
option: str | list[str] | None = None,
538+
runasuser: str | LocalUser | LocalGroup | list[str | LocalUser | LocalGroup] | None = None,
539+
runasgroup: str | LocalGroup | list[str | LocalGroup] | None = None,
540+
order: int | None = None,
541+
nopasswd: bool | None = None,
542+
) -> LocalSudoRule:
543+
"""
544+
Modify existing Local sudo rule.
545+
546+
:param user: sudoUser attribute, defaults to None
547+
:type user: str | LocalUser | LocalGroup | list[str | LocalUser | LocalGroup] | None, optional
548+
:param host: sudoHost attribute, defaults to None
549+
:type host: str | list[str] | None, optional
550+
:param command: sudoCommand attribute, defaults to None
551+
:type command: str | list[str] | None, optional
552+
:param option: sudoOption attribute, defaults to None
553+
:type option: str | list[str] | None, optional
554+
:param runasuser: sudoRunAsUser attribute, defaults to None
555+
:type runasuser: str | LocalUser | LocalGroup | list[str | LocalUser | LocalGroup] | None, optional
556+
:param runasgroup: sudoRunAsGroup attribute, defaults to None
557+
:type runasgroup: str | LocalGroup | list[str | LocalGroup] | None, optional
558+
:param order: sudoOrder attribute, defaults to None
559+
:type order: int | None, optional
560+
:param nopasswd: If true, no authentication is required (NOPASSWD), defaults to None (no change)
561+
:type nopasswd: bool | None, optional
562+
:return: _description_
563+
:rtype: LocalSudoRule
564+
"""
565+
self.delete()
566+
self.add(
567+
user=user if user is not None else self.__rule.get("user", None),
568+
host=host if host is not None else self.__rule.get("host", None),
569+
command=command if command is not None else self.__rule.get("command", None),
570+
option=option if option is not None else self.__rule.get("option", None),
571+
runasuser=runasuser if runasuser is not None else self.__rule.get("runasuser", None),
572+
runasgroup=runasgroup if runasgroup is not None else self.__rule.get("runasgroup", None),
573+
order=order if order is not None else self.__rule.get("order", None),
574+
nopasswd=nopasswd if nopasswd is not None else self.__rule.get("nopasswd", None),
575+
)
576+
return self
577+
578+
def delete(self) -> None:
579+
"""
580+
Delete local sudo rule.
581+
"""
582+
self.util.fs.rm(f"/etc/sudoers.d/{self.filename}")
583+
self.util._sudorules.remove(self)

0 commit comments

Comments
 (0)