samba_gpoupdate: Rename the command to samba-gpupdate
[nivanova/samba-autobuild/.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 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, Ldb
15 from ldb import SCOPE_SUBTREE, SCOPE_BASE, LdbError
16 import sys
17
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")
29
30 opts, args = parser.parse_args()
31 opts.dump_all = True
32
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
47
48 if len(args) != 2:
49     parser.print_usage()
50     sys.exit(1)
51
52 (url, classfile) = args
53
54 lp_ctx = sambaopts.get_loadparm()
55
56 creds = credopts.get_credentials(lp_ctx)
57 ldb = Ldb(url, credentials=creds, lp=lp_ctx)
58
59 objectclasses = {}
60 attributes = {}
61
62 objectclasses_expanded = set()
63
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", 
89                
90                # this attributes are not used by w2k3
91                "schemaFlagsEx",
92                "msDs-IntId",
93                "msDs-Schema-Extensions",
94                "classDisplayName",
95                "isDefunct"]
96
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", 
118                 
119                 # this attributes are not used by w2k3
120                 "schemaFlagsEx",
121                 "msDs-IntId",
122                 "msDs-Schema-Extensions",
123                 "classDisplayName",
124                 "isEphemeral",
125                 "isDefunct"]
126
127 #
128 #  notes:
129 #
130 #  objectClassCategory 
131 #      1: structural
132 #      2: abstract
133 #      3: auxiliary
134
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"]
140
141
142 class Objectclass(dict):
143
144     def __init__(self, ldb, name):
145         """create an objectclass object"""
146         self.name = name
147         self["cn"] = get_object_cn(ldb, name)
148
149
150 class Attribute(dict):
151
152     def __init__(self, ldb, name):
153         """create an attribute object"""
154         self.name = name
155         self["cn"] = get_object_cn(ldb, name)
156
157
158 syntaxmap = dict()
159
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'
177
178
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
185
186
187 def fix_dn(dn):
188     """fix a string DN to use ${SCHEMADN}"""
189     return dn.replace(rootDse["schemaNamingContext"][0], "${SCHEMADN}")
190
191
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).decode('utf8'))
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 ""
209
210
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)
215
216
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)
223
224
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]
234
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]
245
246
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)
253
254     print "testdn is '%s'" % testdn
255
256     ldif = "dn: " + testdn
257     ldif += "\nobjectClass: " + o.name
258     try:
259         ldb.add(ldif)
260     except LdbError as e:
261         print "error adding %s: %s" % (o.name, e)
262         print "%s" % ldif
263         return
264
265     res = ldb.search(base=testdn, scope=ldb.SCOPE_BASE)
266     ldb.delete(testdn)
267
268     for a in res.msgs[0]:
269         attributes[a].autocreate = True
270
271
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",
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)
295
296
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)
311
312
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 as 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 as 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)
332
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 as 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)
348
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
360
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
370
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
382
383
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)
395
396
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
402
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))
408
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"
420
421     list = attribute_list(objectclass, "systemMustContain", "mustContain")
422     line += aggregate_list("MUST", list)
423
424     list = attribute_list(objectclass, "systemMayContain", "mayContain")
425     line += aggregate_list("MAY", list)
426
427     print line + " )"
428
429
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
435
436     line = "dITContentRules: ( %s NAME '%s'" % (objectclass["governsID"], objectclass.name)
437
438     line += aggregate_list("AUX", list)
439
440     may_list = []
441     must_list = []
442
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
450
451     line += aggregate_list("MUST", must_list)
452     line += aggregate_list("MAY", may_list)
453
454     print line + " )"
455
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 "
465
466     print line + ")"
467
468
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
477
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)
484
485 def load_list(file):
486     """load a list from a file"""
487     return [l.strip("\n") for l in open(file, 'r').readlines()]
488
489 # get the rootDSE
490 res = ldb.search(base="", expression="", scope=SCOPE_BASE, attrs=["schemaNamingContext"])
491 rootDse = res[0]
492
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
499
500
501 #
502 #  expand the objectclass list as needed
503 #
504 expanded = 0
505
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
509 #
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)
515
516 #
517 #  find objectclass properties
518 #
519 for name, objectclass in objectclasses.items():
520     find_objectclass_properties(ldb, objectclass)
521
522
523 #
524 #  form the full list of attributes
525 #
526 for name, objectclass in objectclasses.items():
527     add_objectclass_attributes(ldb, objectclass)
528
529 # and attribute properties
530 for name, attr in attributes.items():
531     find_attribute_properties(ldb, attr)
532
533 #
534 # trim the 'may' attribute lists to those really needed
535 #
536 for name, objectclass in objectclasses.items():
537     trim_objectclass_attributes(ldb, objectclass)
538
539 #
540 #  dump an ldif form of the attributes and objectclasses
541 #
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()
548
549 if not opts.verbose:
550     sys.exit(0)
551
552 #
553 #  dump list of objectclasses
554 #
555 print "objectClasses:\n"
556 for objectclass in objectclasses:
557     print "\t%s\n" % objectclass
558
559 print "attributes:\n"
560 for attr in attributes:
561     print "\t%s\n" % attr
562
563 print "autocreated attributes:\n"
564 for attr in attributes:
565     if attr.autocreate:
566         print "\t%s\n" % i