Skip to content

Commit bd42ff8

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 bd42ff8

File tree

4 files changed

+239
-2
lines changed

4 files changed

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

0 commit comments

Comments
 (0)