3 # Samba4 AD database checker
5 # Copyright (C) Andrew Tridgell 2011
7 # This program is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
22 from samba
import dsdb
23 from samba
import common
24 from samba
.dcerpc
import misc
27 class dsdb_DN(object):
28 '''a class to manipulate DN components'''
30 def __init__(self
, samdb
, dnstring
, syntax_oid
):
31 if syntax_oid
in [ dsdb
.DSDB_SYNTAX_BINARY_DN
, dsdb
.DSDB_SYNTAX_STRING_DN
]:
32 colons
= dnstring
.split(':')
34 raise Exception("invalid DN prefix")
35 prefix_len
= 4 + len(colons
[1]) + int(colons
[1])
36 self
.prefix
= dnstring
[0:prefix_len
]
37 self
.dnstring
= dnstring
[prefix_len
:]
39 self
.dnstring
= dnstring
42 self
.dn
= ldb
.Dn(samdb
, self
.dnstring
)
43 except Exception, msg
:
44 print("ERROR: bad DN string '%s'" % self
.dnstring
)
48 return self
.prefix
+ str(self
.dn
.extended_str(mode
=1))
50 class dbcheck(object):
51 """check a SAM database for errors"""
53 def __init__(self
, samdb
, samdb_schema
=None, verbose
=False, fix
=False, yes
=False, quiet
=False):
55 self
.samdb_schema
= (samdb_schema
or samdb
)
56 self
.verbose
= verbose
61 def check_database(self
, DN
=None, scope
=ldb
.SCOPE_SUBTREE
, controls
=[], attrs
=['*']):
62 '''perform a database check, returning the number of errors found'''
64 res
= self
.samdb
.search(base
=DN
, scope
=scope
, attrs
=['dn'], controls
=controls
)
65 self
.report('Checking %u objects' % len(res
))
68 error_count
+= self
.check_object(object.dn
, attrs
=attrs
)
69 if error_count
!= 0 and not self
.fix
:
70 self
.report("Please use --fix to fix these errors")
71 self
.report('Checked %u objects (%u errors)' % (len(res
), error_count
))
76 def report(self
, msg
):
77 '''print a message unless quiet is set'''
82 ################################################################
83 # a local confirm function that obeys the --fix and --yes options
84 def confirm(self
, msg
):
85 '''confirm a change'''
90 return common
.confirm(msg
, forced
=self
.yes
)
93 ################################################################
94 # handle empty attributes
95 def err_empty_attribute(self
, dn
, attrname
):
96 '''fix empty attributes'''
97 self
.report("ERROR: Empty attribute %s in %s" % (attrname
, dn
))
98 if not self
.confirm('Remove empty attribute %s from %s?' % (attrname
, dn
)):
99 self
.report("Not fixing empty attribute %s" % attrname
)
104 m
[attrname
] = ldb
.MessageElement('', ldb
.FLAG_MOD_DELETE
, attrname
)
106 self
.report(self
.samdb
.write_ldif(m
, ldb
.CHANGETYPE_MODIFY
))
108 self
.samdb
.modify(m
, controls
=["relax:0"], validate
=False)
109 except Exception, msg
:
110 self
.report("Failed to remove empty attribute %s : %s" % (attrname
, msg
))
112 self
.report("Removed empty attribute %s" % attrname
)
115 ################################################################
116 # handle normalisation mismatches
117 def err_normalise_mismatch(self
, dn
, attrname
, values
):
118 '''fix attribute normalisation errors'''
119 self
.report("ERROR: Normalisation error for attribute %s in %s" % (attrname
, dn
))
122 normalised
= self
.samdb
.dsdb_normalise_attributes(self
.samdb_schema
, attrname
, [val
])
123 if len(normalised
) != 1:
124 self
.report("Unable to normalise value '%s'" % val
)
125 mod_list
.append((val
, ''))
126 elif (normalised
[0] != val
):
127 self
.report("value '%s' should be '%s'" % (val
, normalised
[0]))
128 mod_list
.append((val
, normalised
[0]))
129 if not self
.confirm('Fix normalisation for %s from %s?' % (attrname
, dn
)):
130 self
.report("Not fixing attribute %s" % attrname
)
135 for i
in range(0, len(mod_list
)):
136 (val
, nval
) = mod_list
[i
]
137 m
['value_%u' % i
] = ldb
.MessageElement(val
, ldb
.FLAG_MOD_DELETE
, attrname
)
139 m
['normv_%u' % i
] = ldb
.MessageElement(nval
, ldb
.FLAG_MOD_ADD
, attrname
)
142 self
.report(self
.samdb
.write_ldif(m
, ldb
.CHANGETYPE_MODIFY
))
144 self
.samdb
.modify(m
, controls
=["relax:0"], validate
=False)
145 except Exception, msg
:
146 self
.report("Failed to normalise attribute %s : %s" % (attrname
, msg
))
148 self
.report("Normalised attribute %s" % attrname
)
150 def is_deleted_objects_dn(self
, dsdb_dn
):
151 '''see if a dsdb_DN is the special Deleted Objects DN'''
152 return dsdb_dn
.prefix
== "B:32:18E2EA80684F11D2B9AA00C04F79F805:"
155 ################################################################
156 # handle a missing GUID extended DN component
157 def err_incorrect_dn_GUID(self
, dn
, attrname
, val
, dsdb_dn
, errstr
):
158 self
.report("ERROR: %s component for %s in object %s - %s" % (errstr
, attrname
, dn
, val
))
159 controls
=["extended_dn:1:1"]
160 if self
.is_deleted_objects_dn(dsdb_dn
):
161 controls
.append("show_deleted:1")
163 res
= self
.samdb
.search(base
=str(dsdb_dn
.dn
), scope
=ldb
.SCOPE_BASE
,
164 attrs
=[], controls
=controls
)
165 except ldb
.LdbError
, (enum
, estr
):
166 self
.report("unable to find object for DN %s - cannot fix (%s)" % (dsdb_dn
.dn
, estr
))
168 dsdb_dn
.dn
= res
[0].dn
170 if not self
.confirm('Change DN to %s?' % str(dsdb_dn
)):
171 self
.report("Not fixing %s" % errstr
)
175 m
['old_value'] = ldb
.MessageElement(val
, ldb
.FLAG_MOD_DELETE
, attrname
)
176 m
['new_value'] = ldb
.MessageElement(str(dsdb_dn
), ldb
.FLAG_MOD_ADD
, attrname
)
178 self
.report(self
.samdb
.write_ldif(m
, ldb
.CHANGETYPE_MODIFY
))
181 except Exception, msg
:
182 self
.report("Failed to fix %s on attribute %s : %s" % (errstr
, attrname
, msg
))
184 self
.report("Fixed %s on attribute %s" % (errstr
, attrname
))
187 ################################################################
188 # handle a DN pointing to a deleted object
189 def err_deleted_dn(self
, dn
, attrname
, val
, dsdb_dn
, correct_dn
):
190 self
.report("ERROR: target DN is deleted for %s in object %s - %s" % (attrname
, dn
, val
))
191 self
.report("Target GUID points at deleted DN %s" % correct_dn
)
192 if not self
.confirm('Remove DN?'):
193 self
.report("Not removing")
197 m
['old_value'] = ldb
.MessageElement(val
, ldb
.FLAG_MOD_DELETE
, attrname
)
199 self
.report(self
.samdb
.write_ldif(m
, ldb
.CHANGETYPE_MODIFY
))
202 except Exception, msg
:
203 self
.report("Failed to remove deleted DN attribute %s : %s" % (attrname
, msg
))
205 self
.report("Removed deleted DN on attribute %s" % attrname
)
208 ################################################################
209 # handle a DN string being incorrect
210 def err_dn_target_mismatch(self
, dn
, attrname
, val
, dsdb_dn
, correct_dn
):
211 self
.report("ERROR: incorrect DN string component for %s in object %s - %s" % (attrname
, dn
, val
))
212 dsdb_dn
.dn
= correct_dn
214 if not self
.confirm('Change DN to %s?' % str(dsdb_dn
)):
215 self
.report("Not fixing %s" % errstr
)
219 m
['old_value'] = ldb
.MessageElement(val
, ldb
.FLAG_MOD_DELETE
, attrname
)
220 m
['new_value'] = ldb
.MessageElement(str(dsdb_dn
), ldb
.FLAG_MOD_ADD
, attrname
)
222 self
.report(self
.samdb
.write_ldif(m
, ldb
.CHANGETYPE_MODIFY
))
225 except Exception, msg
:
226 self
.report("Failed to fix incorrect DN string on attribute %s : %s" % (attrname
, msg
))
228 self
.report("Fixed incorrect DN string on attribute %s" % (attrname
))
231 ################################################################
232 # specialised checking for a dn attribute
233 def check_dn(self
, obj
, attrname
, syntax_oid
):
234 '''check a DN attribute for correctness'''
236 for val
in obj
[attrname
]:
237 dsdb_dn
= dsdb_DN(self
.samdb
, val
, syntax_oid
)
239 # all DNs should have a GUID component
240 guid
= dsdb_dn
.dn
.get_extended_component("GUID")
243 self
.err_incorrect_dn_GUID(obj
.dn
, attrname
, val
, dsdb_dn
, "missing GUID")
246 guidstr
= str(misc
.GUID(guid
))
248 # check its the right GUID
250 res
= self
.samdb
.search(base
="<GUID=%s>" % guidstr
, scope
=ldb
.SCOPE_BASE
,
251 attrs
=['isDeleted'], controls
=["extended_dn:1:1", "show_deleted:1"])
252 except ldb
.LdbError
, (enum
, estr
):
254 self
.err_incorrect_dn_GUID(obj
.dn
, attrname
, val
, dsdb_dn
, "incorrect GUID")
257 # the target DN might be deleted
258 if ((not self
.is_deleted_objects_dn(dsdb_dn
)) and
259 'isDeleted' in res
[0] and
260 res
[0]['isDeleted'][0].upper() == "TRUE"):
261 # note that we don't check this for the special wellKnownObjects prefix
262 # for Deleted Objects, as we expect that to be deleted
264 self
.err_deleted_dn(obj
.dn
, attrname
, val
, dsdb_dn
, res
[0].dn
)
267 # check the DN matches in string form
268 if res
[0].dn
.extended_str() != dsdb_dn
.dn
.extended_str():
270 self
.err_dn_target_mismatch(obj
.dn
, attrname
, val
, dsdb_dn
, res
[0].dn
)
277 ################################################################
278 # check one object - calls to individual error handlers above
279 def check_object(self
, dn
, attrs
=['*']):
280 '''check one object'''
282 self
.report("Checking object %s" % dn
)
283 res
= self
.samdb
.search(base
=dn
, scope
=ldb
.SCOPE_BASE
, controls
=["extended_dn:1:1"], attrs
=attrs
)
285 self
.report("Object %s disappeared during check" % dn
)
293 # check for empty attributes
294 for val
in obj
[attrname
]:
296 self
.err_empty_attribute(dn
, attrname
)
300 # get the syntax oid for the attribute, so we can can have
301 # special handling for some specific attribute types
302 syntax_oid
= self
.samdb_schema
.get_syntax_oid_from_lDAPDisplayName(attrname
)
304 if syntax_oid
in [ dsdb
.DSDB_SYNTAX_BINARY_DN
, dsdb
.DSDB_SYNTAX_OR_NAME
,
305 dsdb
.DSDB_SYNTAX_STRING_DN
, ldb
.LDB_SYNTAX_DN
]:
306 # it's some form of DN, do specialised checking on those
307 error_count
+= self
.check_dn(obj
, attrname
, syntax_oid
)
309 # check for incorrectly normalised attributes
310 for val
in obj
[attrname
]:
311 normalised
= self
.samdb
.dsdb_normalise_attributes(self
.samdb_schema
, attrname
, [val
])
312 if len(normalised
) != 1 or normalised
[0] != val
:
313 self
.err_normalise_mismatch(dn
, attrname
, obj
[attrname
])