3 # Works out the minimal schema for a set of objectclasses
10 # Find right directory when running from source tree
11 sys.path.insert(0, "bin/python")
14 from samba import getopt as options, Ldb
15 from ldb import SCOPE_SUBTREE, SCOPE_BASE, LdbError
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")
30 opts, args = parser.parse_args()
35 if opts.dump_attributes:
37 if opts.dump_subschema:
39 if opts.dump_subschema_auto:
41 opts.dump_subschema = True
43 opts.dump_classes = True
44 opts.dump_attributes = True
45 opts.dump_subschema = True
46 opts.dump_subschema_auto = True
52 (url, classfile) = args
54 lp_ctx = sambaopts.get_loadparm()
56 creds = credopts.get_credentials(lp_ctx)
57 ldb = Ldb(url, credentials=creds, lp=lp_ctx)
62 objectclasses_expanded = set()
64 # the attributes we need for objectclasses
65 class_attrs = ["objectClass",
76 "objectClassCategory",
80 "systemPossSuperiors",
83 "systemAuxiliaryClass",
84 "defaultSecurityDescriptor",
88 "defaultObjectCategory",
90 # this attributes are not used by w2k3
93 "msDs-Schema-Extensions",
97 attrib_attrs = ["objectClass",
110 "extendedCharsAllowed",
113 "attributeSecurityGUID",
116 "isMemberOfPartialAttributeSet",
119 # this attributes are not used by w2k3
122 "msDs-Schema-Extensions",
130 # objectClassCategory
135 def get_object_cn(ldb, name):
137 res = ldb.search(expression="(ldapDisplayName=%s)" % name, base=rootDse["schemaNamingContext"][0], scope=SCOPE_SUBTREE, attrs=attrs)
142 class Objectclass(dict):
144 def __init__(self, ldb, name):
145 """create an objectclass object"""
147 self["cn"] = get_object_cn(ldb, name)
150 class Attribute(dict):
152 def __init__(self, ldb, name):
153 """create an attribute object"""
155 self["cn"] = get_object_cn(ldb, name)
160 syntaxmap['2.5.5.1'] = '1.3.6.1.4.1.1466.115.121.1.12'
161 syntaxmap['2.5.5.2'] = '1.3.6.1.4.1.1466.115.121.1.38'
162 syntaxmap['2.5.5.3'] = '1.2.840.113556.1.4.1362'
163 syntaxmap['2.5.5.4'] = '1.2.840.113556.1.4.905'
164 syntaxmap['2.5.5.5'] = '1.3.6.1.4.1.1466.115.121.1.26'
165 syntaxmap['2.5.5.6'] = '1.3.6.1.4.1.1466.115.121.1.36'
166 syntaxmap['2.5.5.7'] = '1.2.840.113556.1.4.903'
167 syntaxmap['2.5.5.8'] = '1.3.6.1.4.1.1466.115.121.1.7'
168 syntaxmap['2.5.5.9'] = '1.3.6.1.4.1.1466.115.121.1.27'
169 syntaxmap['2.5.5.10'] = '1.3.6.1.4.1.1466.115.121.1.40'
170 syntaxmap['2.5.5.11'] = '1.3.6.1.4.1.1466.115.121.1.24'
171 syntaxmap['2.5.5.12'] = '1.3.6.1.4.1.1466.115.121.1.15'
172 syntaxmap['2.5.5.13'] = '1.3.6.1.4.1.1466.115.121.1.43'
173 syntaxmap['2.5.5.14'] = '1.2.840.113556.1.4.904'
174 syntaxmap['2.5.5.15'] = '1.2.840.113556.1.4.907'
175 syntaxmap['2.5.5.16'] = '1.2.840.113556.1.4.906'
176 syntaxmap['2.5.5.17'] = '1.3.6.1.4.1.1466.115.121.1.40'
179 def map_attribute_syntax(s):
180 """map some attribute syntaxes from some apparently MS specific
181 syntaxes to the standard syntaxes"""
182 if s in list(syntaxmap):
188 """fix a string DN to use ${SCHEMADN}"""
189 return dn.replace(rootDse["schemaNamingContext"][0], "${SCHEMADN}")
192 def write_ldif_one(o, attrs):
193 """dump an object as ldif"""
194 print "dn: CN=%s,${SCHEMADN}" % o["cn"]
198 # special case for oMObjectClass, which is a binary object
202 if a == "oMObjectClass":
203 print "%s:: %s" % (a, base64.b64encode(value).decode('utf8'))
204 elif a.endswith("GUID"):
205 print "%s: %s" % (a, ldb.schema_format_value(a, value))
207 print "%s: %s" % (a, value)
211 def write_ldif(o, attrs):
212 """dump an array of objects as ldif"""
213 for n, i in o.items():
214 write_ldif_one(i, attrs)
217 def create_testdn(exampleDN):
218 """create a testDN based an an example DN
219 the idea is to ensure we obey any structural rules"""
220 a = exampleDN.split(",")
225 def find_objectclass_properties(ldb, o):
226 """the properties of an objectclass"""
228 expression="(ldapDisplayName=%s)" % o.name,
229 base=rootDse["schemaNamingContext"][0], scope=SCOPE_SUBTREE, attrs=class_attrs)
230 assert(len(res) == 1)
235 def find_attribute_properties(ldb, o):
236 """find the properties of an attribute"""
238 expression="(ldapDisplayName=%s)" % o.name,
239 base=rootDse["schemaNamingContext"][0], scope=SCOPE_SUBTREE,
241 assert(len(res) == 1)
247 def find_objectclass_auto(ldb, o):
248 """find the auto-created properties of an objectclass. Only works for
249 classes that can be created using just a DN and the objectclass"""
250 if not o.has_key("exampleDN"):
252 testdn = create_testdn(o.exampleDN)
254 print "testdn is '%s'" % testdn
256 ldif = "dn: " + testdn
257 ldif += "\nobjectClass: " + o.name
260 except LdbError as e:
261 print "error adding %s: %s" % (o.name, e)
265 res = ldb.search(base=testdn, scope=ldb.SCOPE_BASE)
268 for a in res.msgs[0]:
269 attributes[a].autocreate = True
272 def expand_objectclass(ldb, o):
273 """look at auxiliary information from a class to intuit the existence of
274 more classes needed for a minimal schema"""
275 attrs = ["auxiliaryClass", "systemAuxiliaryClass",
276 "possSuperiors", "systemPossSuperiors",
279 expression="(&(objectClass=classSchema)(ldapDisplayName=%s))" % o.name,
280 base=rootDse["schemaNamingContext"][0], scope=SCOPE_SUBTREE,
282 print >>sys.stderr, "Expanding class %s" % o.name
283 assert(len(res) == 1)
289 if isinstance(list, str):
292 if not objectclasses.has_key(name):
293 print >>sys.stderr, "Found new objectclass '%s'" % name
294 objectclasses[name] = Objectclass(ldb, name)
297 def add_objectclass_attributes(ldb, objectclass):
298 """add the must and may attributes from an objectclass to the full list
300 attrs = ["mustContain", "systemMustContain",
301 "mayContain", "systemMayContain"]
303 if not objectclass.has_key(aname):
305 alist = objectclass[aname]
306 if isinstance(alist, str):
309 if not attributes.has_key(a):
310 attributes[a] = Attribute(ldb, a)
313 def walk_dn(ldb, dn):
314 """process an individual record, working out what attributes it has"""
315 # get a list of all possible attributes for this object
316 attrs = ["allowedAttributes"]
318 res = ldb.search("objectClass=*", dn, SCOPE_BASE, attrs)
319 except LdbError as e:
320 print >>sys.stderr, "Unable to fetch allowedAttributes for '%s' - %r" % (dn, e)
322 allattrs = res[0]["allowedAttributes"]
324 res = ldb.search("objectClass=*", dn, SCOPE_BASE, allattrs)
325 except LdbError as e:
326 print >>sys.stderr, "Unable to fetch all attributes for '%s' - %s" % (dn, e)
330 if not attributes.has_key(a):
331 attributes[a] = Attribute(ldb, a)
333 def walk_naming_context(ldb, namingContext):
334 """walk a naming context, looking for all records"""
336 res = ldb.search("objectClass=*", namingContext, SCOPE_DEFAULT,
338 except LdbError as e:
339 print >>sys.stderr, "Unable to fetch objectClasses for '%s' - %s" % (namingContext, e)
342 msg = res.msgs[r]["objectClass"]
343 for objectClass in msg:
344 if not objectclasses.has_key(objectClass):
345 objectclasses[objectClass] = Objectclass(ldb, objectClass)
346 objectclasses[objectClass].exampleDN = res.msgs[r]["dn"]
347 walk_dn(ldb, res.msgs[r].dn)
349 def trim_objectclass_attributes(ldb, objectclass):
350 """trim the may attributes for an objectClass"""
351 # trim possibleInferiors,
352 # include only the classes we extracted
353 if objectclass.has_key("possibleInferiors"):
354 possinf = objectclass["possibleInferiors"]
357 if objectclasses.has_key(x):
359 objectclass["possibleInferiors"] = newpossinf
361 # trim systemMayContain,
363 if objectclass.has_key("systemMayContain"):
364 sysmay = objectclass["systemMayContain"]
367 if not x in newsysmay:
369 objectclass["systemMayContain"] = newsysmay
373 if objectclass.has_key("mayContain"):
374 may = objectclass["mayContain"]
376 if isinstance(may, str):
381 objectclass["mayContain"] = newmay
384 def build_objectclass(ldb, name):
385 """load the basic attributes of an objectClass"""
388 expression="(&(objectClass=classSchema)(ldapDisplayName=%s))" % name,
389 base=rootDse["schemaNamingContext"][0], scope=SCOPE_SUBTREE,
392 print >>sys.stderr, "unknown class '%s'" % name
394 return Objectclass(ldb, name)
397 def attribute_list(objectclass, attr1, attr2):
398 """form a coalesced attribute list"""
399 a1 = list(objectclass.get(attr1, []))
400 a2 = list(objectclass.get(attr2, []))
403 def aggregate_list(name, list):
404 """write out a list in aggregate form"""
407 return " %s ( %s )" % (name, " $ ".join(list))
409 def write_aggregate_objectclass(objectclass):
410 """write the aggregate record for an objectclass"""
411 line = "objectClasses: ( %s NAME '%s' " % (objectclass["governsID"], objectclass.name)
412 if not objectclass.has_key('subClassOf'):
413 line += "SUP %s" % objectclass['subClassOf']
414 if objectclass["objectClassCategory"] == 1:
416 elif objectclass["objectClassCategory"] == 2:
418 elif objectclass["objectClassCategory"] == 3:
421 list = attribute_list(objectclass, "systemMustContain", "mustContain")
422 line += aggregate_list("MUST", list)
424 list = attribute_list(objectclass, "systemMayContain", "mayContain")
425 line += aggregate_list("MAY", list)
430 def write_aggregate_ditcontentrule(objectclass):
431 """write the aggregate record for an ditcontentrule"""
432 list = attribute_list(objectclass, "auxiliaryClass", "systemAuxiliaryClass")
436 line = "dITContentRules: ( %s NAME '%s'" % (objectclass["governsID"], objectclass.name)
438 line += aggregate_list("AUX", list)
444 list2 = attribute_list(objectclasses[c],
445 "mayContain", "systemMayContain")
446 may_list = may_list + list2
447 list2 = attribute_list(objectclasses[c],
448 "mustContain", "systemMustContain")
449 must_list = must_list + list2
451 line += aggregate_list("MUST", must_list)
452 line += aggregate_list("MAY", may_list)
456 def write_aggregate_attribute(attrib):
457 """write the aggregate record for an attribute"""
458 line = "attributeTypes: ( %s NAME '%s' SYNTAX '%s' " % (
459 attrib["attributeID"], attrib.name,
460 map_attribute_syntax(attrib["attributeSyntax"]))
461 if attrib.get('isSingleValued') == "TRUE":
462 line += "SINGLE-VALUE "
463 if attrib.get('systemOnly') == "TRUE":
464 line += "NO-USER-MODIFICATION "
469 def write_aggregate():
470 """write the aggregate record"""
471 print "dn: CN=Aggregate,${SCHEMADN}"
472 print """objectClass: top
473 objectClass: subSchema
474 objectCategory: CN=SubSchema,${SCHEMADN}"""
475 if not opts.dump_subschema_auto:
478 for objectclass in objectclasses.values():
479 write_aggregate_objectclass(objectclass)
480 for attr in attributes.values():
481 write_aggregate_attribute(attr)
482 for objectclass in objectclasses.values():
483 write_aggregate_ditcontentrule(objectclass)
486 """load a list from a file"""
487 return [l.strip("\n") for l in open(file, 'r').readlines()]
490 res = ldb.search(base="", expression="", scope=SCOPE_BASE, attrs=["schemaNamingContext"])
493 # load the list of classes we are interested in
494 classes = load_list(classfile)
495 for classname in classes:
496 objectclass = build_objectclass(ldb, classname)
497 if objectclass is not None:
498 objectclasses[classname] = objectclass
502 # expand the objectclass list as needed
506 # so EJS do not have while nor the break statement
507 # cannot find any other way than doing more loops
508 # than necessary to recursively expand all classes
510 for inf in range(500):
511 for n, o in objectclasses.items():
512 if not n in objectclasses_expanded:
513 expand_objectclass(ldb, o)
514 objectclasses_expanded.add(n)
517 # find objectclass properties
519 for name, objectclass in objectclasses.items():
520 find_objectclass_properties(ldb, objectclass)
524 # form the full list of attributes
526 for name, objectclass in objectclasses.items():
527 add_objectclass_attributes(ldb, objectclass)
529 # and attribute properties
530 for name, attr in attributes.items():
531 find_attribute_properties(ldb, attr)
534 # trim the 'may' attribute lists to those really needed
536 for name, objectclass in objectclasses.items():
537 trim_objectclass_attributes(ldb, objectclass)
540 # dump an ldif form of the attributes and objectclasses
542 if opts.dump_attributes:
543 write_ldif(attributes, attrib_attrs)
544 if opts.dump_classes:
545 write_ldif(objectclasses, class_attrs)
546 if opts.dump_subschema:
553 # dump list of objectclasses
555 print "objectClasses:\n"
556 for objectclass in objectclasses:
557 print "\t%s\n" % objectclass
559 print "attributes:\n"
560 for attr in attributes:
561 print "\t%s\n" % attr
563 print "autocreated attributes:\n"
564 for attr in attributes: