s4-drs: additional delete test cases
[Samba/eduardoll.git] / source4 / scripting / bin / minschema
blob6fca074d53fc01719ab414c9af8523d3e3973a5d
1 #!/usr/bin/env python
2 #
3 # Works out the minimal schema for a set of objectclasses
6 import base64
7 import optparse
8 import os
9 import sys
11 # Find right directory when running from source tree
12 sys.path.insert(0, "bin/python")
14 import samba
15 from samba import getopt as options, Ldb
16 from ldb import SCOPE_SUBTREE, SCOPE_BASE, LdbError
17 import sys
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")
31 opts, args = parser.parse_args()
32 opts.dump_all = True
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
49 if len(args) != 2:
50 parser.print_usage()
51 sys.exit(1)
53 (url, classfile) = args
55 lp_ctx = sambaopts.get_loadparm()
57 creds = credopts.get_credentials(lp_ctx)
58 ldb = Ldb(url, credentials=creds, lp=lp_ctx)
60 objectclasses = {}
61 attributes = {}
63 objectclasses_expanded = set()
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",
91 # this attributes are not used by w2k3
92 "schemaFlagsEx",
93 "msDs-IntId",
94 "msDs-Schema-Extensions",
95 "classDisplayName",
96 "isDefunct"]
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",
120 # this attributes are not used by w2k3
121 "schemaFlagsEx",
122 "msDs-IntId",
123 "msDs-Schema-Extensions",
124 "classDisplayName",
125 "isEphemeral",
126 "isDefunct"]
129 # notes:
131 # objectClassCategory
132 # 1: structural
133 # 2: abstract
134 # 3: auxiliary
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"]
143 class Objectclass(dict):
145 def __init__(self, ldb, name):
146 """create an objectclass object"""
147 self.name = name
148 self["cn"] = get_object_cn(ldb, name)
151 class Attribute(dict):
153 def __init__(self, ldb, name):
154 """create an attribute object"""
155 self.name = name
156 self["cn"] = get_object_cn(ldb, name)
159 syntaxmap = dict()
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'
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
188 def fix_dn(dn):
189 """fix a string DN to use ${SCHEMADN}"""
190 return dn.replace(rootDse["schemaNamingContext"][0], "${SCHEMADN}")
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 ""
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)
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)
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]
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]
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)
255 print "testdn is '%s'" % testdn
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
266 res = ldb.search(base=testdn, scope=ldb.SCOPE_BASE)
267 ldb.delete(testdn)
269 for a in res.msgs[0]:
270 attributes[a].autocreate = True
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)
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)
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)
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)
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
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
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
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)
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
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))
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"
422 list = attribute_list(objectclass, "systemMustContain", "mustContain")
423 line += aggregate_list("MUST", list)
425 list = attribute_list(objectclass, "systemMayContain", "mayContain")
426 line += aggregate_list("MAY", list)
428 print line + " )"
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
437 line = "dITContentRules: ( %s NAME '%s'" % (objectclass["governsID"], objectclass.name)
439 line += aggregate_list("AUX", list)
441 may_list = []
442 must_list = []
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
452 line += aggregate_list("MUST", must_list)
453 line += aggregate_list("MAY", may_list)
455 print line + " )"
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 "
467 print line + ")"
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
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)
486 def load_list(file):
487 """load a list from a file"""
488 return [l.strip("\n") for l in open(file, 'r').readlines()]
490 # get the rootDSE
491 res = ldb.search(base="", expression="", scope=SCOPE_BASE, attrs=["schemaNamingContext"])
492 rootDse = res[0]
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
503 # expand the objectclass list as needed
505 expanded = 0
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
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)
518 # find objectclass properties
520 for name, objectclass in objectclasses.items():
521 find_objectclass_properties(ldb, objectclass)
525 # form the full list of attributes
527 for name, objectclass in objectclasses.items():
528 add_objectclass_attributes(ldb, objectclass)
530 # and attribute properties
531 for name, attr in attributes.items():
532 find_attribute_properties(ldb, attr)
535 # trim the 'may' attribute lists to those really needed
537 for name, objectclass in objectclasses.items():
538 trim_objectclass_attributes(ldb, objectclass)
541 # dump an ldif form of the attributes and objectclasses
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()
550 if not opts.verbose:
551 sys.exit(0)
554 # dump list of objectclasses
556 print "objectClasses:\n"
557 for objectclass in objectclasses:
558 print "\t%s\n" % objectclass
560 print "attributes:\n"
561 for attr in attributes:
562 print "\t%s\n" % attr
564 print "autocreated attributes:\n"
565 for attr in attributes:
566 if attr.autocreate:
567 print "\t%s\n" % i