1# GPO Parser for generic extensions 2# 3# Copyright (C) Andrew Bartlett <abartlet@samba.org> 2018 4# Written by Garming Sam <garming@catalyst.net.nz> 5# 6# This program is free software; you can redistribute it and/or modify 7# it under the terms of the GNU General Public License as published by 8# the Free Software Foundation; either version 3 of the License, or 9# (at your option) any later version. 10# 11# This program is distributed in the hope that it will be useful, 12# but WITHOUT ANY WARRANTY; without even the implied warranty of 13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14# GNU General Public License for more details. 15# 16# You should have received a copy of the GNU General Public License 17# along with this program. If not, see <http://www.gnu.org/licenses/>. 18# 19 20from xml.dom import minidom 21from io import BytesIO 22from xml.etree.ElementTree import ElementTree, fromstring, tostring 23from hashlib import md5 24from samba.compat import get_bytes 25 26 27ENTITY_USER_ID = 0 28ENTITY_SDDL_ACL = 1 29ENTITY_NETWORK_PATH = 2 30 31 32class GPNoParserException(Exception): 33 pass 34 35class GPGeneralizeException(Exception): 36 pass 37 38 39def entity_type_to_string(ent_type): 40 type_str = None 41 42 if ent_type == ENTITY_USER_ID: 43 type_str = "USER_ID" 44 elif ent_type == ENTITY_SDDL_ACL: 45 type_str = "SDDL_ACL" 46 elif ent_type == ENTITY_NETWORK_PATH: 47 type_str = "NETWORK_PATH" 48 49 return type_str 50 51 52# [MS-GPIPSEC] (LDAP) 53# [MS-GPDPC] Deployed Printer Connections (LDAP) 54# [MS-GPPREF] Preferences Extension (XML) 55# [MS-GPWL] Wireless/Wired Protocol Extension (LDAP) 56class GPParser(object): 57 encoding = 'utf-16' 58 output_encoding = 'utf-8' 59 60 def parse(self, contents): 61 pass 62 63 def write_xml(self, filename): 64 with open(filename, 'w') as f: 65 f.write('<?xml version="1.0" encoding="utf-8"?><UnknownFile/>') 66 67 def load_xml(self, filename): 68 pass 69 70 def write_binary(self, filename): 71 raise GPNoParserException("This file has no parser available.") 72 73 def write_pretty_xml(self, xml_element, handle): 74 # Add the xml header as well as format it nicely. 75 # ElementTree doesn't have a pretty-print, so use minidom. 76 77 et = ElementTree(xml_element) 78 temporary_bytes = BytesIO() 79 et.write(temporary_bytes, encoding=self.output_encoding, 80 xml_declaration=True) 81 minidom_parsed = minidom.parseString(temporary_bytes.getvalue()) 82 handle.write(minidom_parsed.toprettyxml(encoding=self.output_encoding)) 83 84 def new_xml_entity(self, name, ent_type): 85 identifier = md5(get_bytes(name)).hexdigest() 86 87 type_str = entity_type_to_string(ent_type) 88 89 if type_str is None: 90 raise GPGeneralizeException("No such entity type") 91 92 # For formattting reasons, align the length of the entities 93 longest = entity_type_to_string(ENTITY_NETWORK_PATH) 94 type_str = type_str.center(len(longest), '_') 95 96 return "&SAMBA__{}__{}__;".format(type_str, identifier) 97 98 def generalize_xml(self, root, out_file, global_entities): 99 entities = [] 100 101 # Locate all user_id and all ACLs 102 user_ids = root.findall('.//*[@user_id="TRUE"]') 103 user_ids.sort(key = lambda x: x.tag) 104 105 for elem in user_ids: 106 old_text = elem.text 107 if old_text is None or old_text == '': 108 continue 109 110 if old_text in global_entities: 111 elem.text = global_entities[old_text] 112 entities.append((elem.text, old_text)) 113 else: 114 elem.text = self.new_xml_entity(old_text, 115 ENTITY_USER_ID) 116 117 entities.append((elem.text, old_text)) 118 global_entities.update([(old_text, elem.text)]) 119 120 acls = root.findall('.//*[@acl="TRUE"]') 121 acls.sort(key = lambda x: x.tag) 122 123 for elem in acls: 124 old_text = elem.text 125 126 if old_text is None or old_text == '': 127 continue 128 129 if old_text in global_entities: 130 elem.text = global_entities[old_text] 131 entities.append((elem.text, old_text)) 132 else: 133 elem.text = self.new_xml_entity(old_text, 134 ENTITY_SDDL_ACL) 135 136 entities.append((elem.text, old_text)) 137 global_entities.update([(old_text, elem.text)]) 138 139 share_paths = root.findall('.//*[@network_path="TRUE"]') 140 share_paths.sort(key = lambda x: x.tag) 141 142 for elem in share_paths: 143 old_text = elem.text 144 145 if old_text is None or old_text == '': 146 continue 147 148 stripped = old_text.lstrip('\\') 149 file_server = stripped.split('\\')[0] 150 151 server_index = old_text.find(file_server) 152 153 remaining = old_text[server_index + len(file_server):] 154 old_text = old_text[:server_index] + file_server 155 156 if old_text in global_entities: 157 elem.text = global_entities[old_text] + remaining 158 to_put = global_entities[old_text] 159 entities.append((to_put, old_text)) 160 else: 161 to_put = self.new_xml_entity(old_text, 162 ENTITY_NETWORK_PATH) 163 elem.text = to_put + remaining 164 165 entities.append((to_put, old_text)) 166 global_entities.update([(old_text, to_put)]) 167 168 # Call any file specific customization of entities 169 # (which appear in any subclasses). 170 entities.extend(self.custom_entities(root, global_entities)) 171 172 output_xml = tostring(root) 173 174 for ent in entities: 175 entb = get_bytes(ent[0]) 176 output_xml = output_xml.replace(entb.replace(b'&', b'&'), entb) 177 178 with open(out_file, 'wb') as f: 179 f.write(output_xml) 180 181 return entities 182 183 def custom_entities(self, root, global_entities): 184 # Override this method to do special entity handling 185 return [] 186