Merge branch 'master' of ssh://git.samba.org/data/git/samba into wspp-schema
[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 is None:
409         return
410     print "%s ( %s )" % (name, "$ ".join(list))
411
412 def write_aggregate_objectclass(objectclass):
413     """write the aggregate record for an objectclass"""
414     print "objectClasses: ( %s NAME '%s' " % (objectclass["governsID"], objectclass.name),
415     if not objectclass.has_key('subClassOf'):
416         print "SUP %s " % objectclass['subClassOf'],
417     if objectclass["objectClassCategory"] == 1:
418         print "STRUCTURAL ",
419     elif objectclass["objectClassCategory"] == 2:
420         print "ABSTRACT ",
421     elif objectclass["objectClassCategory"] == 3:
422         print "AUXILIARY ",
423
424     list = attribute_list(objectclass, "systemMustContain", "mustContain")
425     aggregate_list("MUST", list)
426
427     list = attribute_list(objectclass, "systemMayContain", "mayContain")
428     aggregate_list("MAY", list)
429
430     print ")"
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 is None:
437         return
438
439     print "dITContentRules: ( %s NAME '%s' " % (objectclass["governsID"], objectclass.name)
440
441     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     aggregate_list("MUST", must_list)
455     aggregate_list("MAY", may_list)
456
457     print ")\n"
458
459 def write_aggregate_attribute(attrib):
460     """write the aggregate record for an attribute"""
461     print "attributeTypes: ( %s NAME '%s' SYNTAX '%s' " % (
462            attrib["attributeID"], attrib.name, 
463            map_attribute_syntax(attrib["attributeSyntax"]))
464     if attrib.get('isSingleValued') == "TRUE":
465         print "SINGLE-VALUE "
466     if attrib.get('systemOnly') == "TRUE":
467         print "NO-USER-MODIFICATION "
468
469     print ")\n"
470
471
472 def write_aggregate():
473     """write the aggregate record"""
474     print "dn: CN=Aggregate,${SCHEMADN}\n"
475     print """objectClass: top
476 objectClass: subSchema
477 objectCategory: CN=SubSchema,${SCHEMADN}
478 """
479     if not opts.dump_subschema_auto:
480         return
481
482     for objectclass in objectclasses.values():
483         write_aggregate_objectclass(objectclass)
484     for attr in attributes.values():
485         write_aggregate_attribute(attr)
486     for objectclass in objectclasses.values():
487         write_aggregate_ditcontentrule(objectclass)
488
489 def load_list(file):
490     """load a list from a file"""
491     return [l.strip("\n") for l in open(file, 'r').readlines()]
492
493 # get the rootDSE
494 res = ldb.search(base="", expression="", scope=SCOPE_BASE, attrs=["schemaNamingContext"])
495 rootDse = res[0]
496
497 # load the list of classes we are interested in
498 classes = load_list(classfile)
499 for classname in classes:
500     objectclass = build_objectclass(ldb, classname)
501     if objectclass is not None:
502         objectclasses[classname] = objectclass
503
504
505 #
506 #  expand the objectclass list as needed
507 #
508 expanded = 0
509
510 # so EJS do not have while nor the break statement
511 # cannot find any other way than doing more loops
512 # than necessary to recursively expand all classes
513 #
514 for inf in range(500):
515     for n, o in objectclasses.items():
516         if not n in objectclasses_expanded:
517             expand_objectclass(ldb, o)
518             objectclasses_expanded.add(n)
519
520 #
521 #  find objectclass properties
522 #
523 for name, objectclass in objectclasses.items():
524     find_objectclass_properties(ldb, objectclass)
525
526
527 #
528 #  form the full list of attributes
529 #
530 for name, objectclass in objectclasses.items():
531     add_objectclass_attributes(ldb, objectclass)
532
533 # and attribute properties
534 for name, attr in attributes.items():
535     find_attribute_properties(ldb, attr)
536
537 #
538 # trim the 'may' attribute lists to those really needed
539 #
540 for name, objectclass in objectclasses.items():
541     trim_objectclass_attributes(ldb, objectclass)
542
543 #
544 #  dump an ldif form of the attributes and objectclasses
545 #
546 if opts.dump_attributes:
547     write_ldif(attributes, attrib_attrs)
548 if opts.dump_classes:
549     write_ldif(objectclasses, class_attrs)
550 if opts.dump_subschema:
551     write_aggregate()
552
553 if not opts.verbose:
554     sys.exit(0)
555
556 #
557 #  dump list of objectclasses
558 #
559 print "objectClasses:\n"
560 for objectclass in objectclasses:
561     print "\t%s\n" % objectclass
562
563 print "attributes:\n"
564 for attr in attributes:
565     print "\t%s\n" % attr
566
567 print "autocreated attributes:\n"
568 for attr in attributes:
569     if attr.autocreate:
570         print "\t%s\n" % i