PEP8: fix E303: too many blank lines (2)
[samba.git] / python / samba / ms_schema.py
1 # create schema.ldif (as a string) from WSPP documentation
2 #
3 # based on minschema.py and minschema_wspp
4 #
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 3 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
17
18 from __future__ import print_function
19 """Generate LDIF from WSPP documentation."""
20
21 import re
22 import base64
23 import uuid
24 from samba.compat import string_types
25
26 bitFields = {}
27
28 # ADTS: 2.2.9
29 # bit positions as labeled in the docs
30 bitFields["searchflags"] = {
31     'fATTINDEX': 31,         # IX
32     'fPDNTATTINDEX': 30,     # PI
33     'fANR': 29,  # AR
34     'fPRESERVEONDELETE': 28,         # PR
35     'fCOPY': 27,     # CP
36     'fTUPLEINDEX': 26,       # TP
37     'fSUBTREEATTINDEX': 25,  # ST
38     'fCONFIDENTIAL': 24,     # CF
39     'fCONFIDENTAIL': 24,  # typo
40     'fNEVERVALUEAUDIT': 23,  # NV
41     'fRODCAttribute': 22,    # RO
42
43
44     # missing in ADTS but required by LDIF
45     'fRODCFilteredAttribute': 22,    # RO
46     'fRODCFILTEREDATTRIBUTE': 22,  # case
47     'fEXTENDEDLINKTRACKING': 21,  # XL
48     'fBASEONLY': 20,  # BO
49     'fPARTITIONSECRET': 19,  # SE
50 }
51
52 # ADTS: 2.2.10
53 bitFields["systemflags"] = {
54     'FLAG_ATTR_NOT_REPLICATED': 31, 'FLAG_CR_NTDS_NC': 31,     # NR
55     'FLAG_ATTR_REQ_PARTIAL_SET_MEMBER': 30, 'FLAG_CR_NTDS_DOMAIN': 30,     # PS
56     'FLAG_ATTR_IS_CONSTRUCTED': 29, 'FLAG_CR_NTDS_NOT_GC_REPLICATED': 29,     # CS
57     'FLAG_ATTR_IS_OPERATIONAL': 28,     # OP
58     'FLAG_SCHEMA_BASE_OBJECT': 27,     # BS
59     'FLAG_ATTR_IS_RDN': 26,     # RD
60     'FLAG_DISALLOW_MOVE_ON_DELETE': 6,     # DE
61     'FLAG_DOMAIN_DISALLOW_MOVE': 5,     # DM
62     'FLAG_DOMAIN_DISALLOW_RENAME': 4,     # DR
63     'FLAG_CONFIG_ALLOW_LIMITED_MOVE': 3,     # AL
64     'FLAG_CONFIG_ALLOW_MOVE': 2,     # AM
65     'FLAG_CONFIG_ALLOW_RENAME': 1,     # AR
66     'FLAG_DISALLOW_DELETE': 0     # DD
67 }
68
69 # ADTS: 2.2.11
70 bitFields["schemaflagsex"] = {
71     'FLAG_ATTR_IS_CRITICAL': 31
72 }
73
74 # ADTS: 3.1.1.2.2.2
75 oMObjectClassBER = {
76     '1.3.12.2.1011.28.0.702': base64.b64encode(b'\x2B\x0C\x02\x87\x73\x1C\x00\x85\x3E').decode('utf8'),
77     '1.2.840.113556.1.1.1.12': base64.b64encode(b'\x2A\x86\x48\x86\xF7\x14\x01\x01\x01\x0C').decode('utf8'),
78     '2.6.6.1.2.5.11.29': base64.b64encode(b'\x56\x06\x01\x02\x05\x0B\x1D').decode('utf8'),
79     '1.2.840.113556.1.1.1.11': base64.b64encode(b'\x2A\x86\x48\x86\xF7\x14\x01\x01\x01\x0B').decode('utf8'),
80     '1.3.12.2.1011.28.0.714': base64.b64encode(b'\x2B\x0C\x02\x87\x73\x1C\x00\x85\x4A').decode('utf8'),
81     '1.3.12.2.1011.28.0.732': base64.b64encode(b'\x2B\x0C\x02\x87\x73\x1C\x00\x85\x5C').decode('utf8'),
82     '1.2.840.113556.1.1.1.6': base64.b64encode(b'\x2A\x86\x48\x86\xF7\x14\x01\x01\x01\x06').decode('utf8')
83 }
84
85 # separated by commas in docs, and must be broken up
86 multivalued_attrs = set(["auxiliaryclass", "maycontain", "mustcontain", "posssuperiors",
87                          "systemauxiliaryclass", "systemmaycontain", "systemmustcontain",
88                          "systemposssuperiors"])
89
90
91 def __read_folded_line(f, buffer):
92     """ reads a line from an LDIF file, unfolding it"""
93     line = buffer
94
95     while True:
96         l = f.readline()
97
98         if l[:1] == " ":
99             # continued line
100
101             # cannot fold an empty line
102             assert(line != "" and line != "\n")
103
104             # preserves '\n '
105             line = line + l
106         else:
107             # non-continued line
108             if line == "":
109                 line = l
110
111                 if l == "":
112                     # eof, definitely won't be folded
113                     break
114             else:
115                 # marks end of a folded line
116                 # line contains the now unfolded line
117                 # buffer contains the start of the next possibly folded line
118                 buffer = l
119                 break
120
121     return (line, buffer)
122
123
124 def __read_raw_entries(f):
125     """reads an LDIF entry, only unfolding lines"""
126     import sys
127
128     # will not match options after the attribute type
129     attr_type_re = re.compile("^([A-Za-z]+[A-Za-z0-9-]*):")
130
131     buffer = ""
132
133     while True:
134         entry = []
135
136         while True:
137             (l, buffer) = __read_folded_line(f, buffer)
138
139             if l[:1] == "#":
140                 continue
141
142             if l == "\n" or l == "":
143                 break
144
145             m = attr_type_re.match(l)
146
147             if m:
148                 if l[-1:] == "\n":
149                     l = l[:-1]
150
151                 entry.append(l)
152             else:
153                 print("Invalid line: %s" % l, end=' ', file=sys.stderr)
154                 sys.exit(1)
155
156         if len(entry):
157             yield entry
158
159         if l == "":
160             break
161
162
163 def fix_dn(dn):
164     """fix a string DN to use ${SCHEMADN}"""
165
166     # folding?
167     if dn.find("<RootDomainDN>") != -1:
168         dn = dn.replace("\n ", "")
169         dn = dn.replace(" ", "")
170         return dn.replace("CN=Schema,CN=Configuration,<RootDomainDN>", "${SCHEMADN}")
171     elif dn.endswith("DC=X"):
172         return dn.replace("CN=Schema,CN=Configuration,DC=X", "${SCHEMADN}")
173     elif dn.endswith("CN=X"):
174         return dn.replace("CN=Schema,CN=Configuration,CN=X", "${SCHEMADN}")
175     else:
176         return dn
177
178
179 def __convert_bitfield(key, value):
180     """Evaluate the OR expression in 'value'"""
181     assert(isinstance(value, string_types))
182
183     value = value.replace("\n ", "")
184     value = value.replace(" ", "")
185
186     try:
187         # some attributes already have numeric values
188         o = int(value)
189     except ValueError:
190         o = 0
191         flags = value.split("|")
192         for f in flags:
193             bitpos = bitFields[key][f]
194             o = o | (1 << (31 - bitpos))
195
196     return str(o)
197
198
199 def __write_ldif_one(entry):
200     """Write out entry as LDIF"""
201     out = []
202
203     for l in entry:
204         if isinstance(l[1], string_types):
205             vl = [l[1]]
206         else:
207             vl = l[1]
208
209         if l[2]:
210             out.append("%s:: %s" % (l[0], l[1]))
211             continue
212
213         for v in vl:
214             out.append("%s: %s" % (l[0], v))
215
216     return "\n".join(out)
217
218
219 def __transform_entry(entry, objectClass):
220     """Perform transformations required to convert the LDIF-like schema
221        file entries to LDIF, including Samba-specific stuff."""
222
223     entry = [l.split(":", 1) for l in entry]
224
225     cn = ""
226     skip_dn = skip_objectclass = skip_admin_description = skip_admin_display_name = False
227
228     for l in entry:
229         if l[1].startswith(': '):
230             l.append(True)
231             l[1] = l[1][2:]
232         else:
233             l.append(False)
234
235         key = l[0].lower()
236         l[1] = l[1].lstrip()
237         l[1] = l[1].rstrip()
238
239         if not cn and key == "cn":
240             cn = l[1]
241
242         if key in multivalued_attrs:
243             # unlike LDIF, these are comma-separated
244             l[1] = l[1].replace("\n ", "")
245             l[1] = l[1].replace(" ", "")
246
247             l[1] = l[1].split(",")
248
249         if key in bitFields:
250             l[1] = __convert_bitfield(key, l[1])
251
252         if key == "omobjectclass":
253             if not l[2]:
254                 l[1] = oMObjectClassBER[l[1].strip()]
255                 l[2] = True
256
257         if isinstance(l[1], string_types):
258             l[1] = fix_dn(l[1])
259
260         if key == 'dn':
261             skip_dn = True
262             dn = l[1]
263
264         if key == 'objectclass':
265             skip_objectclass = True
266         elif key == 'admindisplayname':
267             skip_admin_display_name = True
268         elif key == 'admindescription':
269             skip_admin_description = True
270
271     assert(cn)
272
273     header = []
274     if not skip_dn:
275         header.append(["dn", "CN=%s,${SCHEMADN}" % cn, False])
276     else:
277         header.append(["dn", dn, False])
278
279     if not skip_objectclass:
280         header.append(["objectClass", ["top", objectClass], False])
281     if not skip_admin_description:
282         header.append(["adminDescription", cn, False])
283     if not skip_admin_display_name:
284         header.append(["adminDisplayName", cn, False])
285
286     header.append(["objectGUID", str(uuid.uuid4()), False])
287
288     entry = header + [x for x in entry if x[0].lower() not in set(['dn', 'changetype', 'objectcategory'])]
289
290     return entry
291
292
293 def __parse_schema_file(filename, objectClass):
294     """Load and transform a schema file."""
295
296     out = []
297
298     f = open(filename, "rU")
299     for entry in __read_raw_entries(f):
300         out.append(__write_ldif_one(__transform_entry(entry, objectClass)))
301
302     return "\n\n".join(out)
303
304
305 def read_ms_schema(attr_file, classes_file, dump_attributes=True, dump_classes=True, debug=False):
306     """Read WSPP documentation-derived schema files."""
307
308     attr_ldif = ""
309     classes_ldif = ""
310
311     if dump_attributes:
312         attr_ldif = __parse_schema_file(attr_file, "attributeSchema")
313     if dump_classes:
314         classes_ldif = __parse_schema_file(classes_file, "classSchema")
315
316     return attr_ldif + "\n\n" + classes_ldif + "\n\n"
317
318 if __name__ == '__main__':
319     import sys
320
321     try:
322         attr_file = sys.argv[1]
323         classes_file = sys.argv[2]
324     except IndexError:
325         print("Usage: %s attr-file.txt classes-file.txt" % (sys.argv[0]), file=sys.stderr)
326         sys.exit(1)
327
328     print(read_ms_schema(attr_file, classes_file))