6fca074d53fc01719ab414c9af8523d3e3973a5d
[kai/samba.git] / source4 / scripting / bin / minschema
1 #!/usr/bin/env python
2
3 # Works 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                "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                 "adminDisplayName",
107                 "oMObjectClass",
108                 "adminDescription",
109                 "oMSyntax", 
110                 "searchFlags",
111                 "extendedCharsAllowed",
112                 "lDAPDisplayName",
113                 "schemaIDGUID",
114                 "attributeSecurityGUID",
115                 "systemOnly",
116                 "systemFlags",
117                 "isMemberOfPartialAttributeSet",
118                 "objectCategory", 
119                 
120                 # this attributes are not used by w2k3
121                 "schemaFlagsEx",
122                 "msDs-IntId",
123                 "msDs-Schema-Extensions",
124                 "classDisplayName",
125                 "isEphemeral",
126                 "isDefunct"]
127
128 #
129 #  notes:
130 #
131 #  objectClassCategory 
132 #      1: structural
133 #      2: abstract
134 #      3: auxiliary
135
136 def get_object_cn(ldb, name):
137     attrs = ["cn"]
138     res = ldb.search(expression="(ldapDisplayName=%s)" % name, base=rootDse["schemaNamingContext"][0], scope=SCOPE_SUBTREE, attrs=attrs)
139     assert len(res) == 1
140     return res[0]["cn"]
141
142
143 class Objectclass(dict):
144
145     def __init__(self, ldb, name):
146         """create an objectclass object"""
147         self.name = name
148         self["cn"] = get_object_cn(ldb, name)
149
150
151 class Attribute(dict):
152
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 s in list(syntaxmap):
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"][0], "${SCHEMADN}")
191
192
193 def write_ldif_one(o, attrs):
194     """dump an object as ldif"""
195     print "dn: CN=%s,${SCHEMADN}" % 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         v = o[a]
201         for j in v:
202                         value = fix_dn(j)
203                         if a == "oMObjectClass":
204                                 print "%s:: %s" % (a, base64.b64encode(value))
205                         elif a.endswith("GUID"):
206                                 print "%s: %s" % (a, ldb.schema_format_value(a, value))
207                         else:
208                                 print "%s: %s" % (a, value)
209     print ""
210
211
212 def write_ldif(o, attrs):
213     """dump an array of objects as ldif"""
214     for n, i in o.items():
215         write_ldif_one(i, attrs)
216
217
218 def create_testdn(exampleDN):
219     """create a testDN based an an example DN
220     the idea is to ensure we obey any structural rules"""
221     a = exampleDN.split(",")
222     a[0] = "CN=TestDN"
223     return ",".join(a)
224
225
226 def find_objectclass_properties(ldb, o):
227     """the properties of an objectclass"""
228     res = ldb.search(
229         expression="(ldapDisplayName=%s)" % o.name,
230         base=rootDse["schemaNamingContext"][0], scope=SCOPE_SUBTREE, attrs=class_attrs)
231     assert(len(res) == 1)
232     msg = res[0]
233     for a in msg:
234         o[a] = msg[a]
235
236 def find_attribute_properties(ldb, o):
237     """find the properties of an attribute"""
238     res = ldb.search(
239         expression="(ldapDisplayName=%s)" % o.name,
240         base=rootDse["schemaNamingContext"][0], scope=SCOPE_SUBTREE, 
241         attrs=attrib_attrs)
242     assert(len(res) == 1)
243     msg = res[0]
244     for a in msg:
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'" % 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" % (o.name, e)
263         print "%s" % ldif
264         return
265
266     res = ldb.search(base=testdn, scope=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         base=rootDse["schemaNamingContext"][0], scope=SCOPE_SUBTREE, 
282         attrs=attrs)
283     print >>sys.stderr, "Expanding class %s" % o.name
284     assert(len(res) == 1)
285     msg = res[0]
286     for aname in attrs:
287         if not aname in msg:
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 >>sys.stderr, "Found new objectclass '%s'" % 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, SCOPE_BASE, attrs)
320     except LdbError, e:
321         print >>sys.stderr, "Unable to fetch allowedAttributes for '%s' - %r" % (dn, e)
322         return
323     allattrs = res[0]["allowedAttributes"]
324     try:
325         res = ldb.search("objectClass=*", dn, SCOPE_BASE, allattrs)
326     except LdbError, e:
327         print >>sys.stderr, "Unable to fetch all attributes for '%s' - %s" % (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, SCOPE_DEFAULT, 
338                          ["objectClass"])
339     except LdbError, e:
340         print >>sys.stderr, "Unable to fetch objectClasses for '%s' - %s" % (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         for x in possinf:
358             if objectclasses.has_key(x):
359                 newpossinf.append(x)
360         objectclass["possibleInferiors"] = newpossinf
361
362     # trim systemMayContain,
363     # remove duplicates
364     if objectclass.has_key("systemMayContain"):
365         sysmay = objectclass["systemMayContain"]
366         newsysmay = []
367         for x in sysmay:
368             if not x in newsysmay:
369                 newsysmay.append(x)
370         objectclass["systemMayContain"] = newsysmay
371
372     # trim mayContain,
373     # remove duplicates
374     if objectclass.has_key("mayContain"):
375         may = objectclass["mayContain"]
376         newmay = []
377         if isinstance(may, str):
378             may = [may]
379         for x in may:
380             if not x in newmay:
381                 newmay.append(x)
382         objectclass["mayContain"] = newmay
383
384
385 def build_objectclass(ldb, name):
386     """load the basic attributes of an objectClass"""
387     attrs = ["name"]
388     res = ldb.search(
389         expression="(&(objectClass=classSchema)(ldapDisplayName=%s))" % name,
390         base=rootDse["schemaNamingContext"][0], scope=SCOPE_SUBTREE, 
391         attrs=attrs)
392     if len(res) == 0:
393         print >>sys.stderr, "unknown class '%s'" % name
394         return None
395     return Objectclass(ldb, name)
396
397
398 def attribute_list(objectclass, attr1, attr2):
399     """form a coalesced attribute list"""
400     a1 = list(objectclass.get(attr1, []))
401     a2 = list(objectclass.get(attr2, []))
402     return a1 + a2
403
404 def aggregate_list(name, list):
405     """write out a list in aggregate form"""
406     if list == []:
407         return ""
408     return " %s ( %s )" % (name, " $ ".join(list))
409
410 def write_aggregate_objectclass(objectclass):
411     """write the aggregate record for an objectclass"""
412     line = "objectClasses: ( %s NAME '%s' " % (objectclass["governsID"], objectclass.name)
413     if not objectclass.has_key('subClassOf'):
414         line += "SUP %s" % objectclass['subClassOf']
415     if objectclass["objectClassCategory"] == 1:
416         line += "STRUCTURAL"
417     elif objectclass["objectClassCategory"] == 2:
418         line += "ABSTRACT"
419     elif objectclass["objectClassCategory"] == 3:
420         line += "AUXILIARY"
421
422     list = attribute_list(objectclass, "systemMustContain", "mustContain")
423     line += aggregate_list("MUST", list)
424
425     list = attribute_list(objectclass, "systemMayContain", "mayContain")
426     line += aggregate_list("MAY", list)
427
428     print line + " )"
429
430
431 def write_aggregate_ditcontentrule(objectclass):
432     """write the aggregate record for an ditcontentrule"""
433     list = attribute_list(objectclass, "auxiliaryClass", "systemAuxiliaryClass")
434     if list == []:
435         return
436
437     line = "dITContentRules: ( %s NAME '%s'" % (objectclass["governsID"], objectclass.name)
438
439     line += aggregate_list("AUX", list)
440
441     may_list = []
442     must_list = []
443
444     for c in list:
445         list2 = attribute_list(objectclasses[c], 
446                        "mayContain", "systemMayContain")
447         may_list = may_list + list2
448         list2 = attribute_list(objectclasses[c], 
449                        "mustContain", "systemMustContain")
450         must_list = must_list + list2
451
452     line += aggregate_list("MUST", must_list)
453     line += aggregate_list("MAY", may_list)
454
455     print line + " )"
456
457 def write_aggregate_attribute(attrib):
458     """write the aggregate record for an attribute"""
459     line = "attributeTypes: ( %s NAME '%s' SYNTAX '%s' " % (
460            attrib["attributeID"], attrib.name, 
461            map_attribute_syntax(attrib["attributeSyntax"]))
462     if attrib.get('isSingleValued') == "TRUE":
463         line += "SINGLE-VALUE "
464     if attrib.get('systemOnly') == "TRUE":
465         line += "NO-USER-MODIFICATION "
466
467     print line + ")"
468
469
470 def write_aggregate():
471     """write the aggregate record"""
472     print "dn: CN=Aggregate,${SCHEMADN}"
473     print """objectClass: top
474 objectClass: subSchema
475 objectCategory: CN=SubSchema,${SCHEMADN}"""
476     if not opts.dump_subschema_auto:
477         return
478
479     for objectclass in objectclasses.values():
480         write_aggregate_objectclass(objectclass)
481     for attr in attributes.values():
482         write_aggregate_attribute(attr)
483     for objectclass in objectclasses.values():
484         write_aggregate_ditcontentrule(objectclass)
485
486 def load_list(file):
487     """load a list from a file"""
488     return [l.strip("\n") for l in open(file, 'r').readlines()]
489
490 # get the rootDSE
491 res = ldb.search(base="", expression="", scope=SCOPE_BASE, attrs=["schemaNamingContext"])
492 rootDse = res[0]
493
494 # load the list of classes we are interested in
495 classes = load_list(classfile)
496 for classname in classes:
497     objectclass = build_objectclass(ldb, classname)
498     if objectclass is not None:
499         objectclasses[classname] = objectclass
500
501
502 #
503 #  expand the objectclass list as needed
504 #
505 expanded = 0
506
507 # so EJS do not have while nor the break statement
508 # cannot find any other way than doing more loops
509 # than necessary to recursively expand all classes
510 #
511 for inf in range(500):
512     for n, o in objectclasses.items():
513         if not n in objectclasses_expanded:
514             expand_objectclass(ldb, o)
515             objectclasses_expanded.add(n)
516
517 #
518 #  find objectclass properties
519 #
520 for name, objectclass in objectclasses.items():
521     find_objectclass_properties(ldb, objectclass)
522
523
524 #
525 #  form the full list of attributes
526 #
527 for name, objectclass in objectclasses.items():
528     add_objectclass_attributes(ldb, objectclass)
529
530 # and attribute properties
531 for name, attr in attributes.items():
532     find_attribute_properties(ldb, attr)
533
534 #
535 # trim the 'may' attribute lists to those really needed
536 #
537 for name, objectclass in objectclasses.items():
538     trim_objectclass_attributes(ldb, objectclass)
539
540 #
541 #  dump an ldif form of the attributes and objectclasses
542 #
543 if opts.dump_attributes:
544     write_ldif(attributes, attrib_attrs)
545 if opts.dump_classes:
546     write_ldif(objectclasses, class_attrs)
547 if opts.dump_subschema:
548     write_aggregate()
549
550 if not opts.verbose:
551     sys.exit(0)
552
553 #
554 #  dump list of objectclasses
555 #
556 print "objectClasses:\n"
557 for objectclass in objectclasses:
558     print "\t%s\n" % objectclass
559
560 print "attributes:\n"
561 for attr in attributes:
562     print "\t%s\n" % attr
563
564 print "autocreated attributes:\n"
565 for attr in attributes:
566     if attr.autocreate:
567         print "\t%s\n" % i