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