Start working on python conversion of minschema.
[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 getopt
7 import optparse
8 import samba
9
10 parser = optparse.OptionParser("minschema <URL> <classfile>")
11 sambaopts = options.SambaOptions(parser)
12 parser.add_option_group(sambaopts)
13 credopts = options.CredentialsOptions(parser)
14 parser.add_option_group(credopts)
15 parser.add_option_group(options.VersionOptions(parser))
16 parser.add_option("--verbose", help="Be verbose", action="store_true")
17 parser.add_option("--dump-classes", action="store_true")
18 parser.add_option("--dump-attributes", action="store_true")
19 parser.add_option("--dump-subschema", action="store_true")
20 parser.add_option("--dump-subschema-auto", action="store_true")
21
22 opts, args = parser.parse_args()
23 opts.dump_all = True
24
25 if opts.dump_classes:
26     opts.dump_all = False
27 if opts.dump_attributes:
28     opts.dump_all = False
29 if opts.dump_subschema:
30     opts.dump_all = False
31 if dump_subschema_auto:
32         opts.dump_all = False
33         opts.dump_subschema = True
34 if opts.dump_all:
35         opts.dump_classes = True
36         opts.dump_attributes = True
37         opts.dump_subschema = True
38         opts.dump_subschema_auto = True
39
40 if len(args) != 2:
41     parser.print_usage()
42     sys.exit(1)
43
44 (url, classfile) = args
45
46 creds = credopts.get_credentials()
47 ldb = Ldb(url, credentials=creds)
48
49 objectclasses = []
50 attributes = []
51 rootDse = {}
52
53 objectclasses_expanded = []
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     cn = res[0]["cn"]
142
143 #
144 #  create an objectclass object
145 #
146 def obj_objectClass(ldb, name):
147         var o = new Object()
148         o.name = name
149         o.cn = get_object_cn(ldb, name)
150         return o
151
152 #
153 #  create an attribute object
154 #
155 def obj_attribute(ldb, name):
156         var o = new Object()
157         o.name = name
158         o.cn = get_object_cn(ldb, name)
159         return o
160
161
162 syntaxmap = dict()
163
164 syntaxmap['2.5.5.1']  = '1.3.6.1.4.1.1466.115.121.1.12'
165 syntaxmap['2.5.5.2']  = '1.3.6.1.4.1.1466.115.121.1.38'
166 syntaxmap['2.5.5.3']  = '1.2.840.113556.1.4.1362'
167 syntaxmap['2.5.5.4']  = '1.2.840.113556.1.4.905'
168 syntaxmap['2.5.5.5']  = '1.3.6.1.4.1.1466.115.121.1.26'
169 syntaxmap['2.5.5.6']  = '1.3.6.1.4.1.1466.115.121.1.36'
170 syntaxmap['2.5.5.7']  = '1.2.840.113556.1.4.903'
171 syntaxmap['2.5.5.8']  = '1.3.6.1.4.1.1466.115.121.1.7'
172 syntaxmap['2.5.5.9']  = '1.3.6.1.4.1.1466.115.121.1.27'
173 syntaxmap['2.5.5.10'] = '1.3.6.1.4.1.1466.115.121.1.40'
174 syntaxmap['2.5.5.11'] = '1.3.6.1.4.1.1466.115.121.1.24'
175 syntaxmap['2.5.5.12'] = '1.3.6.1.4.1.1466.115.121.1.15'
176 syntaxmap['2.5.5.13'] = '1.3.6.1.4.1.1466.115.121.1.43'
177 syntaxmap['2.5.5.14'] = '1.2.840.113556.1.4.904'
178 syntaxmap['2.5.5.15'] = '1.2.840.113556.1.4.907'
179 syntaxmap['2.5.5.16'] = '1.2.840.113556.1.4.906'
180 syntaxmap['2.5.5.17'] = '1.3.6.1.4.1.1466.115.121.1.40'
181
182 #
183 #  map some attribute syntaxes from some apparently MS specific
184 #  syntaxes to the standard syntaxes
185 #
186 def map_attribute_syntax(s):
187     if syntaxmap.has_key(s):
188                 return syntaxmap[s]
189         return s
190
191 #
192 #  fix a string DN to use ${SCHEMADN}
193 #
194 def fix_dn(dn):
195         s = strstr(dn, rootDse.schemaNamingContext)
196         if (s == NULL) {
197                 return dn
198         }
199         return substr(dn, 0, strlen(dn) - strlen(s)) + "${SCHEMADN}"
200
201 #
202 #  dump an object as ldif
203 #
204 def write_ldif_one(o, attrs):
205         print "dn: CN=%s,${SCHEMADN}\n" % o["cn"]
206     for a in attrs:
207         if not o.has_key(a):
208                         continue
209                 # special case for oMObjectClass, which is a binary object
210         if a == "oMObjectClass":
211                         print "%s:: %s\n" % (a, o[a])
212                         continue
213                 v = o[a]
214         if isinstance(v, str):
215                         v = [v]
216         for j in v:
217                         print "%s: %s\n" % (a, fix_dn(j))
218         print "\n"
219
220 #
221 # dump an array of objects as ldif
222 #
223 def write_ldif(o, attrs):
224     for i in o:
225                 write_ldif_one(i, attrs)
226
227
228 #
229 #  create a testDN based an an example DN
230 #  the idea is to ensure we obey any structural rules
231 #
232 def create_testdn(exampleDN):
233         a = split(",", exampleDN)
234         a[0] = "CN=TestDN"
235         return ",".join(a)
236
237 #
238 #  find the properties of an objectclass
239 #
240 def find_objectclass_properties(ldb, o):
241         res = ldb.search(
242                 expression="(ldapDisplayName=%s)" % o.name,
243                 rootDse.schemaNamingContext, ldb.SCOPE_SUBTREE, class_attrs)
244         assert(len(res) == 1)
245     msg = res[0]
246     for a in msg:
247                 o[a] = msg[a]
248
249 #
250 #  find the properties of an attribute
251 #
252 def find_attribute_properties(ldb, o):
253         res = ldb.search(
254                 expression="(ldapDisplayName=%s)" % o.name,
255                 rootDse.schemaNamingContext, ldb.SCOPE_SUBTREE, attrib_attrs)
256         assert(len(res) == 1)
257     msg = res[0]
258     for a in msg:
259                 # special case for oMObjectClass, which is a binary object
260         if a == "oMObjectClass":
261                         o[a] = ldb.encode(msg[a])
262                         continue
263                 o[a] = msg[a]
264
265 #
266 #  find the auto-created properties of an objectclass. Only works for classes
267 #  that can be created using just a DN and the objectclass
268 #
269 def find_objectclass_auto(ldb, o):
270     if not o.has_key("exampleDN"):
271                 return
272         testdn = create_testdn(o.exampleDN)
273
274         print "testdn is '%s'\n" % testdn
275
276         ldif = "dn: " + testdn
277         ldif += "\nobjectClass: " + o.name
278     try:
279         ldb.add(ldif)
280     except LdbError, e:
281         print "error adding %s: %s\n" % (o.name, e)
282                 print "%s\n" % ldif
283         return
284
285         res = ldb.search("", testdn, ldb.SCOPE_BASE)
286         ldb.delete(testdn)
287
288     for a in res.msgs[0]:
289                 attributes[a].autocreate = True
290
291
292 #
293 #  look at auxiliary information from a class to intuit the existance of more
294 #  classes needed for a minimal schema
295 #
296 def expand_objectclass(ldb, o):
297         attrs = ["auxiliaryClass", "systemAuxiliaryClass",
298                               "possSuperiors", "systemPossSuperiors",
299                               "subClassOf"]
300         res = ldb.search(
301                 "(&(objectClass=classSchema)(ldapDisplayName=%s))" % o.name,
302                 rootDse.schemaNamingContext, ldb.SCOPE_SUBTREE, attrs)
303         print "Expanding class %s\n" % o.name
304         assert(len(res) == 1)
305         msg = res[0]
306     for a in attrs:
307         if not msg.has_key(aname):
308                         continue
309                 list = msg[aname]
310         if isinstance(list, str):
311                         list = [msg[aname]]
312         for name in list:
313             if not objectclasses.has_key(name):
314                                 print "Found new objectclass '%s'\n" % name
315                                 objectclasses[name] = obj_objectClass(ldb, name)
316
317
318 #
319 #  add the must and may attributes from an objectclass to the full list
320 #  of attributes
321 #
322 def add_objectclass_attributes(ldb, class):
323         attrs = ["mustContain", "systemMustContain", 
324                               "mayContain", "systemMayContain"]
325     for aname in attrs:
326         if not class.has_key(aname):
327                         continue
328                 alist = class[aname]
329         if isinstance(alist, str):
330                         alist = [alist]
331         for a in alist:
332             if not attributes.has_key(a):
333                                 attributes[a] = obj_attribute(ldb, a)
334
335
336 #
337 #  process an individual record, working out what attributes it has
338 #
339 def walk_dn(ldb, dn):
340         # get a list of all possible attributes for this object 
341         attrs = ["allowedAttributes"]
342     try:
343         res = ldb.search("objectClass=*", dn, ldb.SCOPE_BASE, attrs)
344     except LdbError, e:
345                 print "Unable to fetch allowedAttributes for '%s' - %r\n" % (dn, e)
346                 return
347         allattrs = res[0]["allowedAttributes"]
348     try:
349         res = ldb.search("objectClass=*", dn, ldb.SCOPE_BASE, allattrs)
350     except LdbError, e:
351         print "Unable to fetch all attributes for '%s' - %s\n" % (dn, e)
352                 return
353         msg = res[0]
354     for a in msg:
355         if not attributes.has_key(a):
356                         attributes[a] = obj_attribute(ldb, a)
357
358 #
359 #  walk a naming context, looking for all records
360 #
361 def walk_naming_context(ldb, namingContext):
362     try:
363         res = ldb.search("objectClass=*", namingContext, ldb.SCOPE_DEFAULT, 
364                          ["objectClass"])
365     except LdbError, e:
366                 print "Unable to fetch objectClasses for '%s' - %s\n" % (namingContext, e)
367                 return
368     for msg in res:
369                 msg = res.msgs[r]["objectClass"]
370         for objectClass in msg:
371             if not objectclasses.has_key(objectClass):
372                                 objectclasses[objectClass] = obj_objectClass(ldb, objectClass)
373                                 objectclasses[objectClass].exampleDN = res.msgs[r]["dn"]
374                 walk_dn(ldb, res.msgs[r].dn)
375
376 #
377 #  trim the may attributes for an objectClass
378 #
379 def trim_objectclass_attributes(ldb, class):
380         # trim possibleInferiors,
381         # include only the classes we extracted
382     if class.has_key("possibleInferiors"):
383         possinf = class["possibleInferiors"]
384                 newpossinf = []
385         if isinstance(possinf, str):
386                         possinf = [possinf]
387         for x in possinf:
388             if objectclasses.has_key(x):
389                                 newpossinf[n] = x
390                                 n++
391                 class["possibleInferiors"] = newpossinf
392
393         # trim systemMayContain,
394         # remove duplicates
395     if class.has_key("systemMayContain"):
396         sysmay = class["systemMayContain"]
397                 newsysmay = []
398         if isinstance(sysmay, str):
399                         sysmay = [sysmay]
400         for x in sysmay:
401                         dup = False
402                         if newsysmay[0] == undefined) {
403                                 newsysmay[0] = x
404             else:
405                                 for (n = 0; n < newsysmay.length; n++) {
406                                         if (newsysmay[n] == x) {
407                                                 dup = True
408                 if not dup:
409                                         newsysmay[n] = x
410                 class["systemMayContain"] = newsysmay
411
412         # trim mayContain,
413         # remove duplicates
414     if not class.has_key("mayContain"):
415         may = class["mayContain"]
416                 newmay = []
417         if isinstance(may, str):
418                         may = [may]
419         for x in may:
420                         dup = False
421                         if (newmay[0] == undefined) {
422                                 newmay[0] = x
423                         } else {
424                                 for (n = 0; n < newmay.length; n++) {
425                                         if (newmay[n] == x) {
426                                                 dup = True
427                 if not dup:
428                                         newmay[n] = x
429                 class["mayContain"] = newmay
430
431 #
432 #  load the basic attributes of an objectClass
433 #
434 def build_objectclass(ldb, name):
435         attrs = ["name"]
436     try:
437         res = ldb.search(
438             expression="(&(objectClass=classSchema)(ldapDisplayName=%s))" % name,
439             rootDse.schemaNamingContext, ldb.SCOPE_SUBTREE, attrs)
440     except LdbError, e:
441                 print "unknown class '%s'\n" % name
442                 return None
443     if len(res) == 0:
444                 print "unknown class '%s'\n" % name
445                 return None
446         return obj_objectClass(ldb, name)
447
448 #
449 #  append 2 lists
450 #
451 def list_append(a1, a2):
452         if (a1 == undefined) {
453                 return a2
454         if (a2 == undefined)
455                 return a1
456     for (i=0;i<a2.length;i++):
457                 a1[a1.length] = a2[i]
458         return a1
459
460 #
461 #  form a coalesced attribute list
462 #
463 def attribute_list(class, attr1, attr2):
464         a1 = class[attr1]
465         a2 = class[attr2]
466     if isinstance(a1, str):
467                 a1 = [a1]
468     if isinstance(a2, str):
469                 a2 = [a2]
470         return list_append(a1, a2)
471
472 #
473 #  write out a list in aggregate form
474 #
475 def aggregate_list(name, list):
476     if list is None:
477                 return
478         print "%s ( %s )" % (name, "$ ".join(list))
479
480 #
481 #  write the aggregate record for an objectclass
482 #
483 def write_aggregate_objectclass(class):
484         print "objectClasses: ( %s NAME '%s' " % (class.governsID, class.name)
485     if not class.has_key('subClassOf'):
486                 print "SUP %s " % class['subClassOf']
487     if class.objectClassCategory == 1:
488                 print "STRUCTURAL "
489     elif class.objectClassCategory == 2:
490                 print "ABSTRACT "
491     elif class.objectClassCategory == 3:
492                 print "AUXILIARY "
493
494         list = attribute_list(class, "systemMustContain", "mustContain")
495         aggregate_list("MUST", list)
496
497         list = attribute_list(class, "systemMayContain", "mayContain")
498         aggregate_list("MAY", list)
499
500         print ")\n"
501
502
503 #
504 #  write the aggregate record for an ditcontentrule
505 #
506 def write_aggregate_ditcontentrule(class):
507         list = attribute_list(class, "auxiliaryClass", "systemAuxiliaryClass")
508     if list is None:
509                 return
510
511         print "dITContentRules: ( %s NAME '%s' " % (class.governsID, class.name)
512
513         aggregate_list("AUX", list)
514
515         may_list = None
516         must_list = None
517
518     for c in list:
519                 list2 = attribute_list(objectclasses[c], 
520                                        "mayContain", "systemMayContain")
521                 may_list = list_append(may_list, list2)
522                 list2 = attribute_list(objectclasses[c], 
523                                        "mustContain", "systemMustContain")
524                 must_list = list_append(must_list, list2)
525
526         aggregate_list("MUST", must_list)
527         aggregate_list("MAY", may_list)
528
529         print ")\n"
530
531 #
532 #  write the aggregate record for an attribute
533 #
534 def write_aggregate_attribute(attrib):
535         print "attributeTypes: ( %s NAME '%s' SYNTAX '%s' " % (
536                attrib.attributeID, attrib.name, 
537                map_attribute_syntax(attrib.attributeSyntax))
538     if attrib['isSingleValued'] == "TRUE":
539                 print "SINGLE-VALUE "
540     if attrib['systemOnly'] == "TRUE":
541                 print "NO-USER-MODIFICATION "
542
543         print ")\n"
544
545
546 #
547 #  write the aggregate record
548 #
549 def write_aggregate():
550         print "dn: CN=Aggregate,${SCHEMADN}\n"
551         print """objectClass: top
552 objectClass: subSchema
553 objectCategory: CN=SubSchema,${SCHEMADN}
554 """
555     if not opts.dump_subschema_auto:
556                 return
557
558     for objectclass in objectclasses:
559                 write_aggregate_objectclass(objectclass)
560     for attr in attributes:
561                 write_aggregate_attribute(attr)
562     for objectclass in objectclasses:
563                 write_aggregate_ditcontentrule(objectclass)
564
565 #
566 #  load a list from a file
567 #
568 def load_list(file):
569         var sys = sys_init()
570         var s = sys.file_load(file)
571         var a = split("\n", s)
572         return a
573
574 # get the rootDSE
575 res = ldb.search("", "", ldb.SCOPE_BASE)
576 rootDse = res[0]
577
578 # load the list of classes we are interested in
579 classes = load_list(classfile)
580 for (i=0;i<classes.length;i++) {
581         var classname = classes[i]
582         var class = build_objectclass(ldb, classname)
583         if (class != undefined) {
584                 objectclasses[classname] = class
585
586
587 #
588 #  expand the objectclass list as needed
589 #
590 num_classes = 0
591 expanded = 0
592 # calculate the actual number of classes
593 num_classes = len(objectclasses)
594
595 # so EJS do not have while nor the break statement
596 # cannot find any other way than doing more loops
597 # than necessary to recursively expand all classes
598 #
599 for inf in range(500):
600     if expanded < num_classes:
601                 for (i in objectclasses) {
602                         var n = objectclasses[i]
603                         if (objectclasses_expanded[i] != "DONE") {
604                                 expand_objectclass(ldb, objectclasses[i])
605                                 objectclasses_expanded[i] = "DONE"
606                                 expanded++
607                 # recalculate the actual number of classes
608                 num_classes = len(objectclasses)
609
610 #
611 #  find objectclass properties
612 #
613 for objectclass in objectclasses:
614         find_objectclass_properties(ldb, objectclass)
615
616
617 #
618 #  form the full list of attributes
619 #
620 for objectclass in objectclasses:
621         add_objectclass_attributes(ldb, objectclass)
622
623 # and attribute properties
624 for attr in attributes:
625         find_attribute_properties(ldb, attr)
626
627 #
628 # trim the 'may' attribute lists to those really needed
629 #
630 for objectclass in objectclasses) {
631         trim_objectclass_attributes(ldb, objectclass)
632
633 #
634 #  dump an ldif form of the attributes and objectclasses
635 #
636 if opts.dump_attributes:
637         write_ldif(attributes, attrib_attrs)
638 if opts.dump_classes:
639         write_ldif(objectclasses, class_attrs)
640 if opts.dump_subschema:
641         write_aggregate()
642
643 if not opts.verbose:
644         sys.exit(0)
645
646 #
647 #  dump list of objectclasses
648 #
649 print "objectClasses:\n"
650 for objectclass in objectclasses:
651         print "\t%s\n" % objectclass
652
653 print "attributes:\n"
654 for attr in attributes:
655         print "\t%s\n" % ttr
656
657 print "autocreated attributes:\n"
658 for (i in attributes) {
659         if (attributes[i].autocreate == true) {
660                 print "\t%s\n" % i