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'&amp;'), 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