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