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."""
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}')
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
78 'Granting': parse_grant
,
84 'Added ACE': parse_ace
,
94 Extract a DN from the textual description
96 :return: DN in string form
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}'
109 def extract_dn_or_none(text
):
111 Same as above, but returns None if it doesn't work
116 return extract_dn(text
)
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
)
135 ldif
.write('\n'.join(attrib
) + '\n')
137 ldif
.write('nTSecurityDescriptor: D:%s\n' % sd
)
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
149 for answer
in answers
:
150 change
, dn
, attrib
, sd
= answer
151 ldif
+= 'dn: %s\n' % dn
152 ldif
+= 'changetype: %s\n' % change
154 ldif
+= '\n'.join(attrib
) + '\n'
156 ldif
+= 'nTSecurityDescriptor: D:%s\n' % sd
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')]
178 def extract_replace_attrib(attributes
):
180 Extract the attributes as an array from the attributes column
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
]
192 return (tag
.text
or '') + \
193 ''.join(innertext(e
) for e
in tag
) + \
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}',
210 output_format
='xhtml')
212 html
= html
.replace('CN=Schema,%ws', '${SCHEMA_DN}')
214 tree
= ET
.fromstring('<root>' + html
+ '</root>')
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])
224 # print output.group(1), output.group(2)
225 guid
= output
.group(2)
226 filename
= "%s-{%s}.ldif" % (output
.group(1).zfill(4), guid
)
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
)
240 save_array(guid
, result
, out_dict
)
244 for operation
in operation_map
:
245 if update
[1].startswith(operation
):
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
)
253 save_array(guid
, [result
], out_dict
)
258 raise Exception(update
)
260 # print ET.tostring(node, method='text')
263 if __name__
== '__main__':
268 if len(sys
.argv
) == 0:
269 print("Usage: %s <Forest-Wide-Updates.md> [<output folder>]" % (sys
.argv
[0]), file=sys
.stderr
)
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
)