pyldb: avoid segfault when adding an element with no name
[kai/samba-autobuild/.git] / source4 / scripting / bin / minschema
1 #!/usr/bin/env python3
2
3 # Works out the minimal schema for a set of objectclasses
4 #
5 from __future__ import print_function
6 import base64
7 import optparse
8 import sys
9
10 # Find right directory when running from source tree
11 sys.path.insert(0, "bin/python")
12
13 import samba
14 from samba import getopt as options, Ldb
15 from ldb import SCOPE_SUBTREE, SCOPE_BASE, LdbError
16 import sys
17
18 parser = optparse.OptionParser("minschema <URL> <classfile>")
19 sambaopts = options.SambaOptions(parser)
20 parser.add_option_group(sambaopts)
21 credopts = options.CredentialsOptions(parser)
22 parser.add_option_group(credopts)
23 parser.add_option_group(options.VersionOptions(parser))
24 parser.add_option("--verbose", help="Be verbose", action="store_true")
25 parser.add_option("--dump-classes", action="store_true")
26 parser.add_option("--dump-attributes", action="store_true")
27 parser.add_option("--dump-subschema", action="store_true")
28 parser.add_option("--dump-subschema-auto", action="store_true")
29
30 opts, args = parser.parse_args()
31 opts.dump_all = True
32
33 if opts.dump_classes:
34     opts.dump_all = False
35 if opts.dump_attributes:
36     opts.dump_all = False
37 if opts.dump_subschema:
38     opts.dump_all = False
39 if opts.dump_subschema_auto:
40     opts.dump_all = False
41     opts.dump_subschema = True
42 if opts.dump_all:
43     opts.dump_classes = True
44     opts.dump_attributes = True
45     opts.dump_subschema = True
46     opts.dump_subschema_auto = True
47
48 if len(args) != 2:
49     parser.print_usage()
50     sys.exit(1)
51
52 (url, classfile) = args
53
54 lp_ctx = sambaopts.get_loadparm()
55
56 creds = credopts.get_credentials(lp_ctx)
57 ldb = Ldb(url, credentials=creds, lp=lp_ctx)
58
59 objectclasses = {}
60 attributes = {}
61
62 objectclasses_expanded = set()
63
64 # the attributes we need for objectclasses
65 class_attrs = ["objectClass", 
66                "subClassOf", 
67                "governsID", 
68                "possSuperiors", 
69                "possibleInferiors",
70                "mayContain",
71                "mustContain",
72                "auxiliaryClass",
73                "rDNAttID",
74                "adminDisplayName",
75                "adminDescription",
76                "objectClassCategory",
77                "lDAPDisplayName",
78                "schemaIDGUID",
79                "systemOnly",
80                "systemPossSuperiors",
81                "systemMayContain",
82                "systemMustContain",
83                "systemAuxiliaryClass",
84                "defaultSecurityDescriptor",
85                "systemFlags",
86                "defaultHidingValue",
87                "objectCategory",
88                "defaultObjectCategory", 
89                
90                # this attributes are not used by w2k3
91                "schemaFlagsEx",
92                "msDs-IntId",
93                "msDs-Schema-Extensions",
94                "classDisplayName",
95                "isDefunct"]
96
97 attrib_attrs = ["objectClass",
98                 "attributeID", 
99                 "attributeSyntax",
100                 "isSingleValued",
101                 "rangeLower",
102                 "rangeUpper",
103                 "mAPIID",
104                 "linkID",
105                 "adminDisplayName",
106                 "oMObjectClass",
107                 "adminDescription",
108                 "oMSyntax", 
109                 "searchFlags",
110                 "extendedCharsAllowed",
111                 "lDAPDisplayName",
112                 "schemaIDGUID",
113                 "attributeSecurityGUID",
114                 "systemOnly",
115                 "systemFlags",
116                 "isMemberOfPartialAttributeSet",
117                 "objectCategory", 
118                 
119                 # this attributes are not used by w2k3
120                 "schemaFlagsEx",
121                 "msDs-IntId",
122                 "msDs-Schema-Extensions",
123                 "classDisplayName",
124                 "isEphemeral",
125                 "isDefunct"]
126
127 #
128 #  notes:
129 #
130 #  objectClassCategory 
131 #      1: structural
132 #      2: abstract
133 #      3: auxiliary
134
135 def get_object_cn(ldb, name):
136     attrs = ["cn"]
137     res = ldb.search(expression="(ldapDisplayName=%s)" % name, base=rootDse["schemaNamingContext"][0], scope=SCOPE_SUBTREE, attrs=attrs)
138     assert len(res) == 1
139     return res[0]["cn"]
140
141
142 class Objectclass(dict):
143
144     def __init__(self, ldb, name):
145         """create an objectclass object"""
146         self.name = name
147         self["cn"] = get_object_cn(ldb, name)
148
149
150 class Attribute(dict):
151
152     def __init__(self, ldb, name):
153         """create an attribute object"""
154         self.name = name
155         self["cn"] = get_object_cn(ldb, name)
156
157
158 syntaxmap = dict()
159
160 syntaxmap['2.5.5.1']  = '1.3.6.1.4.1.1466.115.121.1.12'
161 syntaxmap['2.5.5.2']  = '1.3.6.1.4.1.1466.115.121.1.38'
162 syntaxmap['2.5.5.3']  = '1.2.840.113556.1.4.1362'
163 syntaxmap['2.5.5.4']  = '1.2.840.113556.1.4.905'
164 syntaxmap['2.5.5.5']  = '1.3.6.1.4.1.1466.115.121.1.26'
165 syntaxmap['2.5.5.6']  = '1.3.6.1.4.1.1466.115.121.1.36'
166 syntaxmap['2.5.5.7']  = '1.2.840.113556.1.4.903'
167 syntaxmap['2.5.5.8']  = '1.3.6.1.4.1.1466.115.121.1.7'
168 syntaxmap['2.5.5.9']  = '1.3.6.1.4.1.1466.115.121.1.27'
169 syntaxmap['2.5.5.10'] = '1.3.6.1.4.1.1466.115.121.1.40'
170 syntaxmap['2.5.5.11'] = '1.3.6.1.4.1.1466.115.121.1.24'
171 syntaxmap['2.5.5.12'] = '1.3.6.1.4.1.1466.115.121.1.15'
172 syntaxmap['2.5.5.13'] = '1.3.6.1.4.1.1466.115.121.1.43'
173 syntaxmap['2.5.5.14'] = '1.2.840.113556.1.4.904'
174 syntaxmap['2.5.5.15'] = '1.2.840.113556.1.4.907'
175 syntaxmap['2.5.5.16'] = '1.2.840.113556.1.4.906'
176 syntaxmap['2.5.5.17'] = '1.3.6.1.4.1.1466.115.121.1.40'
177
178
179 def map_attribute_syntax(s):
180     """map some attribute syntaxes from some apparently MS specific
181     syntaxes to the standard syntaxes"""
182     if s in list(syntaxmap):
183         return syntaxmap[s]
184     return s
185
186
187 def fix_dn(dn):
188     """fix a string DN to use ${SCHEMADN}"""
189     return dn.replace(rootDse["schemaNamingContext"][0], "${SCHEMADN}")
190
191
192 def write_ldif_one(o, attrs):
193     """dump an object as ldif"""
194     print("dn: CN=%s,${SCHEMADN}" % o["cn"])
195     for a in attrs:
196         if not o.has_key(a):
197             continue
198         # special case for oMObjectClass, which is a binary object
199         v = o[a]
200         for j in v:
201             value = fix_dn(j)
202             if a == "oMObjectClass":
203                 print("%s:: %s" % (a, base64.b64encode(value).decode('utf8')))
204             elif a.endswith("GUID"):
205                 print("%s: %s" % (a, ldb.schema_format_value(a, value)))
206             else:
207                 print("%s: %s" % (a, value))
208     print()
209
210
211 def write_ldif(o, attrs):
212     """dump an array of objects as ldif"""
213     for n, i in o.items():
214         write_ldif_one(i, attrs)
215
216
217 def create_testdn(exampleDN):
218     """create a testDN based an an example DN
219     the idea is to ensure we obey any structural rules"""
220     a = exampleDN.split(",")
221     a[0] = "CN=TestDN"
222     return ",".join(a)
223
224
225 def find_objectclass_properties(ldb, o):
226     """the properties of an objectclass"""
227     res = ldb.search(
228         expression="(ldapDisplayName=%s)" % o.name,
229         base=rootDse["schemaNamingContext"][0], scope=SCOPE_SUBTREE, attrs=class_attrs)
230     assert(len(res) == 1)
231     msg = res[0]
232     for a in msg:
233         o[a] = msg[a]
234
235 def find_attribute_properties(ldb, o):
236     """find the properties of an attribute"""
237     res = ldb.search(
238         expression="(ldapDisplayName=%s)" % o.name,
239         base=rootDse["schemaNamingContext"][0], scope=SCOPE_SUBTREE, 
240         attrs=attrib_attrs)
241     assert(len(res) == 1)
242     msg = res[0]
243     for a in msg:
244         o[a] = msg[a]
245
246
247 def find_objectclass_auto(ldb, o):
248     """find the auto-created properties of an objectclass. Only works for 
249     classes that can be created using just a DN and the objectclass"""
250     if not o.has_key("exampleDN"):
251         return
252     testdn = create_testdn(o.exampleDN)
253
254     print("testdn is '%s'" % testdn)
255
256     ldif = "dn: " + testdn
257     ldif += "\nobjectClass: " + o.name
258     try:
259         ldb.add(ldif)
260     except LdbError as e:
261         print("error adding %s: %s" % (o.name, e))
262         print("%s" % ldif)
263         return
264
265     res = ldb.search(base=testdn, scope=ldb.SCOPE_BASE)
266     ldb.delete(testdn)
267
268     for a in res.msgs[0]:
269         attributes[a].autocreate = True
270
271
272 def expand_objectclass(ldb, o):
273     """look at auxiliary information from a class to intuit the existence of 
274     more classes needed for a minimal schema"""
275     attrs = ["auxiliaryClass", "systemAuxiliaryClass",
276                   "possSuperiors", "systemPossSuperiors",
277                   "subClassOf"]
278     res = ldb.search(
279         expression="(&(objectClass=classSchema)(ldapDisplayName=%s))" % o.name,
280         base=rootDse["schemaNamingContext"][0], scope=SCOPE_SUBTREE, 
281         attrs=attrs)
282     print("Expanding class %s" % o.name, file=sys.stderr)
283     assert(len(res) == 1)
284     msg = res[0]
285     for aname in attrs:
286         if not aname in msg:
287             continue
288         list = msg[aname]
289         if isinstance(list, str):
290             list = [msg[aname]]
291         for name in list:
292             if not objectclasses.has_key(name):
293                 print("Found new objectclass '%s'" % name, file=sys.stderr)
294                 objectclasses[name] = Objectclass(ldb, name)
295
296
297 def add_objectclass_attributes(ldb, objectclass):
298     """add the must and may attributes from an objectclass to the full list
299     of attributes"""
300     attrs = ["mustContain", "systemMustContain", 
301                   "mayContain", "systemMayContain"]
302     for aname in attrs:
303         if not objectclass.has_key(aname):
304             continue
305         alist = objectclass[aname]
306         if isinstance(alist, str):
307             alist = [alist]
308         for a in alist:
309             if not attributes.has_key(a):
310                 attributes[a] = Attribute(ldb, a)
311
312
313 def walk_dn(ldb, dn):
314     """process an individual record, working out what attributes it has"""
315     # get a list of all possible attributes for this object 
316     attrs = ["allowedAttributes"]
317     try:
318         res = ldb.search("objectClass=*", dn, SCOPE_BASE, attrs)
319     except LdbError as e:
320         print("Unable to fetch allowedAttributes for '%s' - %r" % (dn, e),
321               file=sys.stderr)
322         return
323     allattrs = res[0]["allowedAttributes"]
324     try:
325         res = ldb.search("objectClass=*", dn, SCOPE_BASE, allattrs)
326     except LdbError as e:
327         print("Unable to fetch all attributes for '%s' - %s" % (dn, e),
328               file=sys.stderr)
329         return
330     msg = res[0]
331     for a in msg:
332         if not attributes.has_key(a):
333             attributes[a] = Attribute(ldb, a)
334
335 def walk_naming_context(ldb, namingContext):
336     """walk a naming context, looking for all records"""
337     try:
338         res = ldb.search("objectClass=*", namingContext, SCOPE_DEFAULT, 
339                          ["objectClass"])
340     except LdbError as e:
341         print("Unable to fetch objectClasses for '%s' - %s" % (namingContext, e),
342               file=sys.stderr)
343         return
344     for msg in res:
345         msg = res.msgs[r]["objectClass"]
346         for objectClass in msg:
347             if not objectclasses.has_key(objectClass):
348                 objectclasses[objectClass] = Objectclass(ldb, objectClass)
349                 objectclasses[objectClass].exampleDN = res.msgs[r]["dn"]
350         walk_dn(ldb, res.msgs[r].dn)
351
352 def trim_objectclass_attributes(ldb, objectclass):
353     """trim the may attributes for an objectClass"""
354     # trim possibleInferiors,
355     # include only the classes we extracted
356     if objectclass.has_key("possibleInferiors"):
357         possinf = objectclass["possibleInferiors"]
358         newpossinf = []
359         for x in possinf:
360             if objectclasses.has_key(x):
361                 newpossinf.append(x)
362         objectclass["possibleInferiors"] = newpossinf
363
364     # trim systemMayContain,
365     # remove duplicates
366     if objectclass.has_key("systemMayContain"):
367         sysmay = objectclass["systemMayContain"]
368         newsysmay = []
369         for x in sysmay:
370             if not x in newsysmay:
371                 newsysmay.append(x)
372         objectclass["systemMayContain"] = newsysmay
373
374     # trim mayContain,
375     # remove duplicates
376     if objectclass.has_key("mayContain"):
377         may = objectclass["mayContain"]
378         newmay = []
379         if isinstance(may, str):
380             may = [may]
381         for x in may:
382             if not x in newmay:
383                 newmay.append(x)
384         objectclass["mayContain"] = newmay
385
386
387 def build_objectclass(ldb, name):
388     """load the basic attributes of an objectClass"""
389     attrs = ["name"]
390     res = ldb.search(
391         expression="(&(objectClass=classSchema)(ldapDisplayName=%s))" % name,
392         base=rootDse["schemaNamingContext"][0], scope=SCOPE_SUBTREE, 
393         attrs=attrs)
394     if len(res) == 0:
395         print("unknown class '%s'" % name, file=sys.stderr)
396         return None
397     return Objectclass(ldb, name)
398
399
400 def attribute_list(objectclass, attr1, attr2):
401     """form a coalesced attribute list"""
402     a1 = list(objectclass.get(attr1, []))
403     a2 = list(objectclass.get(attr2, []))
404     return a1 + a2
405
406 def aggregate_list(name, list):
407     """write out a list in aggregate form"""
408     if list == []:
409         return ""
410     return " %s ( %s )" % (name, " $ ".join(list))
411
412 def write_aggregate_objectclass(objectclass):
413     """write the aggregate record for an objectclass"""
414     line = "objectClasses: ( %s NAME '%s' " % (objectclass["governsID"], objectclass.name)
415     if not objectclass.has_key('subClassOf'):
416         line += "SUP %s" % objectclass['subClassOf']
417     if objectclass["objectClassCategory"] == 1:
418         line += "STRUCTURAL"
419     elif objectclass["objectClassCategory"] == 2:
420         line += "ABSTRACT"
421     elif objectclass["objectClassCategory"] == 3:
422         line += "AUXILIARY"
423
424     list = attribute_list(objectclass, "systemMustContain", "mustContain")
425     line += aggregate_list("MUST", list)
426
427     list = attribute_list(objectclass, "systemMayContain", "mayContain")
428     line += aggregate_list("MAY", list)
429
430     print(line + " )")
431
432
433 def write_aggregate_ditcontentrule(objectclass):
434     """write the aggregate record for an ditcontentrule"""
435     list = attribute_list(objectclass, "auxiliaryClass", "systemAuxiliaryClass")
436     if list == []:
437         return
438
439     line = "dITContentRules: ( %s NAME '%s'" % (objectclass["governsID"], objectclass.name)
440
441     line += aggregate_list("AUX", list)
442
443     may_list = []
444     must_list = []
445
446     for c in list:
447         list2 = attribute_list(objectclasses[c], 
448                        "mayContain", "systemMayContain")
449         may_list = may_list + list2
450         list2 = attribute_list(objectclasses[c], 
451                        "mustContain", "systemMustContain")
452         must_list = must_list + list2
453
454     line += aggregate_list("MUST", must_list)
455     line += aggregate_list("MAY", may_list)
456
457     print(line + " )")
458
459 def write_aggregate_attribute(attrib):
460     """write the aggregate record for an attribute"""
461     line = "attributeTypes: ( %s NAME '%s' SYNTAX '%s' " % (
462            attrib["attributeID"], attrib.name, 
463            map_attribute_syntax(attrib["attributeSyntax"]))
464     if attrib.get('isSingleValued') == "TRUE":
465         line += "SINGLE-VALUE "
466     if attrib.get('systemOnly') == "TRUE":
467         line += "NO-USER-MODIFICATION "
468
469     print(line + ")")
470
471
472 def write_aggregate():
473     """write the aggregate record"""
474     print("dn: CN=Aggregate,${SCHEMADN}")
475     print("""objectClass: top
476 objectClass: subSchema
477 objectCategory: CN=SubSchema,${SCHEMADN}""")
478     if not opts.dump_subschema_auto:
479         return
480
481     for objectclass in objectclasses.values():
482         write_aggregate_objectclass(objectclass)
483     for attr in attributes.values():
484         write_aggregate_attribute(attr)
485     for objectclass in objectclasses.values():
486         write_aggregate_ditcontentrule(objectclass)
487
488 def load_list(file):
489     """load a list from a file"""
490     return [l.strip("\n") for l in open(file, 'r').readlines()]
491
492 # get the rootDSE
493 res = ldb.search(base="", expression="", scope=SCOPE_BASE, attrs=["schemaNamingContext"])
494 rootDse = res[0]
495
496 # load the list of classes we are interested in
497 classes = load_list(classfile)
498 for classname in classes:
499     objectclass = build_objectclass(ldb, classname)
500     if objectclass is not None:
501         objectclasses[classname] = objectclass
502
503
504 #
505 #  expand the objectclass list as needed
506 #
507 expanded = 0
508
509 # so EJS do not have while nor the break statement
510 # cannot find any other way than doing more loops
511 # than necessary to recursively expand all classes
512 #
513 for inf in range(500):
514     for n, o in objectclasses.items():
515         if not n in objectclasses_expanded:
516             expand_objectclass(ldb, o)
517             objectclasses_expanded.add(n)
518
519 #
520 #  find objectclass properties
521 #
522 for name, objectclass in objectclasses.items():
523     find_objectclass_properties(ldb, objectclass)
524
525
526 #
527 #  form the full list of attributes
528 #
529 for name, objectclass in objectclasses.items():
530     add_objectclass_attributes(ldb, objectclass)
531
532 # and attribute properties
533 for name, attr in attributes.items():
534     find_attribute_properties(ldb, attr)
535
536 #
537 # trim the 'may' attribute lists to those really needed
538 #
539 for name, objectclass in objectclasses.items():
540     trim_objectclass_attributes(ldb, objectclass)
541
542 #
543 #  dump an ldif form of the attributes and objectclasses
544 #
545 if opts.dump_attributes:
546     write_ldif(attributes, attrib_attrs)
547 if opts.dump_classes:
548     write_ldif(objectclasses, class_attrs)
549 if opts.dump_subschema:
550     write_aggregate()
551
552 if not opts.verbose:
553     sys.exit(0)
554
555 #
556 #  dump list of objectclasses
557 #
558 print("objectClasses:\n")
559 for objectclass in objectclasses:
560     print("\t%s\n" % objectclass)
561
562 print("attributes:\n")
563 for attr in attributes:
564     print("\t%s\n" % attr)
565
566 print("autocreated attributes:\n")
567 for attr in attributes:
568     if attr.autocreate:
569         print("\t%s\n" % i)