The branch, master has been updated via 85d24068265 samba-tool: Add a gpo command for removing VGP Startup Scripts Group Policy via 91655e6d713 samba-tool: Test gpo manage script startup remove command via e5efe17246d samba-tool: Add a gpo command for adding VGP Startup Scripts Group Policy via f6a0bd8b913 samba-tool: Test gpo manage script startup add command via d22196117cd samba-tool: Add a gpo command for listing VGP Startup Scripts Group Policy via 329b6c397b9 samba-tool: Test gpo manage script startup list command via 15cec2ac4d7 gpo: Apply Group Policy Startup Scripts from VGP via b13b2d8c3ec gpo: Test Group Policy VGP Startup Script Policy from e49a0b444ab ldb: remove some 'if PY3's in tests
https://git.samba.org/?p=samba.git;a=shortlog;h=master - Log ----------------------------------------------------------------- commit 85d240682655cdcdcac22606178a257ac28d7783 Author: David Mulder <dmul...@suse.com> Date: Tue Feb 16 14:12:02 2021 -0700 samba-tool: Add a gpo command for removing VGP Startup Scripts Group Policy Signed-off-by: David Mulder <dmul...@suse.com> Reviewed-by: Jeremy Allison <j...@samba.org> Autobuild-User(master): Jeremy Allison <j...@samba.org> Autobuild-Date(master): Wed Feb 24 22:01:08 UTC 2021 on sn-devel-184 commit 91655e6d713cbd6b555ae43a8468f38ba238387e Author: David Mulder <dmul...@suse.com> Date: Fri Feb 12 14:49:16 2021 -0700 samba-tool: Test gpo manage script startup remove command Signed-off-by: David Mulder <dmul...@suse.com> Reviewed-by: Jeremy Allison <j...@samba.org> commit e5efe17246d1ef6b1aab84901a98660845a50bb1 Author: David Mulder <dmul...@suse.com> Date: Fri Feb 12 14:13:51 2021 -0700 samba-tool: Add a gpo command for adding VGP Startup Scripts Group Policy Signed-off-by: David Mulder <dmul...@suse.com> Reviewed-by: Jeremy Allison <j...@samba.org> commit f6a0bd8b91302f4c7a9b1392d92bdaafcd1bcebd Author: David Mulder <dmul...@suse.com> Date: Fri Feb 12 08:04:30 2021 -0700 samba-tool: Test gpo manage script startup add command Signed-off-by: David Mulder <dmul...@suse.com> Reviewed-by: Jeremy Allison <j...@samba.org> commit d22196117cd3f87ca1e3790013909d6e72fdb942 Author: David Mulder <dmul...@suse.com> Date: Tue Feb 9 06:16:32 2021 -0700 samba-tool: Add a gpo command for listing VGP Startup Scripts Group Policy Signed-off-by: David Mulder <dmul...@suse.com> Reviewed-by: Jeremy Allison <j...@samba.org> commit 329b6c397b988839d73e18968dbdec855a471877 Author: David Mulder <dmul...@suse.com> Date: Mon Feb 8 13:08:02 2021 -0700 samba-tool: Test gpo manage script startup list command Signed-off-by: David Mulder <dmul...@suse.com> Reviewed-by: Jeremy Allison <j...@samba.org> commit 15cec2ac4d7af0fa82c21d0109607aa63c86c15a Author: David Mulder <dmul...@suse.com> Date: Tue Feb 2 12:33:11 2021 -0700 gpo: Apply Group Policy Startup Scripts from VGP Signed-off-by: David Mulder <dmul...@suse.com> Reviewed-by: Jeremy Allison <j...@samba.org> commit b13b2d8c3ec2a64ee56ce03bc9130b628a4c0fcb Author: David Mulder <dmul...@suse.com> Date: Fri Jan 29 13:34:50 2021 -0700 gpo: Test Group Policy VGP Startup Script Policy Signed-off-by: David Mulder <dmul...@suse.com> Reviewed-by: Jeremy Allison <j...@samba.org> ----------------------------------------------------------------------- Summary of changes: docs-xml/manpages/samba-tool.8.xml | 15 ++ python/samba/netcmd/gpo.py | 279 ++++++++++++++++++++++++++++++++ python/samba/tests/gpo.py | 122 ++++++++++++++ python/samba/tests/samba_tool/gpo.py | 103 ++++++++++++ python/samba/vgp_startup_scripts_ext.py | 124 ++++++++++++++ 5 files changed, 643 insertions(+) create mode 100644 python/samba/vgp_startup_scripts_ext.py Changeset truncated at 500 lines: diff --git a/docs-xml/manpages/samba-tool.8.xml b/docs-xml/manpages/samba-tool.8.xml index e7ac7dd625a..b808c9b2616 100644 --- a/docs-xml/manpages/samba-tool.8.xml +++ b/docs-xml/manpages/samba-tool.8.xml @@ -939,6 +939,21 @@ <para>Removes a Samba Sudoers Group Policy from the sysvol.</para> </refsect3> +<refsect3> + <title>gpo manage scripts startup list</title> + <para>List VGP Startup Script Group Policy from the sysvol</para> +</refsect3> + +<refsect3> + <title>gpo manage scripts startup add</title> + <para>Adds VGP Startup Script Group Policy to the sysvol</para> +</refsect3> + +<refsect3> + <title>gpo manage scripts startup remove</title> + <para>Removes VGP Startup Script Group Policy from the sysvol</para> +</refsect3> + <refsect2> <title>group</title> <para>Manage groups.</para> diff --git a/python/samba/netcmd/gpo.py b/python/samba/netcmd/gpo.py index 24ecf664ecf..2928ebcf7ce 100644 --- a/python/samba/netcmd/gpo.py +++ b/python/samba/netcmd/gpo.py @@ -69,6 +69,7 @@ from samba.common import get_bytes, get_string from configparser import ConfigParser from io import StringIO, BytesIO from samba.vgp_files_ext import calc_mode, stat_from_mode +import hashlib def gpo_flags_string(value): @@ -3039,6 +3040,283 @@ class cmd_openssh(SuperCommand): subcommands["list"] = cmd_list_openssh() subcommands["set"] = cmd_set_openssh() +class cmd_list_startup(Command): + """List VGP Startup Script Group Policy from the sysvol + +This command lists the startup script policies currently set on the sysvol. + +Example: +samba-tool gpo manage scripts startup list {31B2F340-016D-11D2-945F-00C04FB984F9} + """ + + synopsis = "%prog <gpo> [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_options = [ + Option("-H", "--URL", help="LDB URL for database or target server", type=str, + metavar="URL", dest="H"), + ] + + takes_args = ["gpo"] + + def run(self, gpo, H=None, sambaopts=None, credopts=None, versionopts=None): + self.lp = sambaopts.get_loadparm() + self.creds = credopts.get_credentials(self.lp, fallback_machine=True) + + # We need to know writable DC to setup SMB connection + if H and H.startswith('ldap://'): + dc_hostname = H[7:] + self.url = H + else: + dc_hostname = netcmd_finddc(self.lp, self.creds) + self.url = dc_url(self.lp, self.creds, dc=dc_hostname) + + # SMB connect to DC + conn = smb_connection(dc_hostname, + 'sysvol', + lp=self.lp, + creds=self.creds) + + realm = self.lp.get('realm') + vgp_xml = '\\'.join([realm.lower(), 'Policies', gpo, + 'MACHINE\\VGP\\VTLA\\Unix', + 'Scripts\\Startup\\manifest.xml']) + try: + xml_data = ET.fromstring(conn.loadfile(vgp_xml)) + except NTSTATUSError as e: + # STATUS_OBJECT_NAME_INVALID, STATUS_OBJECT_NAME_NOT_FOUND, + # STATUS_OBJECT_PATH_NOT_FOUND + if e.args[0] in [0xC0000033, 0xC0000034, 0xC000003A]: + return # The file doesn't exist, so there is nothing to list + if e.args[0] == 0xC0000022: # STATUS_ACCESS_DENIED + raise CommandError("The authenticated user does " + "not have sufficient privileges") + raise + + policy = xml_data.find('policysetting') + data = policy.find('data') + for listelement in data.findall('listelement'): + script = listelement.find('script') + script_path = '\\'.join(['\\', realm.lower(), 'Policies', gpo, + 'MACHINE\\VGP\\VTLA\\Unix\\Scripts', + 'Startup', script.text]) + parameters = listelement.find('parameters') + run_as = listelement.find('run_as') + if run_as is not None: + run_as = run_as.text + else: + run_as = 'root' + self.outf.write('@reboot %s %s %s' % (run_as, script_path, + parameters.text)) + +class cmd_add_startup(Command): + """Adds VGP Startup Script Group Policy to the sysvol + +This command adds a startup script policy to the sysvol. + +Example: +samba-tool gpo manage scripts startup add {31B2F340-016D-11D2-945F-00C04FB984F9} test_script.sh '-n' + """ + + synopsis = "%prog <gpo> <script> [args] [run_as] [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_options = [ + Option("-H", "--URL", help="LDB URL for database or target server", type=str, + metavar="URL", dest="H"), + Option("--run-once", dest="run_once", default=False, action='store_true', + help="Whether to run the script only once"), + ] + + takes_args = ["gpo", "script", "args?", "run_as?"] + + def run(self, gpo, script, args=None, run_as=None, run_once=None, + H=None, sambaopts=None, credopts=None, versionopts=None): + self.lp = sambaopts.get_loadparm() + self.creds = credopts.get_credentials(self.lp, fallback_machine=True) + + if not os.path.exists(script): + raise CommandError("Script '%s' does not exist" % script) + + # We need to know writable DC to setup SMB connection + if H and H.startswith('ldap://'): + dc_hostname = H[7:] + self.url = H + else: + dc_hostname = netcmd_finddc(self.lp, self.creds) + self.url = dc_url(self.lp, self.creds, dc=dc_hostname) + + # SMB connect to DC + conn = smb_connection(dc_hostname, + 'sysvol', + lp=self.lp, + creds=self.creds) + + realm = self.lp.get('realm') + vgp_dir = '\\'.join([realm.lower(), 'Policies', gpo, + 'MACHINE\\VGP\\VTLA\\Unix\\Scripts\\Startup']) + vgp_xml = '\\'.join([vgp_dir, 'manifest.xml']) + try: + xml_data = ET.ElementTree(ET.fromstring(conn.loadfile(vgp_xml))) + policy = xml_data.getroot().find('policysetting') + data = policy.find('data') + except NTSTATUSError as e: + # STATUS_OBJECT_NAME_INVALID, STATUS_OBJECT_NAME_NOT_FOUND, + # STATUS_OBJECT_PATH_NOT_FOUND + if e.args[0] in [0xC0000033, 0xC0000034, 0xC000003A]: + # The file doesn't exist, so create the xml structure + xml_data = ET.ElementTree(ET.Element('vgppolicy')) + policysetting = ET.SubElement(xml_data.getroot(), + 'policysetting') + pv = ET.SubElement(policysetting, 'version') + pv.text = '1' + name = ET.SubElement(policysetting, 'name') + name.text = 'Unix Scripts' + description = ET.SubElement(policysetting, 'description') + description.text = \ + 'Represents Unix scripts to run on Group Policy clients' + data = ET.SubElement(policysetting, 'data') + elif e.args[0] == 0xC0000022: # STATUS_ACCESS_DENIED + raise CommandError("The authenticated user does " + "not have sufficient privileges") + else: + raise + + script_data = open(script, 'rb').read() + listelement = ET.SubElement(data, 'listelement') + script_elm = ET.SubElement(listelement, 'script') + script_elm.text = os.path.basename(script) + hash = ET.SubElement(listelement, 'hash') + hash.text = hashlib.md5(script_data).hexdigest().upper() + if args is not None: + parameters = ET.SubElement(listelement, 'parameters') + parameters.text = args.strip('"').strip("'") + if run_as is not None: + run_as_elm = ET.SubElement(listelement, 'run_as') + run_as_elm.text = run_as + if run_once is not None: + ET.SubElement(listelement, 'run_once') + + out = BytesIO() + xml_data.write(out, encoding='UTF-8', xml_declaration=True) + out.seek(0) + sysvol_script = '\\'.join([vgp_dir, os.path.basename(script)]) + try: + create_directory_hier(conn, vgp_dir) + conn.savefile(vgp_xml, out.read()) + conn.savefile(sysvol_script, script_data) + except NTSTATUSError as e: + if e.args[0] == 0xC0000022: # STATUS_ACCESS_DENIED + raise CommandError("The authenticated user does " + "not have sufficient privileges") + raise + +class cmd_remove_startup(Command): + """Removes VGP Startup Script Group Policy from the sysvol + +This command removes a startup script policy from the sysvol. + +Example: +samba-tool gpo manage scripts startup remove {31B2F340-016D-11D2-945F-00C04FB984F9} test_script.sh + """ + + synopsis = "%prog <gpo> <script> [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_options = [ + Option("-H", "--URL", help="LDB URL for database or target server", type=str, + metavar="URL", dest="H"), + ] + + takes_args = ["gpo", "script"] + + def run(self, gpo, script, H=None, sambaopts=None, credopts=None, + versionopts=None): + self.lp = sambaopts.get_loadparm() + self.creds = credopts.get_credentials(self.lp, fallback_machine=True) + + # We need to know writable DC to setup SMB connection + if H and H.startswith('ldap://'): + dc_hostname = H[7:] + self.url = H + else: + dc_hostname = netcmd_finddc(self.lp, self.creds) + self.url = dc_url(self.lp, self.creds, dc=dc_hostname) + + # SMB connect to DC + conn = smb_connection(dc_hostname, + 'sysvol', + lp=self.lp, + creds=self.creds) + + realm = self.lp.get('realm') + vgp_dir = '\\'.join([realm.lower(), 'Policies', gpo, + 'MACHINE\\VGP\\VTLA\\Unix\\Scripts\\Startup']) + vgp_xml = '\\'.join([vgp_dir, 'manifest.xml']) + try: + xml_data = ET.ElementTree(ET.fromstring(conn.loadfile(vgp_xml))) + policy = xml_data.getroot().find('policysetting') + data = policy.find('data') + except NTSTATUSError as e: + # STATUS_OBJECT_NAME_INVALID, STATUS_OBJECT_NAME_NOT_FOUND, + # STATUS_OBJECT_PATH_NOT_FOUND + if e.args[0] in [0xC0000033, 0xC0000034, 0xC000003A]: + raise CommandError("Cannot remove script '%s' " + "because it does not exist" % script) + elif e.args[0] == 0xC0000022: # STATUS_ACCESS_DENIED + raise CommandError("The authenticated user does " + "not have sufficient privileges") + else: + raise + + for listelement in data.findall('listelement'): + script_elm = listelement.find('script') + if script_elm.text == os.path.basename(script.replace('\\', '/')): + data.remove(listelement) + break + else: + raise CommandError("Cannot remove script '%s' " + "because it does not exist" % script) + + out = BytesIO() + xml_data.write(out, encoding='UTF-8', xml_declaration=True) + out.seek(0) + try: + create_directory_hier(conn, vgp_dir) + conn.savefile(vgp_xml, out.read()) + except NTSTATUSError as e: + if e.args[0] == 0xC0000022: # STATUS_ACCESS_DENIED + raise CommandError("The authenticated user does " + "not have sufficient privileges") + raise + +class cmd_startup(SuperCommand): + """Manage Startup Scripts Group Policy Objects""" + subcommands = {} + subcommands["list"] = cmd_list_startup() + subcommands["add"] = cmd_add_startup() + subcommands["remove"] = cmd_remove_startup() + +class cmd_scripts(SuperCommand): + """Manage Scripts Group Policy Objects""" + subcommands = {} + subcommands["startup"] = cmd_startup() + class cmd_manage(SuperCommand): """Manage Group Policy Objects""" subcommands = {} @@ -3048,6 +3326,7 @@ class cmd_manage(SuperCommand): subcommands["symlink"] = cmd_symlink() subcommands["files"] = cmd_files() subcommands["openssh"] = cmd_openssh() + subcommands["scripts"] = cmd_scripts() class cmd_gpo(SuperCommand): """Group Policy Object (GPO) management.""" diff --git a/python/samba/tests/gpo.py b/python/samba/tests/gpo.py index e3b500868ec..2ff3e5e593e 100644 --- a/python/samba/tests/gpo.py +++ b/python/samba/tests/gpo.py @@ -33,6 +33,7 @@ from samba.gpclass import gp_inf_ext from samba.gp_smb_conf_ext import gp_smb_conf_ext from samba.vgp_files_ext import vgp_files_ext from samba.vgp_openssh_ext import vgp_openssh_ext +from samba.vgp_startup_scripts_ext import vgp_startup_scripts_ext import logging from samba.credentials import Credentials from samba.gp_msgs_ext import gp_msgs_ext @@ -42,6 +43,7 @@ from samba.ndr import ndr_pack import codecs from shutil import copyfile import xml.etree.ElementTree as etree +import hashlib realm = os.environ.get('REALM') policies = realm + '/POLICIES' @@ -1106,3 +1108,123 @@ class GPOTests(tests.TestCase): # Unstage the Registry.pol file unstage_file(manifest) + + def test_vgp_startup_scripts(self): + local_path = self.lp.cache_path('gpo_cache') + guid = '{31B2F340-016D-11D2-945F-00C04FB984F9}' + manifest = os.path.join(local_path, policies, guid, 'MACHINE', + 'VGP/VTLA/UNIX/SCRIPTS/STARTUP/MANIFEST.XML') + test_script = os.path.join(os.path.dirname(manifest), 'TEST.SH') + test_data = '#!/bin/sh\necho $@ hello world' + ret = stage_file(test_script, test_data) + self.assertTrue(ret, 'Could not create the target %s' % test_script) + logger = logging.getLogger('gpo_tests') + cache_dir = self.lp.get('cache directory') + store = GPOStorage(os.path.join(cache_dir, 'gpo.tdb')) + + machine_creds = Credentials() + machine_creds.guess(self.lp) + machine_creds.set_machine_account() + + # Initialize the group policy extension + ext = vgp_startup_scripts_ext(logger, self.lp, machine_creds, store) + + ads = gpo.ADS_STRUCT(self.server, self.lp, machine_creds) + if ads.connect(): + gpos = ads.get_gpo_list(machine_creds.get_username()) + + # Stage the manifest.xml file with test data + stage = etree.Element('vgppolicy') + policysetting = etree.SubElement(stage, 'policysetting') + version = etree.SubElement(policysetting, 'version') + version.text = '1' + data = etree.SubElement(policysetting, 'data') + listelement = etree.SubElement(data, 'listelement') + script = etree.SubElement(listelement, 'script') + script.text = os.path.basename(test_script).lower() + parameters = etree.SubElement(listelement, 'parameters') + parameters.text = '-n' + hash = etree.SubElement(listelement, 'hash') + hash.text = \ + hashlib.md5(open(test_script, 'rb').read()).hexdigest().upper() + run_as = etree.SubElement(listelement, 'run_as') + run_as.text = 'root' + ret = stage_file(manifest, etree.tostring(stage)) + self.assertTrue(ret, 'Could not create the target %s' % manifest) + + # Process all gpos, with temp output directory + with TemporaryDirectory() as dname: + ext.process_group_policy([], gpos, dname) + files = os.listdir(dname) + self.assertEquals(len(files), 1, + 'The target script was not created') + entry = '@reboot %s %s %s' % (run_as.text, test_script, + parameters.text) + self.assertIn(entry, + open(os.path.join(dname, files[0]), 'r').read(), + 'The test entry was not found') + + # Remove policy + gp_db = store.get_gplog(machine_creds.get_username()) + del_gpos = get_deleted_gpos_list(gp_db, []) + ext.process_group_policy(del_gpos, []) + files = os.listdir(dname) + self.assertEquals(len(files), 0, + 'The target script was not removed') + + # Test rsop + g = [g for g in gpos if g.name == guid][0] + ret = ext.rsop(g) + self.assertIn(entry, list(ret.values())[0][0], + 'The target entry was not listed by rsop') + + # Unstage the manifest.xml and script files + unstage_file(manifest) + unstage_file(test_script) + + # Stage the manifest.xml file for run once scripts + etree.SubElement(listelement, 'run_once') + run_as.text = pwd.getpwuid(os.getuid()).pw_name + ret = stage_file(manifest, etree.tostring(stage)) + self.assertTrue(ret, 'Could not create the target %s' % manifest) + + # Process all gpos, with temp output directory + # A run once script will be executed immediately, + # instead of creating a cron job + with TemporaryDirectory() as dname: + test_file = '%s/TESTING.txt' % dname + test_data = '#!/bin/sh\ntouch %s' % test_file + ret = stage_file(test_script, test_data) + self.assertTrue(ret, 'Could not create the target %s' % test_script) + + ext.process_group_policy([], gpos, dname) + files = os.listdir(dname) + self.assertEquals(len(files), 1, + 'The test file was not created') + self.assertEquals(files[0], os.path.basename(test_file), + 'The test file was not created') + + # Unlink the test file and ensure that processing + # policy again does not recreate it. + os.unlink(test_file) + ext.process_group_policy([], gpos, dname) + files = os.listdir(dname) + self.assertEquals(len(files), 0, + 'The test file should not have been created') + + # Remove policy + gp_db = store.get_gplog(machine_creds.get_username()) + del_gpos = get_deleted_gpos_list(gp_db, []) + ext.process_group_policy(del_gpos, []) + + # Test rsop + entry = 'Run once as: %s `%s %s`' % (run_as.text, test_script, + parameters.text) + g = [g for g in gpos if g.name == guid][0] + ret = ext.rsop(g) + self.assertIn(entry, list(ret.values())[0][0], + 'The target entry was not listed by rsop') + + # Unstage the manifest.xml and script files + unstage_file(manifest) + unstage_file(test_script) diff --git a/python/samba/tests/samba_tool/gpo.py b/python/samba/tests/samba_tool/gpo.py index c57c6786d79..13734f3b163 100644 --- a/python/samba/tests/samba_tool/gpo.py +++ b/python/samba/tests/samba_tool/gpo.py @@ -32,6 +32,8 @@ from samba.common import get_string from configparser import ConfigParser from io import StringIO import xml.etree.ElementTree as etree +from tempfile import NamedTemporaryFile +from time import sleep source_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../../..")) @@ -1120,6 +1122,107 @@ class GpoCmdTestCase(SambaToolCmdTest): os.environ["PASSWORD"])) self.assertNotIn(openssh, out, 'The test entry was still found!') + def test_startup_script_add(self): + lp = LoadParm() + fname = None + with NamedTemporaryFile() as f: + fname = os.path.basename(f.name) + f.write(b'#!/bin/sh\necho $@ hello world') + f.flush() + (result, out, err) = self.runsublevelcmd("gpo", ("manage", + "scripts", "startup", -- Samba Shared Repository