source4/heimdal_build/wscript_configure: update to handle waf 2.0.4
[Samba.git] / python / samba / ms_forest_updates_markdown.py
blob3afe2ecb8387c5c12cc3c939d807df3f17333cf4
1 # Create forest updates ldif from Github markdown
3 # Each update is converted to an ldif then gets written to a corresponding
4 # .LDF output file or stored in a dictionary.
6 # Only add updates can generally be applied.
8 # Copyright (C) Andrew Bartlett <abartlet@samba.org> 2017
10 # This program is free software; you can redistribute it and/or modify
11 # it under the terms of the GNU General Public License as published by
12 # the Free Software Foundation; either version 3 of the License, or
13 # (at your option) any later version.
15 # This program is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 # GNU General Public License for more details.
20 # You should have received a copy of the GNU General Public License
21 # along with this program. If not, see <http://www.gnu.org/licenses/>.
23 from __future__ import print_function
24 """Generate LDIF from Github documentation."""
26 import re
27 import os
28 import markdown
29 import xml.etree.ElementTree as ET
32 # Display specifier updates or otherwise (ignored in forest_update.py)
33 def noop(description, attributes, sd):
34 return (None, None, [], None)
37 # ACE addition updates (ignored in forest_update.py)
38 def parse_grant(description, attributes, sd):
39 return ('modify', None, [], sd if sd.lower() != 'n/a' else None)
42 # Addition of new objects to the directory (most are applied in forest_update.py)
43 def parse_add(description, attributes, sd):
44 dn = extract_dn(description)
45 return ('add', dn, extract_attrib(dn, attributes), sd if sd.lower() != 'n/a' else None)
48 # Set of a particular attribute (ignored in forest_update.py)
49 def parse_set(description, attributes, sd):
50 return ('modify', extract_dn_or_none(description),
51 extract_replace_attrib(attributes),
52 sd if sd.lower() != 'n/a' else None)
55 # Set of a particular ACE (ignored in forest_update.py)
56 # The general issue is that the list of DNs must be generated dynamically
57 def parse_ace(description, attributes, sd):
59 def extract_dn_ace(text):
60 if 'Sam-Domain' in text:
61 return ('${DOMAIN_DN}', 'CN=Sam-Domain,${SCHEMA_DN}')
62 elif 'Domain-DNS' in text:
63 return ('${...}', 'CN=Domain-DNS,${SCHEMA_DN}')
65 return None
67 return [('modify', extract_dn_ace(description)[0],
68 ['replace: nTSecurityDescriptor',
69 'nTSecurityDescriptor: ${DOMAIN_SCHEMA_SD}%s' % sd], None),
70 ('modify', extract_dn_ace(description)[1],
71 ['replace: defaultSecurityDescriptor',
72 'defaultSecurityDescriptor: ${OLD_SAMBA_SD}%s' % sd], None)]
75 # We are really only interested in 'Created' items
76 operation_map = {
77 # modify
78 'Granting': parse_grant,
79 # add
80 'Created': parse_add,
81 # modify
82 'Set': parse_set,
83 # modify
84 'Added ACE': parse_ace,
85 # modify
86 'Updated': parse_set,
87 # unknown
88 'Call': noop
92 def extract_dn(text):
93 """
94 Extract a DN from the textual description
95 :param text:
96 :return: DN in string form
97 """
98 text = text.replace(' in the Schema partition.', ',${SCHEMA_DN}')
99 text = text.replace(' in the Configuration partition.', ',${CONFIG_DN}')
100 dn = re.search('([CDO][NCU]=.*?,)*([CDO][NCU]=.*)', text).group(0)
102 # This should probably be also fixed upstream
103 if dn == 'CN=ad://ext/AuthenticationSilo,CN=Claim Types,CN=Claims Configuration,CN=Services':
104 return 'CN=ad://ext/AuthenticationSilo,CN=Claim Types,CN=Claims Configuration,CN=Services,${CONFIG_DN}'
106 return dn
109 def extract_dn_or_none(text):
111 Same as above, but returns None if it doesn't work
112 :param text:
113 :return: DN or None
115 try:
116 return extract_dn(text)
117 except:
118 return None
121 def save_ldif(filename, answers, out_folder):
123 Save ldif to disk for each updates
124 :param filename: filename use ([OPERATION NUM]-{GUID}.ldif)
125 :param answers: array of tuples generated with earlier functions
126 :param out_folder: folder to prepend
128 path = os.path.join(out_folder, filename)
129 with open(path, 'w') as ldif:
130 for answer in answers:
131 change, dn, attrib, sd = answer
132 ldif.write('dn: %s\n' % dn)
133 ldif.write('changetype: %s\n' % change)
134 if len(attrib) > 0:
135 ldif.write('\n'.join(attrib) + '\n')
136 if sd is not None:
137 ldif.write('nTSecurityDescriptor: D:%s\n' % sd)
138 ldif.write('-\n\n')
141 def save_array(guid, answers, out_dict):
143 Save ldif to an output dictionary
144 :param guid: GUID to store
145 :param answers: array of tuples generated with earlier functions
146 :param out_dict: output dictionary
148 ldif = ''
149 for answer in answers:
150 change, dn, attrib, sd = answer
151 ldif += 'dn: %s\n' % dn
152 ldif += 'changetype: %s\n' % change
153 if len(attrib) > 0:
154 ldif += '\n'.join(attrib) + '\n'
155 if sd is not None:
156 ldif += 'nTSecurityDescriptor: D:%s\n' % sd
157 ldif += '-\n\n'
159 out_dict[guid] = ldif
162 def extract_attrib(dn, attributes):
164 Extract the attributes as an array from the attributes column
165 :param dn: parsed from markdown
166 :param attributes: from markdown
167 :return: attribute array (ldif-type format)
169 attrib = [x.lstrip('- ') for x in attributes.split('- ') if x.lower() != 'n/a' and x != '']
170 attrib = [x.replace(': True', ': TRUE') if x.endswith(': True') else x for x in attrib]
171 attrib = [x.replace(': False', ': FALSE') if x.endswith(': False') else x for x in attrib]
172 # We only have one such value, we may as well skip them all consistently
173 attrib = [x for x in attrib if not x.lower().startswith('msds-claimpossiblevalues')]
175 return attrib
178 def extract_replace_attrib(attributes):
180 Extract the attributes as an array from the attributes column
181 (for replace)
182 :param attributes: from markdown
183 :return: attribute array (ldif-type format)
185 lines = [x.lstrip('- ') for x in attributes.split('- ') if x.lower() != 'n/a' and x != '']
186 lines = [('replace: %s' % line.split(':')[0], line) for line in lines]
187 lines = [line for pair in lines for line in pair]
188 return lines
191 def innertext(tag):
192 return (tag.text or '') + \
193 ''.join(innertext(e) for e in tag) + \
194 (tag.tail or '')
197 def read_ms_markdown(in_file, out_folder=None, out_dict={}):
199 Read Github documentation to produce forest wide udpates
200 :param in_file: Forest-Wide-Updates.md
201 :param out_folder: output folder
202 :param out_dict: output dictionary
205 with open(in_file) as update_file:
206 # There is a hidden ClaimPossibleValues in this md file
207 html = markdown.markdown(re.sub(r'CN=<forest root domain.*?>',
208 '${FOREST_ROOT_DOMAIN}',
209 update_file.read()),
210 output_format='xhtml')
212 html = html.replace('CN=Schema,%ws', '${SCHEMA_DN}')
214 tree = ET.fromstring('<root>' + html + '</root>')
216 for node in tree:
217 if node.text and node.text.startswith('|Operation'):
218 # Strip first and last |
219 updates = [x[1:len(x) - 1].split('|') for x in
220 ET.tostring(node, method='text').splitlines()]
221 for update in updates[2:]:
222 output = re.match('Operation (\d+): {(.*)}', update[0])
223 if output:
224 # print output.group(1), output.group(2)
225 guid = output.group(2)
226 filename = "%s-{%s}.ldif" % (output.group(1).zfill(4), guid)
228 found = False
230 if update[3].startswith('Created') or update[1].startswith('Added ACE'):
231 # Trigger the security descriptor code
232 # Reduce info to just the security descriptor
233 update[3] = update[3].split(':')[-1]
235 result = parse_ace(update[1], update[2], update[3])
237 if filename and out_folder is not None:
238 save_ldif(filename, result, out_folder)
239 else:
240 save_array(guid, result, out_dict)
242 continue
244 for operation in operation_map:
245 if update[1].startswith(operation):
246 found = True
248 result = operation_map[operation](update[1], update[2], update[3])
250 if filename and out_folder is not None:
251 save_ldif(filename, [result], out_folder)
252 else:
253 save_array(guid, [result], out_dict)
255 break
257 if not found:
258 raise Exception(update)
260 # print ET.tostring(node, method='text')
263 if __name__ == '__main__':
264 import sys
266 out_folder = ''
268 if len(sys.argv) == 0:
269 print("Usage: %s <Forest-Wide-Updates.md> [<output folder>]" % (sys.argv[0]), file=sys.stderr)
270 sys.exit(1)
272 in_file = sys.argv[1]
273 if len(sys.argv) > 2:
274 out_folder = sys.argv[2]
276 read_ms_markdown(in_file, out_folder)