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