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