22
33from __future__ import annotations
44
5+ from typing import Any
6+
57import jc
68from pytest_mh import MultihostHost , MultihostUtility
79from pytest_mh .cli import CLIBuilder , CLIBuilderArgs
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