Merge branch 'master' of ssh://git.samba.org/data/git/samba
[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 optparse
7
8 import os, 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)
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                "showInAdvancedViewOnly",
75                "adminDisplayName",
76                "adminDescription",
77                "objectClassCategory",
78                "lDAPDisplayName",
79                "schemaIDGUID",
80                "systemOnly",
81                "systemPossSuperiors",
82                "systemMayContain",
83                "systemMustContain",
84                "systemAuxiliaryClass",
85                "defaultSecurityDescriptor",
86                "systemFlags",
87                "defaultHidingValue",
88                "objectCategory",
89                "defaultObjectCategory", 
90                
91                # this attributes are not used by w2k3
92                "schemaFlagsEx",
93                "msDs-IntId",
94                "msDs-Schema-Extensions",
95                "classDisplayName",
96                "isDefunct"]
97
98 attrib_attrs = ["objectClass",
99                 "attributeID", 
100                 "attributeSyntax",
101                 "isSingleValued",
102                 "rangeLower",
103                 "rangeUpper",
104                 "mAPIID",
105                 "linkID",
106                 "showInAdvancedViewOnly",
107                 "adminDisplayName",
108                 "oMObjectClass",
109                 "adminDescription",
110                 "oMSyntax", 
111                 "searchFlags",
112                 "extendedCharsAllowed",
113                 "lDAPDisplayName",
114                 "schemaIDGUID",
115                 "attributeSecurityGUID",
116                 "systemOnly",
117                 "systemFlags",
118                 "isMemberOfPartialAttributeSet",
119                 "objectCategory", 
120                 
121                 # this attributes are not used by w2k3
122                 "schemaFlagsEx",
123                 "msDs-IntId",
124                 "msDs-Schema-Extensions",
125                 "classDisplayName",
126                 "isEphemeral",
127                 "isDefunct"]
128
129 #
130 #  notes:
131 #
132 #  objectClassCategory 
133 #      1: structural
134 #      2: abstract
135 #      3: auxiliary
136
137 def get_object_cn(ldb, name):
138     attrs = ["cn"]
139
140     res = ldb.search("(ldapDisplayName=%s)" % name, rootDse["schemaNamingContext"], SCOPE_SUBTREE, attrs)
141     assert len(res) == 1
142
143     return res[0]["cn"]
144
145 class Objectclass:
146     def __init__(self, ldb, name):
147         """create an objectclass object"""
148         self.name = name
149         self.cn = get_object_cn(ldb, name)
150
151
152 class Attribute:
153     def __init__(self, ldb, name):
154         """create an attribute object"""
155         self.name = name
156         self.cn = get_object_cn(ldb, name)
157
158
159 syntaxmap = dict()
160
161 syntaxmap['2.5.5.1']  = '1.3.6.1.4.1.1466.115.121.1.12'
162 syntaxmap['2.5.5.2']  = '1.3.6.1.4.1.1466.115.121.1.38'
163 syntaxmap['2.5.5.3']  = '1.2.840.113556.1.4.1362'
164 syntaxmap['2.5.5.4']  = '1.2.840.113556.1.4.905'
165 syntaxmap['2.5.5.5']  = '1.3.6.1.4.1.1466.115.121.1.26'
166 syntaxmap['2.5.5.6']  = '1.3.6.1.4.1.1466.115.121.1.36'
167 syntaxmap['2.5.5.7']  = '1.2.840.113556.1.4.903'
168 syntaxmap['2.5.5.8']  = '1.3.6.1.4.1.1466.115.121.1.7'
169 syntaxmap['2.5.5.9']  = '1.3.6.1.4.1.1466.115.121.1.27'
170 syntaxmap['2.5.5.10'] = '1.3.6.1.4.1.1466.115.121.1.40'
171 syntaxmap['2.5.5.11'] = '1.3.6.1.4.1.1466.115.121.1.24'
172 syntaxmap['2.5.5.12'] = '1.3.6.1.4.1.1466.115.121.1.15'
173 syntaxmap['2.5.5.13'] = '1.3.6.1.4.1.1466.115.121.1.43'
174 syntaxmap['2.5.5.14'] = '1.2.840.113556.1.4.904'
175 syntaxmap['2.5.5.15'] = '1.2.840.113556.1.4.907'
176 syntaxmap['2.5.5.16'] = '1.2.840.113556.1.4.906'
177 syntaxmap['2.5.5.17'] = '1.3.6.1.4.1.1466.115.121.1.40'
178
179
180 def map_attribute_syntax(s):
181     """map some attribute syntaxes from some apparently MS specific
182     syntaxes to the standard syntaxes"""
183     if syntaxmap.has_key(s):
184         return syntaxmap[s]
185     return s
186
187
188 def fix_dn(dn):
189     """fix a string DN to use ${SCHEMADN}"""
190     return dn.replace(rootDse["schemaNamingContext"], "${SCHEMADN}")
191
192
193 def write_ldif_one(o, attrs):
194     """dump an object as ldif"""
195     print "dn: CN=%s,${SCHEMADN}\n" % o["cn"]
196     for a in attrs:
197         if not o.has_key(a):
198             continue
199         # special case for oMObjectClass, which is a binary object
200         if a == "oMObjectClass":
201             print "%s:: %s\n" % (a, o[a])
202             continue
203         v = o[a]
204         if isinstance(v, str):
205             v = [v]
206         for j in v:
207             print "%s: %s\n" % (a, fix_dn(j))
208     print "\n"
209
210 def write_ldif(o, attrs):
211     """dump an array of objects as ldif"""
212     for i in o:
213         write_ldif_one(i, attrs)
214
215
216 def create_testdn(exampleDN):
217     """create a testDN based an an example DN
218     the idea is to ensure we obey any structural rules"""
219     a = exampleDN.split(",")
220     a[0] = "CN=TestDN"
221     return ",".join(a)
222
223
224 def find_objectclass_properties(ldb, o):
225     """the properties of an objectclass"""
226     res = ldb.search(
227         expression="(ldapDisplayName=%s)" % o.name,
228         base=rootDse["schemaNamingContext"], scope=SCOPE_SUBTREE, attrs=class_attrs)
229     assert(len(res) == 1)
230     msg = res[0]
231     for a in msg:
232         o[a] = msg[a]
233
234 def find_attribute_properties(ldb, o):
235     """find the properties of an attribute"""
236     res = ldb.search(
237         expression="(ldapDisplayName=%s)" % o.name,
238         base=rootDse["schemaNamingContext"], scope=SCOPE_SUBTREE, 
239         attrs=attrib_attrs)
240     assert(len(res) == 1)
241     msg = res[0]
242     for a in msg:
243         # special case for oMObjectClass, which is a binary object
244         if a == "oMObjectClass":
245             o[a] = ldb.encode(msg[a])
246             continue
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'\n" % 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\n" % (o.name, e)
265         print "%s\n" % 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"], scope=SCOPE_SUBTREE, 
284         attrs=attrs)
285     print "Expanding class %s\n" % o.name
286     assert(len(res) == 1)
287     msg = res[0]
288     for a in attrs:
289         if not msg.has_key(aname):
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 "Found new objectclass '%s'\n" % 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 "Unable to fetch allowedAttributes for '%s' - %r\n" % (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 "Unable to fetch all attributes for '%s' - %s\n" % (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 "Unable to fetch objectClasses for '%s' - %s\n" % (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         if isinstance(possinf, str):
360             possinf = [possinf]
361         for x in possinf:
362             if objectclasses.has_key(x):
363                 newpossinf[n] = x
364                 n+=1
365         objectclass["possibleInferiors"] = newpossinf
366
367     # trim systemMayContain,
368     # remove duplicates
369     if objectclass.has_key("systemMayContain"):
370         sysmay = objectclass["systemMayContain"]
371         newsysmay = []
372         if isinstance(sysmay, str):
373             sysmay = [sysmay]
374         for x in sysmay:
375             if not x in newsysmay:
376                 newsysmay.append(x)
377         objectclass["systemMayContain"] = newsysmay
378
379     # trim mayContain,
380     # remove duplicates
381     if not objectclass.has_key("mayContain"):
382         may = objectclass["mayContain"]
383         newmay = []
384         if isinstance(may, str):
385             may = [may]
386         for x in may:
387             if not x in newmay:
388                 newmay.append(x)
389         objectclass["mayContain"] = newmay
390
391 def build_objectclass(ldb, name):
392     """load the basic attributes of an objectClass"""
393     attrs = ["name"]
394     try:
395         res = ldb.search(
396             expression="(&(objectClass=classSchema)(ldapDisplayName=%s))" % name,
397             base=rootDse["schemaNamingContext"], scope=SCOPE_SUBTREE, 
398             attrs=attrs)
399     except LdbError, e:
400         print "unknown class '%s'\n" % name
401         return None
402     if len(res) == 0:
403         print "unknown class '%s'\n" % name
404         return None
405     return Objectclass(ldb, name)
406
407 def attribute_list(objectclass, attr1, attr2):
408     """form a coalesced attribute list"""
409     a1 = objectclass[attr1]
410     a2 = objectclass[attr2]
411     if isinstance(a1, str):
412         a1 = [a1]
413     if isinstance(a2, str):
414         a2 = [a2]
415     return a1 + a2
416
417 def aggregate_list(name, list):
418     """write out a list in aggregate form"""
419     if list is None:
420         return
421     print "%s ( %s )" % (name, "$ ".join(list))
422
423 def write_aggregate_objectclass(objectclass):
424     """write the aggregate record for an objectclass"""
425     print "objectClasses: ( %s NAME '%s' " % (objectclass.governsID, objectclass.name)
426     if not objectclass.has_key('subClassOf'):
427         print "SUP %s " % objectclass['subClassOf']
428     if objectclass.objectClassCategory == 1:
429         print "STRUCTURAL "
430     elif objectclass.objectClassCategory == 2:
431         print "ABSTRACT "
432     elif objectclass.objectClassCategory == 3:
433         print "AUXILIARY "
434
435     list = attribute_list(objectclass, "systemMustContain", "mustContain")
436     aggregate_list("MUST", list)
437
438     list = attribute_list(objectclass, "systemMayContain", "mayContain")
439     aggregate_list("MAY", list)
440
441     print ")\n"
442
443
444 def write_aggregate_ditcontentrule(objectclass):
445     """write the aggregate record for an ditcontentrule"""
446     list = attribute_list(objectclass, "auxiliaryClass", "systemAuxiliaryClass")
447     if list is None:
448         return
449
450     print "dITContentRules: ( %s NAME '%s' " % (objectclass.governsID, objectclass.name)
451
452     aggregate_list("AUX", list)
453
454     may_list = None
455     must_list = None
456
457     for c in list:
458         list2 = attribute_list(objectclasses[c], 
459                        "mayContain", "systemMayContain")
460         may_list = may_list + list2
461         list2 = attribute_list(objectclasses[c], 
462                        "mustContain", "systemMustContain")
463         must_list = must_list + list2
464
465     aggregate_list("MUST", must_list)
466     aggregate_list("MAY", may_list)
467
468     print ")\n"
469
470 def write_aggregate_attribute(attrib):
471     """write the aggregate record for an attribute"""
472     print "attributeTypes: ( %s NAME '%s' SYNTAX '%s' " % (
473            attrib.attributeID, attrib.name, 
474            map_attribute_syntax(attrib.attributeSyntax))
475     if attrib['isSingleValued'] == "TRUE":
476         print "SINGLE-VALUE "
477     if attrib['systemOnly'] == "TRUE":
478         print "NO-USER-MODIFICATION "
479
480     print ")\n"
481
482
483 def write_aggregate():
484     """write the aggregate record"""
485     print "dn: CN=Aggregate,${SCHEMADN}\n"
486     print """objectClass: top
487 objectClass: subSchema
488 objectCategory: CN=SubSchema,${SCHEMADN}
489 """
490     if not opts.dump_subschema_auto:
491         return
492
493     for objectclass in objectclasses:
494         write_aggregate_objectclass(objectclass)
495     for attr in attributes:
496         write_aggregate_attribute(attr)
497     for objectclass in objectclasses:
498         write_aggregate_ditcontentrule(objectclass)
499
500 def load_list(file):
501     """load a list from a file"""
502     return open(file, 'r').readlines()
503
504 # get the rootDSE
505 res = ldb.search(base="", expression="", scope=SCOPE_BASE, attrs=["schemaNamingContext"])
506 rootDse = res[0]
507
508 # load the list of classes we are interested in
509 classes = load_list(classfile)
510 for classname in classes:
511     objectclass = build_objectclass(ldb, classname)
512     if objectclass is not None:
513         objectclasses[classname] = objectclass
514
515
516 #
517 #  expand the objectclass list as needed
518 #
519 expanded = 0
520
521 # so EJS do not have while nor the break statement
522 # cannot find any other way than doing more loops
523 # than necessary to recursively expand all classes
524 #
525 for inf in range(500):
526     for n in objectclasses:
527         if not n in objectclasses_expanded:
528             expand_objectclass(ldb, objectclasses[i])
529             objectclasses_expanded.add(n)
530
531 #
532 #  find objectclass properties
533 #
534 for objectclass in objectclasses:
535     find_objectclass_properties(ldb, objectclass)
536
537
538 #
539 #  form the full list of attributes
540 #
541 for objectclass in objectclasses:
542     add_objectclass_attributes(ldb, objectclass)
543
544 # and attribute properties
545 for attr in attributes:
546     find_attribute_properties(ldb, attr)
547
548 #
549 # trim the 'may' attribute lists to those really needed
550 #
551 for objectclass in objectclasses:
552     trim_objectclass_attributes(ldb, objectclass)
553
554 #
555 #  dump an ldif form of the attributes and objectclasses
556 #
557 if opts.dump_attributes:
558     write_ldif(attributes, attrib_attrs)
559 if opts.dump_classes:
560     write_ldif(objectclasses, class_attrs)
561 if opts.dump_subschema:
562     write_aggregate()
563
564 if not opts.verbose:
565     sys.exit(0)
566
567 #
568 #  dump list of objectclasses
569 #
570 print "objectClasses:\n"
571 for objectclass in objectclasses:
572     print "\t%s\n" % objectclass
573
574 print "attributes:\n"
575 for attr in attributes:
576     print "\t%s\n" % attr
577
578 print "autocreated attributes:\n"
579 for attr in attributes:
580     if attr.autocreate:
581         print "\t%s\n" % i