scripting ntacls: Do not place a SACL in the GPO filesystem ACL
[Samba/bb.git] / source4 / scripting / bin / minschema
blob1744f6cbc3a8312c80128bffd8de13bd7fd10917
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 sys
10 # Find right directory when running from source tree
11 sys.path.insert(0, "bin/python")
13 import samba
14 from samba import getopt as options, Ldb
15 from ldb import SCOPE_SUBTREE, SCOPE_BASE, LdbError
16 import sys
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()
31 opts.dump_all = True
33 if opts.dump_classes:
34 opts.dump_all = False
35 if opts.dump_attributes:
36 opts.dump_all = False
37 if opts.dump_subschema:
38 opts.dump_all = False
39 if opts.dump_subschema_auto:
40 opts.dump_all = False
41 opts.dump_subschema = True
42 if opts.dump_all:
43 opts.dump_classes = True
44 opts.dump_attributes = True
45 opts.dump_subschema = True
46 opts.dump_subschema_auto = True
48 if len(args) != 2:
49 parser.print_usage()
50 sys.exit(1)
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)
59 objectclasses = {}
60 attributes = {}
62 objectclasses_expanded = set()
64 # the attributes we need for objectclasses
65 class_attrs = ["objectClass",
66 "subClassOf",
67 "governsID",
68 "possSuperiors",
69 "possibleInferiors",
70 "mayContain",
71 "mustContain",
72 "auxiliaryClass",
73 "rDNAttID",
74 "adminDisplayName",
75 "adminDescription",
76 "objectClassCategory",
77 "lDAPDisplayName",
78 "schemaIDGUID",
79 "systemOnly",
80 "systemPossSuperiors",
81 "systemMayContain",
82 "systemMustContain",
83 "systemAuxiliaryClass",
84 "defaultSecurityDescriptor",
85 "systemFlags",
86 "defaultHidingValue",
87 "objectCategory",
88 "defaultObjectCategory",
90 # this attributes are not used by w2k3
91 "schemaFlagsEx",
92 "msDs-IntId",
93 "msDs-Schema-Extensions",
94 "classDisplayName",
95 "isDefunct"]
97 attrib_attrs = ["objectClass",
98 "attributeID",
99 "attributeSyntax",
100 "isSingleValued",
101 "rangeLower",
102 "rangeUpper",
103 "mAPIID",
104 "linkID",
105 "adminDisplayName",
106 "oMObjectClass",
107 "adminDescription",
108 "oMSyntax",
109 "searchFlags",
110 "extendedCharsAllowed",
111 "lDAPDisplayName",
112 "schemaIDGUID",
113 "attributeSecurityGUID",
114 "systemOnly",
115 "systemFlags",
116 "isMemberOfPartialAttributeSet",
117 "objectCategory",
119 # this attributes are not used by w2k3
120 "schemaFlagsEx",
121 "msDs-IntId",
122 "msDs-Schema-Extensions",
123 "classDisplayName",
124 "isEphemeral",
125 "isDefunct"]
128 # notes:
130 # objectClassCategory
131 # 1: structural
132 # 2: abstract
133 # 3: auxiliary
135 def get_object_cn(ldb, name):
136 attrs = ["cn"]
137 res = ldb.search(expression="(ldapDisplayName=%s)" % name, base=rootDse["schemaNamingContext"][0], scope=SCOPE_SUBTREE, attrs=attrs)
138 assert len(res) == 1
139 return res[0]["cn"]
142 class Objectclass(dict):
144 def __init__(self, ldb, name):
145 """create an objectclass object"""
146 self.name = name
147 self["cn"] = get_object_cn(ldb, name)
150 class Attribute(dict):
152 def __init__(self, ldb, name):
153 """create an attribute object"""
154 self.name = name
155 self["cn"] = get_object_cn(ldb, name)
158 syntaxmap = dict()
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):
183 return syntaxmap[s]
184 return s
187 def fix_dn(dn):
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"]
195 for a in attrs:
196 if not o.has_key(a):
197 continue
198 # special case for oMObjectClass, which is a binary object
199 v = o[a]
200 for j in v:
201 value = fix_dn(j)
202 if a == "oMObjectClass":
203 print "%s:: %s" % (a, base64.b64encode(value))
204 elif a.endswith("GUID"):
205 print "%s: %s" % (a, ldb.schema_format_value(a, value))
206 else:
207 print "%s: %s" % (a, value)
208 print ""
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(",")
221 a[0] = "CN=TestDN"
222 return ",".join(a)
225 def find_objectclass_properties(ldb, o):
226 """the properties of an objectclass"""
227 res = ldb.search(
228 expression="(ldapDisplayName=%s)" % o.name,
229 base=rootDse["schemaNamingContext"][0], scope=SCOPE_SUBTREE, attrs=class_attrs)
230 assert(len(res) == 1)
231 msg = res[0]
232 for a in msg:
233 o[a] = msg[a]
235 def find_attribute_properties(ldb, o):
236 """find the properties of an attribute"""
237 res = ldb.search(
238 expression="(ldapDisplayName=%s)" % o.name,
239 base=rootDse["schemaNamingContext"][0], scope=SCOPE_SUBTREE,
240 attrs=attrib_attrs)
241 assert(len(res) == 1)
242 msg = res[0]
243 for a in msg:
244 o[a] = msg[a]
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"):
251 return
252 testdn = create_testdn(o.exampleDN)
254 print "testdn is '%s'" % testdn
256 ldif = "dn: " + testdn
257 ldif += "\nobjectClass: " + o.name
258 try:
259 ldb.add(ldif)
260 except LdbError, e:
261 print "error adding %s: %s" % (o.name, e)
262 print "%s" % ldif
263 return
265 res = ldb.search(base=testdn, scope=ldb.SCOPE_BASE)
266 ldb.delete(testdn)
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 existance of
274 more classes needed for a minimal schema"""
275 attrs = ["auxiliaryClass", "systemAuxiliaryClass",
276 "possSuperiors", "systemPossSuperiors",
277 "subClassOf"]
278 res = ldb.search(
279 expression="(&(objectClass=classSchema)(ldapDisplayName=%s))" % o.name,
280 base=rootDse["schemaNamingContext"][0], scope=SCOPE_SUBTREE,
281 attrs=attrs)
282 print >>sys.stderr, "Expanding class %s" % o.name
283 assert(len(res) == 1)
284 msg = res[0]
285 for aname in attrs:
286 if not aname in msg:
287 continue
288 list = msg[aname]
289 if isinstance(list, str):
290 list = [msg[aname]]
291 for name in list:
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
299 of attributes"""
300 attrs = ["mustContain", "systemMustContain",
301 "mayContain", "systemMayContain"]
302 for aname in attrs:
303 if not objectclass.has_key(aname):
304 continue
305 alist = objectclass[aname]
306 if isinstance(alist, str):
307 alist = [alist]
308 for a in alist:
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"]
317 try:
318 res = ldb.search("objectClass=*", dn, SCOPE_BASE, attrs)
319 except LdbError, e:
320 print >>sys.stderr, "Unable to fetch allowedAttributes for '%s' - %r" % (dn, e)
321 return
322 allattrs = res[0]["allowedAttributes"]
323 try:
324 res = ldb.search("objectClass=*", dn, SCOPE_BASE, allattrs)
325 except LdbError, e:
326 print >>sys.stderr, "Unable to fetch all attributes for '%s' - %s" % (dn, e)
327 return
328 msg = res[0]
329 for a in msg:
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"""
335 try:
336 res = ldb.search("objectClass=*", namingContext, SCOPE_DEFAULT,
337 ["objectClass"])
338 except LdbError, e:
339 print >>sys.stderr, "Unable to fetch objectClasses for '%s' - %s" % (namingContext, e)
340 return
341 for msg in res:
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"]
355 newpossinf = []
356 for x in possinf:
357 if objectclasses.has_key(x):
358 newpossinf.append(x)
359 objectclass["possibleInferiors"] = newpossinf
361 # trim systemMayContain,
362 # remove duplicates
363 if objectclass.has_key("systemMayContain"):
364 sysmay = objectclass["systemMayContain"]
365 newsysmay = []
366 for x in sysmay:
367 if not x in newsysmay:
368 newsysmay.append(x)
369 objectclass["systemMayContain"] = newsysmay
371 # trim mayContain,
372 # remove duplicates
373 if objectclass.has_key("mayContain"):
374 may = objectclass["mayContain"]
375 newmay = []
376 if isinstance(may, str):
377 may = [may]
378 for x in may:
379 if not x in newmay:
380 newmay.append(x)
381 objectclass["mayContain"] = newmay
384 def build_objectclass(ldb, name):
385 """load the basic attributes of an objectClass"""
386 attrs = ["name"]
387 res = ldb.search(
388 expression="(&(objectClass=classSchema)(ldapDisplayName=%s))" % name,
389 base=rootDse["schemaNamingContext"][0], scope=SCOPE_SUBTREE,
390 attrs=attrs)
391 if len(res) == 0:
392 print >>sys.stderr, "unknown class '%s'" % name
393 return None
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, []))
401 return a1 + a2
403 def aggregate_list(name, list):
404 """write out a list in aggregate form"""
405 if list == []:
406 return ""
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:
415 line += "STRUCTURAL"
416 elif objectclass["objectClassCategory"] == 2:
417 line += "ABSTRACT"
418 elif objectclass["objectClassCategory"] == 3:
419 line += "AUXILIARY"
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)
427 print line + " )"
430 def write_aggregate_ditcontentrule(objectclass):
431 """write the aggregate record for an ditcontentrule"""
432 list = attribute_list(objectclass, "auxiliaryClass", "systemAuxiliaryClass")
433 if list == []:
434 return
436 line = "dITContentRules: ( %s NAME '%s'" % (objectclass["governsID"], objectclass.name)
438 line += aggregate_list("AUX", list)
440 may_list = []
441 must_list = []
443 for c in 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)
454 print line + " )"
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 "
466 print line + ")"
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:
476 return
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)
485 def load_list(file):
486 """load a list from a file"""
487 return [l.strip("\n") for l in open(file, 'r').readlines()]
489 # get the rootDSE
490 res = ldb.search(base="", expression="", scope=SCOPE_BASE, attrs=["schemaNamingContext"])
491 rootDse = res[0]
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
504 expanded = 0
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:
547 write_aggregate()
549 if not opts.verbose:
550 sys.exit(0)
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:
565 if attr.autocreate:
566 print "\t%s\n" % i