From 56f5ea683001d573e9ddeb30e68aae8ba2d742ed Mon Sep 17 00:00:00 2001 From: David Mulder Date: Fri, 27 May 2022 10:56:25 -0600 Subject: gpo: Move Group Policy code below gp directory Moves the Group Policy extensions and supporting code within the existing python/samba/gp directory. Meant to clean up the clutter that's accumulating in python/samba. Signed-off-by: David Mulder Reviewed-by: Jeremy Allison Autobuild-User(master): Jeremy Allison Autobuild-Date(master): Tue May 31 20:15:45 UTC 2022 on sn-devel-184 --- python/samba/gp/gp_centrify_crontab_ext.py | 160 +++++++ python/samba/gp/gp_centrify_sudoers_ext.py | 100 +++++ python/samba/gp/gp_cert_auto_enroll_ext.py | 498 +++++++++++++++++++++ python/samba/gp/gp_chromium_ext.py | 491 +++++++++++++++++++++ python/samba/gp/gp_ext_loader.py | 59 +++ python/samba/gp/gp_firefox_ext.py | 170 ++++++++ python/samba/gp/gp_firewalld_ext.py | 155 +++++++ python/samba/gp/gp_gnome_settings_ext.py | 452 +++++++++++++++++++ python/samba/gp/gp_msgs_ext.py | 83 ++++ python/samba/gp/gp_scripts_ext.py | 172 ++++++++ python/samba/gp/gp_sec_ext.py | 217 ++++++++++ python/samba/gp/gp_smb_conf_ext.py | 102 +++++ python/samba/gp/gp_sudoers_ext.py | 109 +++++ python/samba/gp/gpclass.py | 671 +++++++++++++++++++++++++++++ python/samba/gp/vgp_access_ext.py | 133 ++++++ python/samba/gp/vgp_files_ext.py | 139 ++++++ python/samba/gp/vgp_issue_ext.py | 71 +++ python/samba/gp/vgp_motd_ext.py | 71 +++ python/samba/gp/vgp_openssh_ext.py | 105 +++++ python/samba/gp/vgp_startup_scripts_ext.py | 124 ++++++ python/samba/gp/vgp_sudoers_ext.py | 117 +++++ python/samba/gp/vgp_symlink_ext.py | 76 ++++ python/samba/gp_centrify_crontab_ext.py | 160 ------- python/samba/gp_centrify_sudoers_ext.py | 100 ----- python/samba/gp_cert_auto_enroll_ext.py | 498 --------------------- python/samba/gp_chromium_ext.py | 491 --------------------- python/samba/gp_ext_loader.py | 59 --- python/samba/gp_firefox_ext.py | 170 -------- python/samba/gp_firewalld_ext.py | 155 ------- python/samba/gp_gnome_settings_ext.py | 452 ------------------- python/samba/gp_msgs_ext.py | 83 ---- python/samba/gp_scripts_ext.py | 172 -------- python/samba/gp_sec_ext.py | 217 ---------- python/samba/gp_smb_conf_ext.py | 102 ----- python/samba/gp_sudoers_ext.py | 109 ----- python/samba/gpclass.py | 671 ----------------------------- python/samba/netcmd/gpo.py | 2 +- python/samba/tests/gpo.py | 54 +-- python/samba/tests/gpo_member.py | 4 +- python/samba/vgp_access_ext.py | 133 ------ python/samba/vgp_files_ext.py | 139 ------ python/samba/vgp_issue_ext.py | 71 --- python/samba/vgp_motd_ext.py | 71 --- python/samba/vgp_openssh_ext.py | 105 ----- python/samba/vgp_startup_scripts_ext.py | 124 ------ python/samba/vgp_sudoers_ext.py | 117 ----- python/samba/vgp_symlink_ext.py | 76 ---- 47 files changed, 4305 insertions(+), 4305 deletions(-) create mode 100644 python/samba/gp/gp_centrify_crontab_ext.py create mode 100644 python/samba/gp/gp_centrify_sudoers_ext.py create mode 100644 python/samba/gp/gp_cert_auto_enroll_ext.py create mode 100644 python/samba/gp/gp_chromium_ext.py create mode 100644 python/samba/gp/gp_ext_loader.py create mode 100644 python/samba/gp/gp_firefox_ext.py create mode 100644 python/samba/gp/gp_firewalld_ext.py create mode 100644 python/samba/gp/gp_gnome_settings_ext.py create mode 100644 python/samba/gp/gp_msgs_ext.py create mode 100644 python/samba/gp/gp_scripts_ext.py create mode 100644 python/samba/gp/gp_sec_ext.py create mode 100644 python/samba/gp/gp_smb_conf_ext.py create mode 100644 python/samba/gp/gp_sudoers_ext.py create mode 100644 python/samba/gp/gpclass.py create mode 100644 python/samba/gp/vgp_access_ext.py create mode 100644 python/samba/gp/vgp_files_ext.py create mode 100644 python/samba/gp/vgp_issue_ext.py create mode 100644 python/samba/gp/vgp_motd_ext.py create mode 100644 python/samba/gp/vgp_openssh_ext.py create mode 100644 python/samba/gp/vgp_startup_scripts_ext.py create mode 100644 python/samba/gp/vgp_sudoers_ext.py create mode 100644 python/samba/gp/vgp_symlink_ext.py delete mode 100644 python/samba/gp_centrify_crontab_ext.py delete mode 100644 python/samba/gp_centrify_sudoers_ext.py delete mode 100644 python/samba/gp_cert_auto_enroll_ext.py delete mode 100644 python/samba/gp_chromium_ext.py delete mode 100644 python/samba/gp_ext_loader.py delete mode 100644 python/samba/gp_firefox_ext.py delete mode 100644 python/samba/gp_firewalld_ext.py delete mode 100644 python/samba/gp_gnome_settings_ext.py delete mode 100644 python/samba/gp_msgs_ext.py delete mode 100644 python/samba/gp_scripts_ext.py delete mode 100644 python/samba/gp_sec_ext.py delete mode 100644 python/samba/gp_smb_conf_ext.py delete mode 100644 python/samba/gp_sudoers_ext.py delete mode 100644 python/samba/gpclass.py delete mode 100644 python/samba/vgp_access_ext.py delete mode 100644 python/samba/vgp_files_ext.py delete mode 100644 python/samba/vgp_issue_ext.py delete mode 100644 python/samba/vgp_motd_ext.py delete mode 100644 python/samba/vgp_openssh_ext.py delete mode 100644 python/samba/vgp_startup_scripts_ext.py delete mode 100644 python/samba/vgp_sudoers_ext.py delete mode 100644 python/samba/vgp_symlink_ext.py (limited to 'python') diff --git a/python/samba/gp/gp_centrify_crontab_ext.py b/python/samba/gp/gp_centrify_crontab_ext.py new file mode 100644 index 00000000000..220feb776a5 --- /dev/null +++ b/python/samba/gp/gp_centrify_crontab_ext.py @@ -0,0 +1,160 @@ +# gp_centrify_crontab_ext samba gpo policy +# Copyright (C) David Mulder 2022 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os, re +from subprocess import Popen, PIPE +from samba.gp.gpclass import gp_pol_ext, drop_privileges +from hashlib import blake2b +from tempfile import NamedTemporaryFile + +intro = ''' +### autogenerated by samba +# +# This file is generated by the gp_centrify_crontab_ext Group Policy +# Client Side Extension. To modify the contents of this file, +# modify the appropriate Group Policy objects which apply +# to this machine. DO NOT MODIFY THIS FILE DIRECTLY. +# + +''' +end = ''' +### autogenerated by samba ### +''' + +class gp_centrify_crontab_ext(gp_pol_ext): + def __str__(self): + return 'Centrify/CrontabEntries' + + def process_group_policy(self, deleted_gpo_list, changed_gpo_list, + cdir=None): + for guid, settings in deleted_gpo_list: + self.gp_db.set_guid(guid) + if str(self) in settings: + for attribute, script in settings[str(self)].items(): + if os.path.exists(script): + os.unlink(script) + self.gp_db.delete(str(self), attribute) + self.gp_db.commit() + + for gpo in changed_gpo_list: + if gpo.file_sys_path: + section = \ + 'Software\\Policies\\Centrify\\UnixSettings\\CrontabEntries' + self.gp_db.set_guid(gpo.name) + pol_file = 'MACHINE/Registry.pol' + path = os.path.join(gpo.file_sys_path, pol_file) + pol_conf = self.parse(path) + if not pol_conf: + continue + for e in pol_conf.entries: + if e.keyname == section and e.data.strip(): + cron_dir = '/etc/cron.d' if not cdir else cdir + attribute = blake2b(e.data.encode()).hexdigest() + old_val = self.gp_db.retrieve(str(self), attribute) + if not old_val: + with NamedTemporaryFile(prefix='gp_', mode="w+", + delete=False, dir=cron_dir) as f: + contents = '%s\n%s\n%s' % (intro, e.data, end) + f.write(contents) + self.gp_db.store(str(self), attribute, f.name) + self.gp_db.commit() + + def rsop(self, gpo, target='MACHINE'): + output = {} + section = 'Software\\Policies\\Centrify\\UnixSettings\\CrontabEntries' + pol_file = '%s/Registry.pol' % target + if gpo.file_sys_path: + path = os.path.join(gpo.file_sys_path, pol_file) + pol_conf = self.parse(path) + if not pol_conf: + return output + for e in pol_conf.entries: + if e.keyname == section and e.data.strip(): + if str(self) not in output.keys(): + output[str(self)] = [] + output[str(self)].append(e.data) + return output + +def fetch_crontab(username): + p = Popen(['crontab', '-l', '-u', username], stdout=PIPE, stderr=PIPE) + out, err = p.communicate() + if p.returncode != 0: + raise RuntimeError('Failed to read the crontab: %s' % err) + m = re.findall('%s(.*)%s' % (intro, end), out.decode(), re.DOTALL) + if len(m) == 1: + entries = m[0].strip().split('\n') + else: + entries = [] + m = re.findall('(.*)%s.*%s(.*)' % (intro, end), out.decode(), re.DOTALL) + if len(m) == 1: + others = '\n'.join([l.strip() for l in m[0]]) + else: + others = out.decode() + return others, entries + +def install_crontab(fname, username): + p = Popen(['crontab', fname, '-u', username], stdout=PIPE, stderr=PIPE) + _, err = p.communicate() + if p.returncode != 0: + raise RuntimeError('Failed to install crontab: %s' % err) + +class gp_user_centrify_crontab_ext(gp_centrify_crontab_ext): + def process_group_policy(self, deleted_gpo_list, changed_gpo_list): + for guid, settings in deleted_gpo_list: + self.gp_db.set_guid(guid) + if str(self) in settings: + others, entries = fetch_crontab(self.username) + for attribute, entry in settings[str(self)].items(): + if entry in entries: + entries.remove(entry) + self.gp_db.delete(str(self), attribute) + with NamedTemporaryFile() as f: + if len(entries) > 0: + f.write('\n'.join([others, intro, + '\n'.join(entries), end]).encode()) + else: + f.write(others.encode()) + f.flush() + install_crontab(f.name, self.username) + self.gp_db.commit() + + for gpo in changed_gpo_list: + if gpo.file_sys_path: + section = \ + 'Software\\Policies\\Centrify\\UnixSettings\\CrontabEntries' + self.gp_db.set_guid(gpo.name) + pol_file = 'USER/Registry.pol' + path = os.path.join(gpo.file_sys_path, pol_file) + pol_conf = drop_privileges('root', self.parse, path) + if not pol_conf: + continue + for e in pol_conf.entries: + if e.keyname == section and e.data.strip(): + attribute = blake2b(e.data.encode()).hexdigest() + old_val = self.gp_db.retrieve(str(self), attribute) + others, entries = fetch_crontab(self.username) + if not old_val or e.data not in entries: + entries.append(e.data) + with NamedTemporaryFile() as f: + f.write('\n'.join([others, intro, + '\n'.join(entries), end]).encode()) + f.flush() + install_crontab(f.name, self.username) + self.gp_db.store(str(self), attribute, e.data) + self.gp_db.commit() + + def rsop(self, gpo): + return super().rsop(gpo, target='USER') diff --git a/python/samba/gp/gp_centrify_sudoers_ext.py b/python/samba/gp/gp_centrify_sudoers_ext.py new file mode 100644 index 00000000000..2d03a4871a4 --- /dev/null +++ b/python/samba/gp/gp_centrify_sudoers_ext.py @@ -0,0 +1,100 @@ +# gp_centrify_sudoers_ext samba gpo policy +# Copyright (C) David Mulder 2022 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +from samba.gp.gpclass import gp_pol_ext +from base64 import b64encode +from tempfile import NamedTemporaryFile +from subprocess import Popen, PIPE +from samba.gp.gp_sudoers_ext import visudo, intro +from samba.gp.util.logging import log + +def ext_enabled(entries): + section = 'Software\\Policies\\Centrify\\UnixSettings' + for e in entries: + if e.keyname == section and e.valuename == 'sudo.enabled': + return e.data == 1 + return False + +class gp_centrify_sudoers_ext(gp_pol_ext): + def __str__(self): + return 'Centrify/Sudo Rights' + + def process_group_policy(self, deleted_gpo_list, changed_gpo_list, + sdir='/etc/sudoers.d'): + for guid, settings in deleted_gpo_list: + self.gp_db.set_guid(guid) + if str(self) in settings: + for attribute, sudoers in settings[str(self)].items(): + if os.path.exists(sudoers): + os.unlink(sudoers) + self.gp_db.delete(str(self), attribute) + self.gp_db.commit() + + for gpo in changed_gpo_list: + if gpo.file_sys_path: + section = 'Software\\Policies\\Centrify\\UnixSettings\\SuDo' + self.gp_db.set_guid(gpo.name) + pol_file = 'MACHINE/Registry.pol' + path = os.path.join(gpo.file_sys_path, pol_file) + pol_conf = self.parse(path) + if not pol_conf or not ext_enabled(pol_conf.entries): + continue + for e in pol_conf.entries: + if e.keyname == section and e.data.strip(): + if '**delvals.' in e.valuename: + continue + attribute = b64encode(e.data.encode()).decode() + old_val = self.gp_db.retrieve(str(self), attribute) + if not old_val: + contents = intro + contents += '%s\n' % e.data + with NamedTemporaryFile() as f: + with open(f.name, 'w') as w: + w.write(contents) + sudo_validation = \ + Popen([visudo, '-c', '-f', f.name], + stdout=PIPE, stderr=PIPE).wait() + if sudo_validation == 0: + with NamedTemporaryFile(prefix='gp_', + delete=False, + dir=sdir) as f: + with open(f.name, 'w') as w: + w.write(contents) + self.gp_db.store(str(self), + attribute, + f.name) + else: + log.error('Sudoers apply failed', e.data) + self.gp_db.commit() + + def rsop(self, gpo): + output = {} + section = 'Software\\Policies\\Centrify\\UnixSettings\\SuDo' + pol_file = 'MACHINE/Registry.pol' + if gpo.file_sys_path: + path = os.path.join(gpo.file_sys_path, pol_file) + pol_conf = self.parse(path) + if not pol_conf: + return output + for e in pol_conf.entries: + if e.keyname == section and e.data.strip(): + if '**delvals.' in e.valuename: + continue + if str(self) not in output.keys(): + output[str(self)] = [] + output[str(self)].append(e.data) + return output diff --git a/python/samba/gp/gp_cert_auto_enroll_ext.py b/python/samba/gp/gp_cert_auto_enroll_ext.py new file mode 100644 index 00000000000..f1dcf6bbafb --- /dev/null +++ b/python/samba/gp/gp_cert_auto_enroll_ext.py @@ -0,0 +1,498 @@ +# gp_cert_auto_enroll_ext samba group policy +# Copyright (C) David Mulder 2021 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import operator +import requests +from samba.gp.gpclass import gp_pol_ext +from samba import Ldb +from ldb import SCOPE_SUBTREE, SCOPE_BASE +from samba.auth import system_session +from samba.gp.gpclass import get_dc_hostname +import base64 +from shutil import which +from subprocess import Popen, PIPE +import re +import json +from samba.gp.util.logging import log +import struct +try: + from cryptography.hazmat.primitives.serialization.pkcs7 import \ + load_der_pkcs7_certificates +except ModuleNotFoundError: + def load_der_pkcs7_certificates(x): return [] + log.error('python cryptography missing pkcs7 support. ' + 'Certificate chain parsing will fail') +from cryptography.hazmat.primitives.serialization import Encoding +from cryptography.x509 import load_der_x509_certificate +from cryptography.hazmat.backends import default_backend + +cert_wrap = b""" +-----BEGIN CERTIFICATE----- +%s +-----END CERTIFICATE-----""" +global_trust_dir = '/etc/pki/trust/anchors' +endpoint_re = '(https|HTTPS)://(?P[a-zA-Z0-9.-]+)/ADPolicyProvider' + \ + '_CEP_(?P[a-zA-Z]+)/service.svc/CEP' + + +def octet_string_to_objectGUID(data): + """Convert an octet string to an objectGUID.""" + return '%s-%s-%s-%s-%s' % ('%02x' % struct.unpack('H', data[8:10])[0], + '%02x%02x' % struct.unpack('>HL', data[10:])) + + +def group_and_sort_end_point_information(end_point_information): + """Group and Sort End Point Information. + + [MS-CAESO] 4.4.5.3.2.3 + In this step autoenrollment processes the end point information by grouping + it by CEP ID and sorting in the order with which it will use the end point + to access the CEP information. + """ + # Create groups of the CertificateEnrollmentPolicyEndPoint instances that + # have the same value of the EndPoint.PolicyID datum. + end_point_groups = {} + for e in end_point_information: + if e['PolicyID'] not in end_point_groups.keys(): + end_point_groups[e['PolicyID']] = [] + end_point_groups[e['PolicyID']].append(e) + + # Sort each group by following these rules: + for end_point_group in end_point_groups.values(): + # Sort the CertificateEnrollmentPolicyEndPoint instances in ascending + # order based on the EndPoint.Cost value. + end_point_group.sort(key=lambda e: e['Cost']) + + # For instances that have the same EndPoint.Cost: + cost_list = [e['Cost'] for e in end_point_group] + costs = set(cost_list) + for cost in costs: + i = cost_list.index(cost) + j = len(cost_list)-operator.indexOf(reversed(cost_list), cost)-1 + if i == j: + continue + + # Sort those that have EndPoint.Authentication equal to Kerberos + # first. Then sort those that have EndPoint.Authentication equal to + # Anonymous. The rest of the CertificateEnrollmentPolicyEndPoint + # instances follow in an arbitrary order. + def sort_auth(e): + # 0x2 - Kerberos + if e['AuthFlags'] == 0x2: + return 0 + # 0x1 - Anonymous + elif e['AuthFlags'] == 0x1: + return 1 + else: + return 2 + end_point_group[i:j+1] = sorted(end_point_group[i:j+1], + key=sort_auth) + return list(end_point_groups.values()) + +def obtain_end_point_information(entries): + """Obtain End Point Information. + + [MS-CAESO] 4.4.5.3.2.2 + In this step autoenrollment initializes the + CertificateEnrollmentPolicyEndPoints table. + """ + end_point_information = {} + section = 'Software\\Policies\\Microsoft\\Cryptography\\PolicyServers\\' + for e in entries: + if not e.keyname.startswith(section): + continue + name = e.keyname.replace(section, '') + if name not in end_point_information.keys(): + end_point_information[name] = {} + end_point_information[name][e.valuename] = e.data + for ca in end_point_information.values(): + m = re.match(endpoint_re, ca['URL']) + if m: + name = '%s-CA' % m.group('server').replace('.', '-') + ca['name'] = name + ca['hostname'] = m.group('server') + ca['auth'] = m.group('auth') + elif ca['URL'].lower() != 'ldap:': + edata = { 'endpoint': ca['URL'] } + log.error('Failed to parse the endpoint', edata) + return {} + end_point_information = \ + group_and_sort_end_point_information(end_point_information.values()) + return end_point_information + +def fetch_certification_authorities(ldb): + """Initialize CAs. + + [MS-CAESO] 4.4.5.3.1.2 + """ + result = [] + basedn = ldb.get_default_basedn() + # Autoenrollment MUST do an LDAP search for the CA information + # (pKIEnrollmentService) objects under the following container: + dn = 'CN=Enrollment Services,CN=Public Key Services,CN=Services,CN=Configuration,%s' % basedn + attrs = ['cACertificate', 'cn', 'dNSHostName'] + expr = '(objectClass=pKIEnrollmentService)' + res = ldb.search(dn, SCOPE_SUBTREE, expr, attrs) + if len(res) == 0: + return result + for es in res: + data = { 'name': es['cn'][0], + 'hostname': es['dNSHostName'][0], + 'cACertificate': es['cACertificate'][0] + } + result.append(data) + return result + +def fetch_template_attrs(ldb, name, attrs=['msPKI-Minimal-Key-Size']): + basedn = ldb.get_default_basedn() + dn = 'CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration,%s' % basedn + expr = '(cn=%s)' % name + res = ldb.search(dn, SCOPE_SUBTREE, expr, attrs) + if len(res) == 1 and 'msPKI-Minimal-Key-Size' in res[0]: + return dict(res[0]) + else: + return {'msPKI-Minimal-Key-Size': ['2048']} + +def format_root_cert(cert): + cert = base64.b64encode(cert) + return cert_wrap % re.sub(b"(.{64})", b"\\1\n", cert, 0, re.DOTALL) + +def find_cepces_submit(): + certmonger_dirs = [os.environ.get("PATH"), '/usr/lib/certmonger', + '/usr/libexec/certmonger'] + return which('cepces-submit', path=':'.join(certmonger_dirs)) + +def get_supported_templates(server): + cepces_submit = find_cepces_submit() + if os.path.exists(cepces_submit): + env = os.environ + env['CERTMONGER_OPERATION'] = 'GET-SUPPORTED-TEMPLATES' + p = Popen([cepces_submit, '--server=%s' % server, '--auth=Kerberos'], + env=env, stdout=PIPE, stderr=PIPE) + out, err = p.communicate() + if p.returncode != 0: + data = { 'Error': err.decode() } + log.error('Failed to fetch the list of supported templates.', data) + return out.strip().split() + return [] + + +def getca(ca, url, trust_dir): + """Fetch Certificate Chain from the CA.""" + root_cert = os.path.join(trust_dir, '%s.crt' % ca['name']) + root_certs = [] + + try: + r = requests.get(url=url, params={'operation': 'GetCACert', + 'message': 'CAIdentifier'}) + except requests.exceptions.ConnectionError: + log.warn('Failed to establish a new connection') + r = None + if r is None or r.content == b'' or r.headers['Content-Type'] == 'text/html': + log.warn('Failed to fetch the root certificate chain.') + log.warn('The Network Device Enrollment Service is either not' + + ' installed or not configured.') + if 'cACertificate' in ca: + log.warn('Installing the server certificate only.') + try: + cert = load_der_x509_certificate(ca['cACertificate']) + except TypeError: + cert = load_der_x509_certificate(ca['cACertificate'], + default_backend()) + cert_data = cert.public_bytes(Encoding.PEM) + with open(root_cert, 'wb') as w: + w.write(cert_data) + root_certs.append(root_cert) + return root_certs + + if r.headers['Content-Type'] == 'application/x-x509-ca-cert': + # Older versions of load_der_x509_certificate require a backend param + try: + cert = load_der_x509_certificate(r.content) + except TypeError: + cert = load_der_x509_certificate(r.content, default_backend()) + cert_data = cert.public_bytes(Encoding.PEM) + with open(root_cert, 'wb') as w: + w.write(cert_data) + root_certs.append(root_cert) + elif r.headers['Content-Type'] == 'application/x-x509-ca-ra-cert': + certs = load_der_pkcs7_certificates(r.content) + for i in range(0, len(certs)): + cert = certs[i].public_bytes(Encoding.PEM) + dest = '%s.%d' % (root_cert, i) + with open(dest, 'wb') as w: + w.write(cert) + root_certs.append(dest) + else: + log.warn('getca: Wrong (or missing) MIME content type') + + return root_certs + + +def cert_enroll(ca, ldb, trust_dir, private_dir, auth='Kerberos'): + """Install the root certificate chain.""" + data = {'files': [], 'templates': []} + url = 'http://%s/CertSrv/mscep/mscep.dll/pkiclient.exe?' % ca['hostname'] + root_certs = getca(ca, url, trust_dir) + data['files'].extend(root_certs) + for src in root_certs: + # Symlink the certs to global trust dir + dst = os.path.join(global_trust_dir, os.path.basename(src)) + try: + os.symlink(src, dst) + data['files'].append(dst) + except PermissionError: + log.warn('Failed to symlink root certificate to the' + ' admin trust anchors') + except FileNotFoundError: + log.warn('Failed to symlink root certificate to the' + ' admin trust anchors.' + ' The directory was not found', global_trust_dir) + except FileExistsError: + # If we're simply downloading a renewed cert, the symlink + # already exists. Ignore the FileExistsError. Preserve the + # existing symlink in the unapply data. + data['files'].append(dst) + update = which('update-ca-certificates') + if update is not None: + Popen([update]).wait() + # Setup Certificate Auto Enrollment + getcert = which('getcert') + cepces_submit = find_cepces_submit() + if getcert is not None and os.path.exists(cepces_submit): + p = Popen([getcert, 'add-ca', '-c', ca['name'], '-e', + '%s --server=%s --auth=%s' % (cepces_submit, + ca['hostname'], auth)], + stdout=PIPE, stderr=PIPE) + out, err = p.communicate() + log.debug(out.decode()) + if p.returncode != 0: + data = { 'Error': err.decode(), 'CA': ca['name'] } + log.error('Failed to add Certificate Authority', data) + supported_templates = get_supported_templates(ca['hostname']) + for template in supported_templates: + attrs = fetch_template_attrs(ldb, template) + nickname = '%s.%s' % (ca['name'], template.decode()) + keyfile = os.path.join(private_dir, '%s.key' % nickname) + certfile = os.path.join(trust_dir, '%s.crt' % nickname) + p = Popen([getcert, 'request', '-c', ca['name'], + '-T', template.decode(), + '-I', nickname, '-k', keyfile, '-f', certfile, + '-g', attrs['msPKI-Minimal-Key-Size'][0]], + stdout=PIPE, stderr=PIPE) + out, err = p.communicate() + log.debug(out.decode()) + if p.returncode != 0: + data = { 'Error': err.decode(), 'Certificate': nickname } + log.error('Failed to request certificate', data) + data['files'].extend([keyfile, certfile]) + data['templates'].append(nickname) + if update is not None: + Popen([update]).wait() + else: + log.warn('certmonger and cepces must be installed for ' + + 'certificate auto enrollment to work') + return json.dumps(data) + +class gp_cert_auto_enroll_ext(gp_pol_ext): + def __str__(self): + return 'Cryptography\AutoEnrollment' + + def process_group_policy(self, deleted_gpo_list, changed_gpo_list, + trust_dir=None, private_dir=None): + if trust_dir is None: + trust_dir = self.lp.cache_path('certs') + if private_dir is None: + private_dir = self.lp.private_path('certs') + if not os.path.exists(trust_dir): + os.mkdir(trust_dir, mode=0o755) + if not os.path.exists(private_dir): + os.mkdir(private_dir, mode=0o700) + + for guid, settings in deleted_gpo_list: + self.gp_db.set_guid(guid) + if str(self) in settings: + for ca_cn_enc, data in settings[str(self)].items(): + ca_cn = base64.b64decode(ca_cn_enc) + data = json.loads(data) + getcert = which('getcert') + if getcert is not None: + Popen([getcert, 'remove-ca', '-c', ca_cn]).wait() + for nickname in data['templates']: + Popen([getcert, 'stop-tracking', + '-i', nickname]).wait() + for f in data['files']: + if os.path.exists(f): + os.unlink(f) + self.gp_db.delete(str(self), ca_cn_enc) + self.gp_db.commit() + + for gpo in changed_gpo_list: + if gpo.file_sys_path: + section = 'Software\Policies\Microsoft\Cryptography\AutoEnrollment' + self.gp_db.set_guid(gpo.name) + pol_file = 'MACHINE/Registry.pol' + path = os.path.join(gpo.file_sys_path, pol_file) + pol_conf = self.parse(path) + if not pol_conf: + continue + for e in pol_conf.entries: + if e.keyname == section and e.valuename == 'AEPolicy': + # This policy applies as specified in [MS-CAESO] 4.4.5.1 + if e.data & 0x8000: + continue # The policy is disabled + enroll = e.data & 0x1 == 0x1 + manage = e.data & 0x2 == 0x2 + retrive_pending = e.data & 0x4 == 0x4 + if enroll: + self.__enroll(pol_conf.entries, trust_dir, + private_dir) + self.gp_db.commit() + + def __read_cep_data(self, ldb, end_point_information, + trust_dir, private_dir): + """Read CEP Data. + + [MS-CAESO] 4.4.5.3.2.4 + In this step autoenrollment initializes instances of the + CertificateEnrollmentPolicy by accessing end points associated with CEP + groups created in the previous step. + """ + # For each group created in the previous step: + for end_point_group in end_point_information: + # Pick an arbitrary instance of the + # CertificateEnrollmentPolicyEndPoint from the group + e = end_point_group[0] + + # If this instance does not have the AutoEnrollmentEnabled flag set + # in the EndPoint.Flags, continue with the next group. + if not e['Flags'] & 0x10: + continue + + # If the current group contains a + # CertificateEnrollmentPolicyEndPoint instance with EndPoint.URI + # equal to "LDAP": + if any([e['URL'] == 'LDAP:' for e in end_point_group]): + # Perform an LDAP search to read the value of the objectGuid + # attribute of the root object of the forest root domain NC. If + # any errors are encountered, continue with the next group. + res = ldb.search('', SCOPE_BASE, '(objectClass=*)', + ['rootDomainNamingContext']) + if len(res) != 1: + continue + res2 = ldb.search(res[0]['rootDomainNamingContext'][0], + SCOPE_BASE, '(objectClass=*)', + ['objectGUID']) + if len(res2) != 1: + continue + + # Compare the value read in the previous step to the + # EndPoint.PolicyId datum CertificateEnrollmentPolicyEndPoint + # instance. If the values do not match, continue with the next + # group. + objectGUID = '{%s}' % \ + octet_string_to_objectGUID(res2[0]['objectGUID'][0]).upper() + if objectGUID != e['PolicyID']: + continue + + # For each CertificateEnrollmentPolicyEndPoint instance for that + # group: + for ca in end_point_group: + # If EndPoint.URI equals "LDAP": + if ca['URL'] == 'LDAP:': + # This is a basic configuration. + cas = fetch_certification_authorities(ldb) + for _ca in cas: + data = cert_enroll(_ca, ldb, trust_dir, private_dir) + self.gp_db.store(str(self), + base64.b64encode(_ca['name']).decode(), + data) + # If EndPoint.URI starts with "HTTPS//": + elif ca['URL'].lower().startswith('https://'): + data = cert_enroll(ca, ldb, trust_dir, + private_dir, auth=ca['auth']) + self.gp_db.store(str(self), + base64.b64encode(ca['name'].encode()).decode(), + data) + else: + edata = { 'endpoint': ca['URL'] } + log.error('Unrecognized endpoint', edata) + + def __enroll(self, entries, trust_dir, private_dir): + url = 'ldap://%s' % get_dc_hostname(self.creds, self.lp) + ldb = Ldb(url=url, session_info=system_session(), + lp=self.lp, credentials=self.creds) + + end_point_information = obtain_end_point_information(entries) + if len(end_point_information) > 0: + self.__read_cep_data(ldb, end_point_information, + trust_dir, private_dir) + else: + cas = fetch_certification_authorities(ldb) + for ca in cas: + data = cert_enroll(ca, ldb, trust_dir, private_dir) + self.gp_db.store(str(self), + base64.b64encode(ca['name']).decode(), data) + + def rsop(self, gpo): + output = {} + pol_file = 'MACHINE/Registry.pol' + section = 'Software\Policies\Microsoft\Cryptography\AutoEnrollment' + if gpo.file_sys_path: + path = os.path.join(gpo.file_sys_path, pol_file) + pol_conf = self.parse(path) + if not pol_conf: + return output + for e in pol_conf.entries: + if e.keyname == section and e.valuename == 'AEPolicy': + enroll = e.data & 0x1 == 0x1 + if e.data & 0x8000 or not enroll: + continue + output['Auto Enrollment Policy'] = {} + url = 'ldap://%s' % get_dc_hostname(self.creds, self.lp) + ldb = Ldb(url=url, session_info=system_session(), + lp=self.lp, credentials=self.creds) + end_point_information = \ + obtain_end_point_information(pol_conf.entries) + cas = fetch_certification_authorities(ldb) + if len(end_point_information) > 0: + cas2 = [ep for sl in end_point_information for ep in sl] + if any([ca['URL'] == 'LDAP:' for ca in cas2]): + cas.extend(cas2) + else: + cas = cas2 + for ca in cas: + if 'URL' in ca and ca['URL'] == 'LDAP:': + continue + policy = 'Auto Enrollment Policy' + cn = ca['name'] + if policy not in output: + output[policy] = {} + output[policy][cn] = {} + if 'cACertificate' in ca: + output[policy][cn]['CA Certificate'] = \ + format_root_cert(ca['cACertificate']).decode() + output[policy][cn]['Auto Enrollment Server'] = \ + ca['hostname'] + supported_templates = \ + get_supported_templates(ca['hostname']) + output[policy][cn]['Templates'] = \ + [t.decode() for t in supported_templates] + return output diff --git a/python/samba/gp/gp_chromium_ext.py b/python/samba/gp/gp_chromium_ext.py new file mode 100644 index 00000000000..ae4bc8a7a80 --- /dev/null +++ b/python/samba/gp/gp_chromium_ext.py @@ -0,0 +1,491 @@ +# gp_chromium_ext samba gpo policy +# Copyright (C) David Mulder 2021 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import json +from samba.gp.gpclass import gp_pol_ext +from samba.dcerpc import misc +from samba.common import get_string +from samba.gp.util.logging import log + +def parse_entry_data(name, e): + dict_entries = ['VirtualKeyboardFeatures', + 'DeviceArcDataSnapshotHours', + 'RequiredClientCertificateForDevice', + 'RequiredClientCertificateForUser', + 'RegisteredProtocolHandlers', + 'WebUsbAllowDevicesForUrls', + 'DeviceAutoUpdateTimeRestrictions', + 'DeviceUpdateStagingSchedule', + 'DeviceMinimumVersion', + 'DeviceDisplayResolution', + 'ExtensionSettings', + 'KerberosAccounts', + 'NetworkFileSharesPreconfiguredShares', + 'NetworkThrottlingEnabled', + 'TPMFirmwareUpdateSettings', + 'DeviceOffHours', + 'ParentAccessCodeConfig', + 'PerAppTimeLimits', + 'PerAppTimeLimitsWhitelist', + 'PerAppTimeLimitsAllowlist', + 'UsageTimeLimit', + 'PluginVmImage', + 'DeviceLoginScreenPowerManagement', + 'PowerManagementIdleSettings', + 'ScreenLockDelays', + 'ScreenBrightnessPercent', + 'DevicePowerPeakShiftDayConfig', + 'DeviceAdvancedBatteryChargeModeDayConfig', + 'PrintingPaperSizeDefault', + 'AutoLaunchProtocolsFromOrigins', + 'BrowsingDataLifetime', + 'DataLeakPreventionRulesList', + 'DeviceLoginScreenWebUsbAllowDevicesForUrls', + 'DeviceScheduledUpdateCheck', + 'KeyPermissions', + 'ManagedBookmarks', + 'ManagedConfigurationPerOrigin', + 'ProxySettings', + 'SystemProxySettings', + 'WebAppInstallForceList'] + bools = ['ShowAccessibilityOptionsInSystemTrayMenu', + 'LargeCursorEnabled', + 'SpokenFeedbackEnabled', + 'HighContrastEnabled', + 'VirtualKeyboardEnabled', + 'StickyKeysEnabled', + 'KeyboardDefaultToFunctionKeys', + 'DictationEnabled', + 'SelectToSpeakEnabled', + 'KeyboardFocusHighlightEnabled', + 'CursorHighlightEnabled', + 'CaretHighlightEnabled', + 'MonoAudioEnabled', + 'AccessibilityShortcutsEnabled', + 'AutoclickEnabled', + 'DeviceLoginScreenDefaultLargeCursorEnabled', + 'DeviceLoginScreenDefaultSpokenFeedbackEnabled', + 'DeviceLoginScreenDefaultHighContrastEnabled', + 'DeviceLoginScreenDefaultVirtualKeyboardEnabled', + 'DeviceLoginScreenLargeCursorEnabled', + 'DeviceLoginScreenSpokenFeedbackEnabled', + 'DeviceLoginScreenHighContrastEnabled', + 'DeviceLoginScreenVirtualKeyboardEnabled', + 'DeviceLoginScreenDictationEnabled', + 'DeviceLoginScreenSelectToSpeakEnabled', + 'DeviceLoginScreenCursorHighlightEnabled', + 'DeviceLoginScreenCaretHighlightEnabled', + 'DeviceLoginScreenMonoAudioEnabled', + 'DeviceLoginScreenAutoclickEnabled', + 'DeviceLoginScreenStickyKeysEnabled', + 'DeviceLoginScreenKeyboardFocusHighlightEnabled', + 'DeviceLoginScreenShowOptionsInSystemTrayMenu', + 'DeviceLoginScreenAccessibilityShortcutsEnabled', + 'FloatingAccessibilityMenuEnabled', + 'ArcEnabled', + 'UnaffiliatedArcAllowed', + 'AppRecommendationZeroStateEnabled', + 'DeviceBorealisAllowed', + 'UserBorealisAllowed', + 'SystemUse24HourClock', + 'DefaultSearchProviderEnabled', + 'ChromeOsReleaseChannelDelegated', + 'DeviceAutoUpdateDisabled', + 'DeviceAutoUpdateP2PEnabled', + 'DeviceUpdateHttpDownloadsEnabled', + 'RebootAfterUpdate', + 'BlockExternalExtensions', + 'VoiceInteractionContextEnabled', + 'VoiceInteractionHotwordEnabled', + 'EnableMediaRouter', + 'ShowCastIconInToolbar', + 'DriveDisabled', + 'DriveDisabledOverCellular', + 'DisableAuthNegotiateCnameLookup', + 'EnableAuthNegotiatePort', + 'BasicAuthOverHttpEnabled', + 'AuthNegotiateDelegateByKdcPolicy', + 'AllowCrossOriginAuthPrompt', + 'NtlmV2Enabled', + 'IntegratedWebAuthenticationAllowed', + 'BrowserSwitcherEnabled', + 'BrowserSwitcherKeepLastChromeTab', + 'BrowserSwitcherUseIeSitelist', + 'VirtualMachinesAllowed', + 'CrostiniAllowed', + 'DeviceUnaffiliatedCrostiniAllowed', + 'CrostiniExportImportUIAllowed', + 'CrostiniPortForwardingAllowed', + 'NativeMessagingUserLevelHosts', + 'NetworkFileSharesAllowed', + 'NetBiosShareDiscoveryEnabled', + 'NTLMShareAuthenticationEnabled', + 'DeviceDataRoamingEnabled', + 'DeviceWiFiFastTransitionEnabled', + 'DeviceWiFiAllowed', + 'DeviceAllowBluetooth', + 'DeviceAllowRedeemChromeOsRegistrationOffers', + 'DeviceQuirksDownloadEnabled', + 'SuggestedContentEnabled', + 'DeviceShowLowDiskSpaceNotification', + 'PasswordManagerEnabled', + 'PasswordLeakDetectionEnabled', + 'PluginVmAllowed', + 'PluginVmDataCollectionAllowed', + 'UserPluginVmAllowed', + 'DeviceRebootOnShutdown', + 'PowerManagementUsesAudioActivity', + 'PowerManagementUsesVideoActivity', + 'AllowWakeLocks', + 'AllowScreenWakeLocks', + 'WaitForInitialUserActivity', + 'PowerSmartDimEnabled', + 'DevicePowerPeakShiftEnabled', + 'DeviceBootOnAcEnabled', + 'DeviceAdvancedBatteryChargeModeEnabled', + 'DeviceUsbPowerShareEnabled', + 'PrintingEnabled', + 'CloudPrintProxyEnabled', + 'PrintingSendUsernameAndFilenameEnabled', + 'CloudPrintSubmitEnabled', + 'DisablePrintPreview', + 'PrintHeaderFooter', + 'PrintPreviewUseSystemDefaultPrinter', + 'UserNativePrintersAllowed', + 'UserPrintersAllowed', + 'DeletePrintJobHistoryAllowed', + 'DeviceLoginScreenPrivacyScreenEnabled', + 'PrivacyScreenEnabled', + 'PinUnlockWeakPinsAllowed', + 'PinUnlockAutosubmitEnabled', + 'RemoteAccessHostFirewallTraversal', + 'RemoteAccessHostRequireCurtain', + 'RemoteAccessHostAllowClientPairing', + 'RemoteAccessHostAllowRelayedConnection', + 'RemoteAccessHostAllowUiAccessForRemoteAssistance', + 'RemoteAccessHostAllowFileTransfer', + 'RemoteAccessHostAllowRemoteAccessConnections', + 'AttestationEnabledForUser', + 'SafeBrowsingEnabled', + 'SafeBrowsingExtendedReportingEnabled', + 'DeviceGuestModeEnabled', + 'DeviceAllowNewUsers', + 'DeviceShowUserNamesOnSignin', + 'DeviceEphemeralUsersEnabled', + 'DeviceShowNumericKeyboardForPassword', + 'DeviceFamilyLinkAccountsAllowed', + 'ShowHomeButton', + 'HomepageIsNewTabPage', + 'DeviceMetricsReportingEnabled', + 'DeviceWilcoDtcAllowed', + 'AbusiveExperienceInterventionEnforce', + 'AccessibilityImageLabelsEnabled', + 'AdditionalDnsQueryTypesEnabled', + 'AdvancedProtectionAllowed', + 'AllowDeletingBrowserHistory', + 'AllowDinosaurEasterEgg', + 'AllowFileSelectionDialogs', + 'AllowScreenLock', + 'AllowSyncXHRInPageDismissal', + 'AlternateErrorPagesEnabled', + 'AlwaysOpenPdfExternally', + 'AppCacheForceEnabled', + 'AudioCaptureAllowed', + 'AudioOutputAllowed', + 'AudioProcessHighPriorityEnabled', + 'AudioSandboxEnabled', + 'AutoFillEnabled', + 'AutofillAddressEnabled', + 'AutofillCreditCardEnabled', + 'AutoplayAllowed', + 'BackgroundModeEnabled', + 'BlockThirdPartyCookies', + 'BookmarkBarEnabled', + 'BrowserAddPersonEnabled', + 'BrowserGuestModeEnabled', + 'BrowserGuestModeEnforced', + 'BrowserLabsEnabled', + 'BrowserNetworkTimeQueriesEnabled', + 'BuiltInDnsClientEnabled', + 'CECPQ2Enabled', + 'CaptivePortalAuthenticationIgnoresProxy', + 'ChromeCleanupEnabled', + 'ChromeCleanupReportingEnabled', + 'ChromeOsLockOnIdleSuspend', + 'ClickToCallEnabled', + 'CloudManagementEnrollmentMandatory', + 'CloudPolicyOverridesPlatformPolicy', + 'CloudUserPolicyMerge', + 'CommandLineFlagSecurityWarningsEnabled', + 'ComponentUpdatesEnabled', + 'DNSInterceptionChecksEnabled', + 'DataLeakPreventionReportingEnabled', + 'DefaultBrowserSettingEnabled', + 'DefaultSearchProviderContextMenuAccessAllowed', + 'DeveloperToolsDisabled', + 'DeviceAllowMGSToStoreDisplayProperties', + 'DeviceDebugPacketCaptureAllowed', + 'DeviceLocalAccountManagedSessionEnabled', + 'DeviceLoginScreenPrimaryMouseButtonSwitch', + 'DevicePciPeripheralDataAccessEnabled', + 'DevicePowerwashAllowed', + 'DeviceSystemWideTracingEnabled', + 'Disable3DAPIs', + 'DisableSafeBrowsingProceedAnyway', + 'DisableScreenshots', + 'EasyUnlockAllowed', + 'EditBookmarksEnabled', + 'EmojiSuggestionEnabled', + 'EnableDeprecatedPrivetPrinting', + 'EnableOnlineRevocationChecks', + 'EnableSyncConsent', + 'EnterpriseHardwarePlatformAPIEnabled', + 'ExternalProtocolDialogShowAlwaysOpenCheckbox', + 'ExternalStorageDisabled', + 'ExternalStorageReadOnly', + 'ForceBrowserSignin', + 'ForceEphemeralProfiles', + 'ForceGoogleSafeSearch', + 'ForceMaximizeOnFirstRun', + 'ForceSafeSearch', + 'ForceYouTubeSafetyMode', + 'FullscreenAlertEnabled', + 'FullscreenAllowed', + 'GloballyScopeHTTPAuthCacheEnabled', + 'HardwareAccelerationModeEnabled', + 'HideWebStoreIcon', + 'ImportAutofillFormData', + 'ImportBookmarks', + 'ImportHistory', + 'ImportHomepage', + 'ImportSavedPasswords', + 'ImportSearchEngine', + 'IncognitoEnabled', + 'InsecureFormsWarningsEnabled', + 'InsecurePrivateNetworkRequestsAllowed', + 'InstantTetheringAllowed', + 'IntensiveWakeUpThrottlingEnabled', + 'JavascriptEnabled', + 'LacrosAllowed', + 'LacrosSecondaryProfilesAllowed', + 'LockScreenMediaPlaybackEnabled', + 'LoginDisplayPasswordButtonEnabled', + 'ManagedGuestSessionPrivacyWarningsEnabled', + 'MediaRecommendationsEnabled', + 'MediaRouterCastAllowAllIPs', + 'MetricsReportingEnabled', + 'NTPCardsVisible', + 'NTPCustomBackgroundEnabled', + 'NativeWindowOcclusionEnabled', + 'NearbyShareAllowed', + 'PaymentMethodQueryEnabled', + 'PdfAnnotationsEnabled', + 'PhoneHubAllowed', + 'PhoneHubNotificationsAllowed', + 'PhoneHubTaskContinuationAllowed', + 'PolicyAtomicGroupsEnabled', + 'PrimaryMouseButtonSwitch', + 'PromotionalTabsEnabled', + 'PromptForDownloadLocation', + 'QuicAllowed', + 'RendererCodeIntegrityEnabled', + 'RequireOnlineRevocationChecksForLocalAnchors', + 'RoamingProfileSupportEnabled', + 'SSLErrorOverrideAllowed', + 'SafeBrowsingForTrustedSourcesEnabled', + 'SavingBrowserHistoryDisabled', + 'ScreenCaptureAllowed', + 'ScrollToTextFragmentEnabled', + 'SearchSuggestEnabled', + 'SecondaryGoogleAccountSigninAllowed', + 'SharedArrayBufferUnrestrictedAccessAllowed', + 'SharedClipboardEnabled', + 'ShowAppsShortcutInBookmarkBar', + 'ShowFullUrlsInAddressBar', + 'ShowLogoutButtonInTray', + 'SignedHTTPExchangeEnabled', + 'SigninAllowed', + 'SigninInterceptionEnabled', + 'SitePerProcess', + 'SmartLockSigninAllowed', + 'SmsMessagesAllowed', + 'SpellCheckServiceEnabled', + 'SpellcheckEnabled', + 'StartupBrowserWindowLaunchSuppressed', + 'StricterMixedContentTreatmentEnabled', + 'SuggestLogoutAfterClosingLastWindow', + 'SuppressDifferentOriginSubframeDialogs', + 'SuppressUnsupportedOSWarning', + 'SyncDisabled', + 'TargetBlankImpliesNoOpener', + 'TaskManagerEndProcessEnabled', + 'ThirdPartyBlockingEnabled', + 'TouchVirtualKeyboardEnabled', + 'TranslateEnabled', + 'TripleDESEnabled', + 'UnifiedDesktopEnabledByDefault', + 'UrlKeyedAnonymizedDataCollectionEnabled', + 'UserAgentClientHintsEnabled', + 'UserFeedbackAllowed', + 'VideoCaptureAllowed', + 'VmManagementCliAllowed', + 'VpnConfigAllowed', + 'WPADQuickCheckEnabled', + 'WebRtcAllowLegacyTLSProtocols', + 'WebRtcEventLogCollectionAllowed', + 'WifiSyncAndroidAllowed', + 'WindowOcclusionEnabled'] + if name in dict_entries: + return json.loads(get_string(e.data)) + elif e.type == misc.REG_DWORD and name in bools: + return e.data == 1 + return e.data + +def assign_entry(policies, e): + if e.valuename.isnumeric(): + name = e.keyname.split('\\')[-1] + if name not in policies: + policies[name] = [] + policies[name].append(parse_entry_data(name, e)) + else: + name = e.valuename + policies[name] = parse_entry_data(name, e) + +def convert_pol_to_json(managed, recommended, section, entries): + recommended_section = '\\'.join([section, 'Recommended']) + for e in entries: + if '**delvals.' in e.valuename: + continue + if e.keyname.startswith(recommended_section): + assign_entry(recommended, e) + elif e.keyname.startswith(section): + assign_entry(managed, e) + return managed, recommended + +class gp_chromium_ext(gp_pol_ext): + __managed_policies_path = '/etc/chromium/policies/managed' + __recommended_policies_path = '/etc/chromium/policies/recommended' + + def __str__(self): + return 'Google/Chromium' + + def set_managed_machine_policy(self, managed): + try: + managed_policies = os.path.join(self.__managed_policies_path, + 'policies.json') + os.makedirs(self.__managed_policies_path, exist_ok=True) + with open(managed_policies, 'w') as f: + json.dump(managed, f) + log.debug('Wrote Chromium preferences', managed_policies) + except PermissionError: + log.debug('Failed to write Chromium preferences', + managed_policies) + + + def set_recommended_machine_policy(self, recommended): + try: + recommended_policies = os.path.join(self.__recommended_policies_path, + 'policies.json') + os.makedirs(self.__recommended_policies_path, exist_ok=True) + with open(recommended_policies, 'w') as f: + json.dump(recommended, f) + log.debug('Wrote Chromium preferences', recommended_policies) + except PermissionError: + log.debug('Failed to write Chromium preferences', + recommended_policies) + + def get_managed_machine_policy(self): + managed_policies = os.path.join(self.__managed_policies_path, + 'policies.json') + if os.path.exists(managed_policies): + with open(managed_policies, 'r') as r: + managed = json.load(r) + log.debug('Read Chromium preferences', managed_policies) + else: + managed = {} + return managed + + def get_recommended_machine_policy(self): + recommended_policies = os.path.join(self.__recommended_policies_path, + 'policies.json') + if os.path.exists(recommended_policies): + with open(recommended_policies, 'r') as r: + recommended = json.load(r) + log.debug('Read Chromium preferences', recommended_policies) + else: + recommended = {} + return recommended + + def process_group_policy(self, deleted_gpo_list, changed_gpo_list, + policy_dir=None): + if policy_dir is not None: + self.__recommended_policies_path = os.path.join(policy_dir, + 'recommended') + self.__managed_policies_path = os.path.join(policy_dir, 'managed') + for guid, settings in deleted_gpo_list: + self.gp_db.set_guid(guid) + if str(self) in settings: + for attribute, policies in settings[str(self)].items(): + if attribute == 'managed': + self.set_managed_machine_policy(json.loads(policies)) + elif attribute == 'recommended': + self.set_recommended_machine_policy(json.loads(policies)) + self.gp_db.delete(str(self), attribute) + self.gp_db.commit() + + for gpo in changed_gpo_list: + if gpo.file_sys_path: + section = 'Software\\Policies\\Google\\Chrome' + self.gp_db.set_guid(gpo.name) + pol_file = 'MACHINE/Registry.pol' + path = os.path.join(gpo.file_sys_path, pol_file) + pol_conf = self.parse(path) + if not pol_conf: + continue + + managed = self.get_managed_machine_policy() + recommended = self.get_recommended_machine_policy() + self.gp_db.store(str(self), 'managed', json.dumps(managed)) + self.gp_db.store(str(self), 'recommended', + json.dumps(recommended)) + managed, recommended = convert_pol_to_json(managed, + recommended, section, + pol_conf.entries) + self.set_managed_machine_policy(managed) + self.set_recommended_machine_policy(recommended) + self.gp_db.commit() + + def rsop(self, gpo): + output = {} + pol_file = 'MACHINE/Registry.pol' + section = 'Software\\Policies\\Google\\Chrome' + if gpo.file_sys_path: + path = os.path.join(gpo.file_sys_path, pol_file) + pol_conf = self.parse(path) + if not pol_conf: + return output + for e in pol_conf.entries: + if e.keyname.startswith(section): + output['%s\\%s' % (e.keyname, e.valuename)] = e.data + return output + +class gp_chrome_ext(gp_chromium_ext): + __managed_policies_path = '/etc/opt/chrome/policies/managed' + __recommended_policies_path = '/etc/opt/chrome/policies/recommended' + + def __str__(self): + return 'Google/Chrome' diff --git a/python/samba/gp/gp_ext_loader.py b/python/samba/gp/gp_ext_loader.py new file mode 100644 index 00000000000..bd702de55a5 --- /dev/null +++ b/python/samba/gp/gp_ext_loader.py @@ -0,0 +1,59 @@ +# Group Policy Client Side Extension Loader +# Copyright (C) David Mulder 2018 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from samba.gp.gpclass import list_gp_extensions +from samba.gp.gpclass import gp_ext +from samba.gp.util.logging import log + +try: + import importlib.util + + def import_file(name, location): + spec = importlib.util.spec_from_file_location(name, location) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module +except ImportError: + import imp + + def import_file(name, location): + return imp.load_source(name, location) + + +def get_gp_ext_from_module(name, mod): + if mod: + for k, v in vars(mod).items(): + if k == name and issubclass(v, gp_ext): + return v + return None + + +def get_gp_client_side_extensions(smb_conf): + user_exts = [] + machine_exts = [] + gp_exts = list_gp_extensions(smb_conf) + for gp_ext in gp_exts.values(): + module = import_file(gp_ext['ProcessGroupPolicy'], gp_ext['DllName']) + ext = get_gp_ext_from_module(gp_ext['ProcessGroupPolicy'], module) + if ext and gp_ext['MachinePolicy']: + machine_exts.append(ext) + log.info('Loaded machine extension from %s: %s' + % (gp_ext['DllName'], ext.__name__)) + if ext and gp_ext['UserPolicy']: + user_exts.append(ext) + log.info('Loaded user extension from %s: %s' + % (gp_ext['DllName'], ext.__name__)) + return (machine_exts, user_exts) diff --git a/pyth