2011-06-10 17:17:12 +10:00
#!/usr/bin/env python
#
# Samba4 AD database checker
#
# Copyright (C) Andrew Tridgell 2011
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
2011-06-19 21:27:32 +04:00
import ldb , sys
2011-06-10 17:17:12 +10:00
import samba . getopt as options
2011-06-22 14:44:36 +10:00
from samba import dsdb
2011-06-22 17:08:28 +10:00
from samba import common
2011-06-10 17:17:12 +10:00
from samba . auth import system_session
from samba . samdb import SamDB
2011-06-22 14:44:36 +10:00
from samba . dcerpc import misc
2011-06-10 17:17:12 +10:00
from samba . netcmd import (
Command ,
CommandError ,
Option
)
2011-06-22 14:44:36 +10:00
class dsdb_DN ( object ) :
''' a class to manipulate DN components '''
def __init__ ( self , samdb , dnstring , syntax_oid ) :
if syntax_oid in [ dsdb . DSDB_SYNTAX_BINARY_DN , dsdb . DSDB_SYNTAX_STRING_DN ] :
colons = dnstring . split ( ' : ' )
if len ( colons ) < 4 :
raise Exception ( " invalid DN prefix " )
prefix_len = 4 + len ( colons [ 1 ] ) + int ( colons [ 1 ] )
self . prefix = dnstring [ 0 : prefix_len ]
self . dnstring = dnstring [ prefix_len : ]
else :
self . dnstring = dnstring
self . prefix = ' '
try :
self . dn = ldb . Dn ( samdb , self . dnstring )
except Exception , msg :
print ( " ERROR: bad DN string ' %s ' " % self . dnstring )
raise
def __str__ ( self ) :
return self . prefix + str ( self . dn . extended_str ( mode = 1 ) )
2011-06-10 17:17:12 +10:00
class cmd_dbcheck ( Command ) :
""" check local AD database for errors """
synopsis = " dbcheck <DN> [options] "
takes_optiongroups = {
" sambaopts " : options . SambaOptions ,
" versionopts " : options . VersionOptions ,
" credopts " : options . CredentialsOptionsDouble ,
}
takes_args = [ " DN? " ]
takes_options = [
Option ( " --scope " , dest = " scope " , default = " SUB " ,
help = " Pass search scope that builds DN list. Options: SUB, ONE, BASE " ) ,
Option ( " --fix " , dest = " fix " , default = False , action = ' store_true ' ,
help = ' Fix any errors found ' ) ,
Option ( " --yes " , dest = " yes " , default = False , action = ' store_true ' ,
2011-06-22 17:08:28 +10:00
help = " don ' t confirm changes, just do them all as a single transaction " ) ,
2011-06-17 11:31:25 +10:00
Option ( " --cross-ncs " , dest = " cross_ncs " , default = False , action = ' store_true ' ,
help = " cross naming context boundaries " ) ,
2011-06-10 17:17:12 +10:00
Option ( " -v " , " --verbose " , dest = " verbose " , action = " store_true " , default = False ,
help = " Print more details of checking " ) ,
2011-06-22 19:32:45 +10:00
Option ( " --quiet " , dest = " quiet " , action = " store_true " , default = False ,
help = " don ' t print details of checking " ) ,
2011-06-22 17:38:19 +10:00
Option ( " -H " , help = " LDB URL for database or target server (defaults to local SAM database) " , type = str ) ,
2011-06-10 17:17:12 +10:00
]
2011-06-22 19:32:45 +10:00
def run ( self , H = None , DN = None , verbose = False , fix = False , yes = False , cross_ncs = False , quiet = False ,
2011-06-10 17:17:12 +10:00
scope = " SUB " , credopts = None , sambaopts = None , versionopts = None ) :
self . lp = sambaopts . get_loadparm ( )
self . creds = credopts . get_credentials ( self . lp , fallback_machine = True )
2011-06-22 17:38:19 +10:00
self . samdb = SamDB ( session_info = system_session ( ) , url = H ,
2011-06-10 17:17:12 +10:00
credentials = self . creds , lp = self . lp )
2011-06-22 17:38:19 +10:00
if H is None :
self . local_samdb = self . samdb
else :
self . local_samdb = SamDB ( session_info = system_session ( ) , url = None ,
credentials = self . creds , lp = self . lp )
2011-06-10 17:17:12 +10:00
self . verbose = verbose
self . fix = fix
self . yes = yes
2011-06-22 19:32:45 +10:00
self . quiet = quiet
2011-06-10 17:17:12 +10:00
scope_map = { " SUB " : ldb . SCOPE_SUBTREE , " BASE " : ldb . SCOPE_BASE , " ONE " : ldb . SCOPE_ONELEVEL }
scope = scope . upper ( )
if not scope in scope_map :
raise CommandError ( " Unknown scope %s " % scope )
self . search_scope = scope_map [ scope ]
2011-06-17 11:31:25 +10:00
controls = [ ]
2011-06-22 17:38:19 +10:00
if H is not None :
controls . append ( ' paged_results:1:1000 ' )
2011-06-17 11:31:25 +10:00
if cross_ncs :
controls . append ( " search_options:1:2 " )
2011-06-22 17:08:28 +10:00
if self . yes and self . fix :
self . samdb . transaction_start ( )
2011-06-17 11:31:25 +10:00
res = self . samdb . search ( base = DN , scope = self . search_scope , attrs = [ ' dn ' ] , controls = controls )
2011-06-22 19:32:45 +10:00
self . report ( ' Checking %u objects ' % len ( res ) )
2011-06-17 11:34:19 +10:00
error_count = 0
2011-06-10 17:17:12 +10:00
for object in res :
2011-06-19 21:26:48 +04:00
error_count + = self . check_object ( object . dn )
2011-06-17 11:34:19 +10:00
if error_count != 0 and not self . fix :
2011-06-22 19:32:45 +10:00
self . report ( " Please use --fix to fix these errors " )
self . report ( ' Checked %u objects ( %u errors) ' % ( len ( res ) , error_count ) )
2011-06-22 17:08:28 +10:00
if self . yes and self . fix :
self . samdb . transaction_commit ( )
2011-06-17 14:40:48 +10:00
if error_count != 0 :
sys . exit ( 1 )
2011-06-19 20:43:11 +04:00
2011-06-22 12:23:05 +10:00
2011-06-22 19:32:45 +10:00
def report ( self , msg ) :
''' print a message unless quiet is set '''
if not self . quiet :
print ( msg )
2011-06-22 17:08:28 +10:00
################################################################
# a local confirm function that obeys the --fix and --yes options
def confirm ( self , msg ) :
''' confirm a change '''
if not self . fix :
return False
return common . confirm ( msg , forced = self . yes )
2011-06-22 12:23:05 +10:00
################################################################
# handle empty attributes
2011-06-22 14:44:36 +10:00
def err_empty_attribute ( self , dn , attrname ) :
2011-06-19 20:43:11 +04:00
''' fix empty attributes '''
2011-06-22 19:32:45 +10:00
self . report ( " ERROR: Empty attribute %s in %s " % ( attrname , dn ) )
2011-06-22 17:08:28 +10:00
if not self . confirm ( ' Remove empty attribute %s from %s ? ' % ( attrname , dn ) ) :
2011-06-22 19:32:45 +10:00
self . report ( " Not fixing empty attribute %s " % attrname )
2011-06-19 20:43:11 +04:00
return
m = ldb . Message ( )
m . dn = dn
m [ attrname ] = ldb . MessageElement ( ' ' , ldb . FLAG_MOD_DELETE , attrname )
2011-06-22 14:44:36 +10:00
if self . verbose :
2011-06-22 19:32:45 +10:00
self . report ( self . samdb . write_ldif ( m , ldb . CHANGETYPE_MODIFY ) )
2011-06-19 20:43:11 +04:00
try :
self . samdb . modify ( m , controls = [ " relax:0 " ] , validate = False )
except Exception , msg :
2011-06-22 19:32:45 +10:00
self . report ( " Failed to remove empty attribute %s : %s " % ( attrname , msg ) )
2011-06-19 20:43:11 +04:00
return
2011-06-22 19:32:45 +10:00
self . report ( " Removed empty attribute %s " % attrname )
2011-06-19 20:43:11 +04:00
2011-06-22 12:23:05 +10:00
################################################################
# handle normalisation mismatches
2011-06-22 14:44:36 +10:00
def err_normalise_mismatch ( self , dn , attrname , values ) :
2011-06-19 20:43:11 +04:00
''' fix attribute normalisation errors '''
2011-06-22 19:32:45 +10:00
self . report ( " ERROR: Normalisation error for attribute %s in %s " % ( attrname , dn ) )
2011-06-19 20:43:11 +04:00
mod_list = [ ]
for val in values :
2011-06-22 17:38:19 +10:00
normalised = self . samdb . dsdb_normalise_attributes ( self . local_samdb , attrname , [ val ] )
2011-06-19 20:43:11 +04:00
if len ( normalised ) != 1 :
2011-06-22 19:32:45 +10:00
self . report ( " Unable to normalise value ' %s ' " % val )
2011-06-19 20:43:11 +04:00
mod_list . append ( ( val , ' ' ) )
elif ( normalised [ 0 ] != val ) :
2011-06-22 19:32:45 +10:00
self . report ( " value ' %s ' should be ' %s ' " % ( val , normalised [ 0 ] ) )
2011-06-19 20:43:11 +04:00
mod_list . append ( ( val , normalised [ 0 ] ) )
2011-06-22 17:08:28 +10:00
if not self . confirm ( ' Fix normalisation for %s from %s ? ' % ( attrname , dn ) ) :
2011-06-22 19:32:45 +10:00
self . report ( " Not fixing attribute %s " % attrname )
2011-06-19 20:43:11 +04:00
return
m = ldb . Message ( )
m . dn = dn
for i in range ( 0 , len ( mod_list ) ) :
( val , nval ) = mod_list [ i ]
m [ ' value_ %u ' % i ] = ldb . MessageElement ( val , ldb . FLAG_MOD_DELETE , attrname )
if nval != ' ' :
m [ ' normv_ %u ' % i ] = ldb . MessageElement ( nval , ldb . FLAG_MOD_ADD , attrname )
2011-06-22 14:44:36 +10:00
if self . verbose :
2011-06-22 19:32:45 +10:00
self . report ( self . samdb . write_ldif ( m , ldb . CHANGETYPE_MODIFY ) )
2011-06-19 20:43:11 +04:00
try :
self . samdb . modify ( m , controls = [ " relax:0 " ] , validate = False )
except Exception , msg :
2011-06-22 19:32:45 +10:00
self . report ( " Failed to normalise attribute %s : %s " % ( attrname , msg ) )
2011-06-19 20:43:11 +04:00
return
2011-06-22 19:32:45 +10:00
self . report ( " Normalised attribute %s " % attrname )
2011-06-22 12:23:05 +10:00
2011-06-22 14:44:36 +10:00
################################################################
# handle a missing GUID extended DN component
2011-06-22 17:08:28 +10:00
def err_incorrect_dn_GUID ( self , dn , attrname , val , dsdb_dn , errstr ) :
2011-06-22 19:32:45 +10:00
self . report ( " ERROR: %s component for %s in object %s - %s " % ( errstr , attrname , dn , val ) )
2011-06-22 14:44:36 +10:00
try :
2011-06-22 17:08:28 +10:00
res = self . samdb . search ( base = dsdb_dn . dn , scope = ldb . SCOPE_BASE ,
attrs = [ ] , controls = [ " extended_dn:1:1 " ] )
except ldb . LdbError , ( enum , estr ) :
2011-06-22 19:32:45 +10:00
self . report ( " unable to find object for DN %s - cannot fix ( %s ) " % ( dsdb_dn . dn , estr ) )
2011-06-22 14:44:36 +10:00
return
2011-06-22 17:08:28 +10:00
dsdb_dn . dn = res [ 0 ] . dn
2011-06-22 14:44:36 +10:00
2011-06-22 17:08:28 +10:00
if not self . confirm ( ' Change DN to %s ? ' % str ( dsdb_dn ) ) :
2011-06-22 19:32:45 +10:00
self . report ( " Not fixing %s " % errstr )
2011-06-22 14:44:36 +10:00
return
m = ldb . Message ( )
m . dn = dn
m [ ' old_value ' ] = ldb . MessageElement ( val , ldb . FLAG_MOD_DELETE , attrname )
m [ ' new_value ' ] = ldb . MessageElement ( str ( dsdb_dn ) , ldb . FLAG_MOD_ADD , attrname )
if self . verbose :
2011-06-22 19:32:45 +10:00
self . report ( self . samdb . write_ldif ( m , ldb . CHANGETYPE_MODIFY ) )
2011-06-22 14:44:36 +10:00
try :
self . samdb . modify ( m )
except Exception , msg :
2011-06-22 19:32:45 +10:00
self . report ( " Failed to fix %s on attribute %s : %s " % ( errstr , attrname , msg ) )
2011-06-22 17:08:28 +10:00
return
2011-06-22 19:32:45 +10:00
self . report ( " Fixed %s on attribute %s " % ( errstr , attrname ) )
2011-06-22 17:08:28 +10:00
################################################################
# handle a DN pointing to a deleted object
def err_deleted_dn ( self , dn , attrname , val , dsdb_dn , correct_dn ) :
2011-06-22 19:32:45 +10:00
self . report ( " ERROR: target DN is deleted for %s in object %s - %s " % ( attrname , dn , val ) )
self . report ( " Target GUID points at deleted DN %s " % correct_dn )
2011-06-22 17:08:28 +10:00
if not self . confirm ( ' Remove DN? ' ) :
2011-06-22 19:32:45 +10:00
self . report ( " Not removing " )
2011-06-22 17:08:28 +10:00
return
m = ldb . Message ( )
m . dn = dn
m [ ' old_value ' ] = ldb . MessageElement ( val , ldb . FLAG_MOD_DELETE , attrname )
if self . verbose :
2011-06-22 19:32:45 +10:00
self . report ( self . samdb . write_ldif ( m , ldb . CHANGETYPE_MODIFY ) )
2011-06-22 17:08:28 +10:00
try :
self . samdb . modify ( m )
except Exception , msg :
2011-06-22 19:32:45 +10:00
self . report ( " Failed to remove deleted DN attribute %s : %s " % ( attrname , msg ) )
2011-06-22 14:44:36 +10:00
return
2011-06-22 19:32:45 +10:00
self . report ( " Removed deleted DN on attribute %s " % attrname )
2011-06-22 14:44:36 +10:00
2011-06-22 17:08:28 +10:00
################################################################
# handle a DN string being incorrect
def err_dn_target_mismatch ( self , dn , attrname , val , dsdb_dn , correct_dn ) :
2011-06-22 19:32:45 +10:00
self . report ( " ERROR: incorrect DN string component for %s in object %s - %s " % ( attrname , dn , val ) )
2011-06-22 17:08:28 +10:00
dsdb_dn . dn = correct_dn
if not self . confirm ( ' Change DN to %s ? ' % str ( dsdb_dn ) ) :
2011-06-22 19:32:45 +10:00
self . report ( " Not fixing %s " % errstr )
2011-06-22 17:08:28 +10:00
return
m = ldb . Message ( )
m . dn = dn
m [ ' old_value ' ] = ldb . MessageElement ( val , ldb . FLAG_MOD_DELETE , attrname )
m [ ' new_value ' ] = ldb . MessageElement ( str ( dsdb_dn ) , ldb . FLAG_MOD_ADD , attrname )
if self . verbose :
2011-06-22 19:32:45 +10:00
self . report ( self . samdb . write_ldif ( m , ldb . CHANGETYPE_MODIFY ) )
2011-06-22 17:08:28 +10:00
try :
self . samdb . modify ( m )
except Exception , msg :
2011-06-22 19:32:45 +10:00
self . report ( " Failed to fix incorrect DN string on attribute %s : %s " % ( attrname , msg ) )
2011-06-22 17:08:28 +10:00
return
2011-06-22 19:32:45 +10:00
self . report ( " Fixed incorrect DN string on attribute %s " % ( attrname ) )
2011-06-22 17:08:28 +10:00
2011-06-22 14:44:36 +10:00
################################################################
# specialised checking for a dn attribute
def check_dn ( self , obj , attrname , syntax_oid ) :
''' check a DN attribute for correctness '''
error_count = 0
for val in obj [ attrname ] :
dsdb_dn = dsdb_DN ( self . samdb , val , syntax_oid )
# all DNs should have a GUID component
guid = dsdb_dn . dn . get_extended_component ( " GUID " )
if guid is None :
error_count + = 1
2011-06-22 17:08:28 +10:00
self . err_incorrect_dn_GUID ( obj . dn , attrname , val , dsdb_dn , " missing GUID " )
continue
guidstr = str ( misc . GUID ( guid ) )
# check its the right GUID
try :
res = self . samdb . search ( base = " <GUID= %s > " % guidstr , scope = ldb . SCOPE_BASE ,
attrs = [ ' isDeleted ' ] , controls = [ " extended_dn:1:1 " , " show_deleted:1 " ] )
except ldb . LdbError , ( enum , estr ) :
error_count + = 1
self . err_incorrect_dn_GUID ( obj . dn , attrname , val , dsdb_dn , " incorrect GUID " )
continue
# the target DN might be deleted
if ( dsdb_dn . prefix != " B:32:18E2EA80684F11D2B9AA00C04F79F805: " and
' isDeleted ' in res [ 0 ] and
res [ 0 ] [ ' isDeleted ' ] [ 0 ] . upper ( ) == " TRUE " ) :
# note that we don't check this for the special wellKnownObjects prefix
# for Deleted Objects, as we expect that to be deleted
error_count + = 1
self . err_deleted_dn ( obj . dn , attrname , val , dsdb_dn , res [ 0 ] . dn )
continue
# check the DN matches in string form
if res [ 0 ] . dn . extended_str ( ) != dsdb_dn . dn . extended_str ( ) :
error_count + = 1
self . err_dn_target_mismatch ( obj . dn , attrname , val , dsdb_dn , res [ 0 ] . dn )
continue
2011-06-22 14:44:36 +10:00
2011-06-22 17:08:28 +10:00
return error_count
2011-06-22 14:44:36 +10:00
2011-06-22 12:23:05 +10:00
################################################################
# check one object - calls to individual error handlers above
def check_object ( self , dn ) :
''' check one object '''
if self . verbose :
2011-06-22 19:32:45 +10:00
self . report ( " Checking object %s " % dn )
2011-06-22 12:23:05 +10:00
res = self . samdb . search ( base = dn , scope = ldb . SCOPE_BASE , controls = [ " extended_dn:1:1 " ] , attrs = [ ' * ' , ' ntSecurityDescriptor ' ] )
if len ( res ) != 1 :
2011-06-22 19:32:45 +10:00
self . report ( " Object %s disappeared during check " % dn )
2011-06-22 12:23:05 +10:00
return 1
obj = res [ 0 ]
error_count = 0
for attrname in obj :
if attrname == ' dn ' :
continue
# check for empty attributes
for val in obj [ attrname ] :
if val == ' ' :
2011-06-22 14:44:36 +10:00
self . err_empty_attribute ( dn , attrname )
2011-06-22 12:23:05 +10:00
error_count + = 1
continue
2011-06-22 14:44:36 +10:00
# get the syntax oid for the attribute, so we can can have
# special handling for some specific attribute types
2011-06-22 17:38:19 +10:00
syntax_oid = self . local_samdb . get_syntax_oid_from_lDAPDisplayName ( attrname )
2011-06-22 14:44:36 +10:00
if syntax_oid in [ dsdb . DSDB_SYNTAX_BINARY_DN , dsdb . DSDB_SYNTAX_OR_NAME ,
dsdb . DSDB_SYNTAX_STRING_DN , ldb . LDB_SYNTAX_DN ] :
# it's some form of DN, do specialised checking on those
error_count + = self . check_dn ( obj , attrname , syntax_oid )
2011-06-22 12:23:05 +10:00
# check for incorrectly normalised attributes
for val in obj [ attrname ] :
2011-06-22 17:38:19 +10:00
normalised = self . samdb . dsdb_normalise_attributes ( self . local_samdb , attrname , [ val ] )
2011-06-22 12:23:05 +10:00
if len ( normalised ) != 1 or normalised [ 0 ] != val :
2011-06-22 14:44:36 +10:00
self . err_normalise_mismatch ( dn , attrname , obj [ attrname ] )
2011-06-22 12:23:05 +10:00
error_count + = 1
break
return error_count