Add minschema like tool to extract and dump the full schema from AD
[kai/samba.git] / source4 / scripting / bin / minschema
1 #!/usr/bin/python
2
3 #  work out the minimal schema for a set of objectclasses 
4 #
5
6 import base64
7 import optparse
8 import os
9 import sys
10
11 # Find right directory when running from source tree
12 sys.path.insert(0, "bin/python")
13
14 import samba
15 from samba import getopt as options, Ldb
16 from ldb import SCOPE_SUBTREE, SCOPE_BASE, LdbError
17 import sys
18
19 parser = optparse.OptionParser("minschema <URL> <classfile>")
20 sambaopts = options.SambaOptions(parser)
21 parser.add_option_group(sambaopts)
22 credopts = options.CredentialsOptions(parser)
23 parser.add_option_group(credopts)
24 parser.add_option_group(options.VersionOptions(parser))
25 parser.add_option("--verbose", help="Be verbose", action="store_true")
26 parser.add_option("--dump-classes", action="store_true")
27 parser.add_option("--dump-attributes", action="store_true")
28 parser.add_option("--dump-subschema", action="store_true")
29 parser.add_option("--dump-subschema-auto", action="store_true")
30
31 opts, args = parser.parse_args()
32 opts.dump_all = True
33
34 if opts.dump_classes:
35     opts.dump_all = False
36 if opts.dump_attributes:
37     opts.dump_all = False
38 if opts.dump_subschema:
39     opts.dump_all = False
40 if opts.dump_subschema_auto:
41     opts.dump_all = False
42     opts.dump_subschema = True
43 if opts.dump_all:
44     opts.dump_classes = True
45     opts.dump_attributes = True
46     opts.dump_subschema = True
47     opts.dump_subschema_auto = True
48
49 if len(args) != 2:
50     parser.print_usage()
51     sys.exit(1)
52
53 (url, classfile) = args
54
55 lp_ctx = sambaopts.get_loadparm()
56
57 creds = credopts.get_credentials(lp_ctx)
58 ldb = Ldb(url, credentials=creds, lp=lp_ctx)
59
60 objectclasses = {}
61 attributes = {}
62
63 objectclasses_expanded = set()
64
65 # the attributes we need for objectclasses
66 class_attrs = ["objectClass", 
67                "subClassOf", 
68                "governsID", 
69                "possSuperiors", 
70                "possibleInferiors",
71                "mayContain",
72                "mustContain",
73                "auxiliaryClass",
74                "rDNAttID",
75                "showInAdvancedViewOnly",
76                "adminDisplayName",
77                "adminDescription",
78                "objectClassCategory",
79                "lDAPDisplayName",
80                "schemaIDGUID",
81                "systemOnly",
82                "systemPossSuperiors",
83                "systemMayContain",
84                "systemMustContain",
85                "systemAuxiliaryClass",
86                "defaultSecurityDescriptor",
87                "systemFlags",
88                "defaultHidingValue",
89                "objectCategory",
90                "defaultObjectCategory", 
91                
92                # this attributes are not used by w2k3
93                "schemaFlagsEx",
94                "msDs-IntId",
95                "msDs-Schema-Extensions",
96                "classDisplayName",
97                "isDefunct"]
98
99 attrib_attrs = ["objectClass",
100                 "attributeID", 
101                 "attributeSyntax",
102                 "isSingleValued",
103                 "rangeLower",
104                 "rangeUpper",
105                 "mAPIID",
106                 "linkID",
107                 "showInAdvancedViewOnly",
108                 "adminDisplayName",
109                 "oMObjectClass",
110                 "adminDescription",
111                 "oMSyntax", 
112                 "searchFlags",
113                 "extendedCharsAllowed",
114                 "lDAPDisplayName",
115                 "schemaIDGUID",
116                 "attributeSecurityGUID",
117                 "systemOnly",
118                 "systemFlags",
119                 "isMemberOfPartialAttributeSet",
120                 "objectCategory", 
121                 
122                 # this attributes are not used by w2k3
123                 "schemaFlagsEx",
124                 "msDs-IntId",
125                 "msDs-Schema-Extensions",
126                 "classDisplayName",
127                 "isEphemeral",
128                 "isDefunct"]
129
130 #
131 #  notes:
132 #
133 #  objectClassCategory 
134 #      1: structural
135 #      2: abstract
136 #      3: auxiliary
137
138 def get_object_cn(ldb, name):
139     attrs = ["cn"]
140     res = ldb.search(expression="(ldapDisplayName=%s)" % name, base=rootDse["schemaNamingContext"][0], scope=SCOPE_SUBTREE, attrs=attrs)
141     assert len(res) == 1
142     return res[0]["cn"]
143
144
145 class Objectclass(dict):
146
147     def __init__(self, ldb, name):
148         """create an objectclass object"""
149         self.name = name
150         self["cn"] = get_object_cn(ldb, name)
151
152
153 class Attribute(dict):
154
155     def __init__(self, ldb, name):
156         """create an attribute object"""
157         self.name = name
158         self["cn"] = get_object_cn(ldb, name)
159
160
161 syntaxmap = dict()
162
163 syntaxmap['2.5.5.1']  = '1.3.6.1.4.1.1466.115.121.1.12'
164 syntaxmap['2.5.5.2']  = '1.3.6.1.4.1.1466.115.121.1.38'
165 syntaxmap['2.5.5.3']  = '1.2.840.113556.1.4.1362'
166 syntaxmap['2.5.5.4']  = '1.2.840.113556.1.4.905'
167 syntaxmap['2.5.5.5']  = '1.3.6.1.4.1.1466.115.121.1.26'
168 syntaxmap['2.5.5.6']  = '1.3.6.1.4.1.1466.115.121.1.36'
169 syntaxmap['2.5.5.7']  = '1.2.840.113556.1.4.903'
170 syntaxmap['2.5.5.8']  = '1.3.6.1.4.1.1466.115.121.1.7'
171 syntaxmap['2.5.5.9']  = '1.3.6.1.4.1.1466.115.121.1.27'
172 syntaxmap['2.5.5.10'] = '1.3.6.1.4.1.1466.115.121.1.40'
173 syntaxmap['2.5.5.11'] = '1.3.6.1.4.1.1466.115.121.1.24'
174 syntaxmap['2.5.5.12'] = '1.3.6.1.4.1.1466.115.121.1.15'
175 syntaxmap['2.5.5.13'] = '1.3.6.1.4.1.1466.115.121.1.43'
176 syntaxmap['2.5.5.14'] = '1.2.840.113556.1.4.904'
177 syntaxmap['2.5.5.15'] = '1.2.840.113556.1.4.907'
178 syntaxmap['2.5.5.16'] = '1.2.840.113556.1.4.906'
179 syntaxmap['2.5.5.17'] = '1.3.6.1.4.1.1466.115.121.1.40'
180
181
182 def map_attribute_syntax(s):
183     """map some attribute syntaxes from some apparently MS specific
184     syntaxes to the standard syntaxes"""
185     if s in list(syntaxmap):
186         return syntaxmap[s]
187     return s
188
189
190 def fix_dn(dn):
191     """fix a string DN to use ${SCHEMADN}"""
192     return dn.replace(rootDse["schemaNamingContext"][0], "${SCHEMADN}")
193
194
195 def write_ldif_one(o, attrs):
196     """dump an object as ldif"""
197     print "dn: CN=%s,${SCHEMADN}" % o["cn"]
198     for a in attrs:
199         if not o.has_key(a):
200             continue
201         # special case for oMObjectClass, which is a binary object
202         v = o[a]
203         for j in v:
204                         value = fix_dn(j)
205                         if a == "oMObjectClass":
206                                 print "%s:: %s" % (a, base64.b64encode(value))
207                         elif a.endswith("GUID"):
208                                 print "%s: %s" % (a, ldb.schema_format_value(a, value))
209                         else:
210                                 print "%s: %s" % (a, value)
211     print ""
212
213
214 def write_ldif(o, attrs):
215     """dump an array of objects as ldif"""
216     for n, i in o.items():
217         write_ldif_one(i, attrs)
218
219
220 def create_testdn(exampleDN):
221     """create a testDN based an an example DN
222     the idea is to ensure we obey any structural rules"""
223     a = exampleDN.split(",")
224     a[0] = "CN=TestDN"
225     return ",".join(a)
226
227
228 def find_objectclass_properties(ldb, o):
229     """the properties of an objectclass"""
230     res = ldb.search(
231         expression="(ldapDisplayName=%s)" % o.name,
232         base=rootDse["schemaNamingContext"][0], scope=SCOPE_SUBTREE, attrs=class_attrs)
233     assert(len(res) == 1)
234     msg = res[0]
235     for a in msg:
236         o[a] = msg[a]
237
238 def find_attribute_properties(ldb, o):
239     """find the properties of an attribute"""
240     res = ldb.search(
241         expression="(ldapDisplayName=%s)" % o.name,
242         base=rootDse["schemaNamingContext"][0], scope=SCOPE_SUBTREE, 
243         attrs=attrib_attrs)
244     assert(len(res) == 1)
245     msg = res[0]
246     for a in msg:
247         o[a] = msg[a]
248
249
250 def find_objectclass_auto(ldb, o):
251     """find the auto-created properties of an objectclass. Only works for 
252     classes that can be created using just a DN and the objectclass"""
253     if not o.has_key("exampleDN"):
254         return
255     testdn = create_testdn(o.exampleDN)
256
257     print "testdn is '%s'" % testdn
258
259     ldif = "dn: " + testdn
260     ldif += "\nobjectClass: " + o.name
261     try:
262         ldb.add(ldif)
263     except LdbError, e:
264         print "error adding %s: %s" % (o.name, e)
265         print "%s" % ldif
266         return
267
268     res = ldb.search(base=testdn, scope=ldb.SCOPE_BASE)
269     ldb.delete(testdn)
270
271     for a in res.msgs[0]:
272         attributes[a].autocreate = True
273
274
275 def expand_objectclass(ldb, o):
276     """look at auxiliary information from a class to intuit the existance of 
277     more classes needed for a minimal schema"""
278     attrs = ["auxiliaryClass", "systemAuxiliaryClass",
279                   "possSuperiors", "systemPossSuperiors",
280                   "subClassOf"]
281     res = ldb.search(
282         expression="(&(objectClass=classSchema)(ldapDisplayName=%s))" % o.name,
283         base=rootDse["schemaNamingContext"][0], scope=SCOPE_SUBTREE, 
284         attrs=attrs)
285     print >>sys.stderr, "Expanding class %s" % o.name
286     assert(len(res) == 1)
287     msg = res[0]
288     for aname in attrs:
289         if not aname in msg:
290             continue
291         list = msg[aname]
292         if isinstance(list, str):
293             list = [msg[aname]]
294         for name in list:
295             if not objectclasses.has_key(name):
296                 print >>sys.stderr, "Found new objectclass '%s'" % name
297                 objectclasses[name] = Objectclass(ldb, name)
298
299
300 def add_objectclass_attributes(ldb, objectclass):
301     """add the must and may attributes from an objectclass to the full list
302     of attributes"""
303     attrs = ["mustContain", "systemMustContain", 
304                   "mayContain", "systemMayContain"]
305     for aname in attrs:
306         if not objectclass.has_key(aname):
307             continue
308         alist = objectclass[aname]
309         if isinstance(alist, str):
310             alist = [alist]
311         for a in alist:
312             if not attributes.has_key(a):
313                 attributes[a] = Attribute(ldb, a)
314
315
316 def walk_dn(ldb, dn):
317     """process an individual record, working out what attributes it has"""
318     # get a list of all possible attributes for this object 
319     attrs = ["allowedAttributes"]
320     try:
321         res = ldb.search("objectClass=*", dn, SCOPE_BASE, attrs)
322     except LdbError, e:
323         print >>sys.stderr, "Unable to fetch allowedAttributes for '%s' - %r" % (dn, e)
324         return
325     allattrs = res[0]["allowedAttributes"]
326     try:
327         res = ldb.search("objectClass=*", dn, SCOPE_BASE, allattrs)
328     except LdbError, e:
329         print >>sys.stderr, "Unable to fetch all attributes for '%s' - %s" % (dn, e)
330         return
331     msg = res[0]
332     for a in msg:
333         if not attributes.has_key(a):
334             attributes[a] = Attribute(ldb, a)
335
336 def walk_naming_context(ldb, namingContext):
337     """walk a naming context, looking for all records"""
338     try:
339         res = ldb.search("objectClass=*", namingContext, SCOPE_DEFAULT, 
340                          ["objectClass"])
341     except LdbError, e:
342         print >>sys.stderr, "Unable to fetch objectClasses for '%s' - %s" % (namingContext, e)
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 >>sys.stderr, "unknown class '%s'" % name
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