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