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