summaryrefslogtreecommitdiff
path: root/source4/scripting/python/samba/netcmd/dbcheck.py
blob: 4ec1365f1402824a9d551e2da52c89ff8178ad48 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
#!/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/>.
#

import ldb, sys
import samba.getopt as options
from samba.common import confirm
from samba.auth import system_session
from samba.samdb import SamDB
from samba.netcmd import (
    Command,
    CommandError,
    Option
    )

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',
               help="don't confirm changes, just do them all"),
        Option("--cross-ncs", dest="cross_ncs", default=False, action='store_true',
               help="cross naming context boundaries"),
        Option("-v", "--verbose", dest="verbose", action="store_true", default=False,
            help="Print more details of checking"),
        ]

    def run(self, DN=None, verbose=False, fix=False, yes=False, cross_ncs=False,
            scope="SUB", credopts=None, sambaopts=None, versionopts=None):
        self.lp = sambaopts.get_loadparm()
        self.creds = credopts.get_credentials(self.lp, fallback_machine=True)

        self.samdb = SamDB(session_info=system_session(), url=None,
                           credentials=self.creds, lp=self.lp)
        self.verbose = verbose
        self.fix = fix
        self.yes = yes

        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]

        controls = []
        if cross_ncs:
            controls.append("search_options:1:2")

        res = self.samdb.search(base=DN, scope=self.search_scope, attrs=['dn'], controls=controls)
        print('Checking %u objects' % len(res))
        error_count = 0
        for object in res:
            error_count += self.check_object(object.dn)
        if error_count != 0 and not self.fix:
            print("Please use --fix to fix these errors")
        print('Checked %u objects (%u errors)' % (len(res), error_count))
        if error_count != 0:
            sys.exit(1)


    ################################################################
    # handle empty attributes
    def empty_attribute(self, dn, attrname):
        '''fix empty attributes'''
        print("ERROR: Empty attribute %s in %s" % (attrname, dn))
        if not self.fix:
            return
        if not confirm('Remove empty attribute %s from %s?' % (attrname, dn), self.yes):
            print("Not fixing empty attribute %s" % attrname)
            return

        m = ldb.Message()
        m.dn = dn
        m[attrname] = ldb.MessageElement('', ldb.FLAG_MOD_DELETE, attrname)
        try:
            self.samdb.modify(m, controls=["relax:0"], validate=False)
        except Exception, msg:
            print("Failed to remove empty attribute %s : %s" % (attrname, msg))
            return
        print("Removed empty attribute %s" % attrname)


    ################################################################
    # handle normalisation mismatches
    def normalise_mismatch(self, dn, attrname, values):
        '''fix attribute normalisation errors'''
        print("ERROR: Normalisation error for attribute %s in %s" % (attrname, dn))
        mod_list = []
        for val in values:
            normalised = self.samdb.dsdb_normalise_attributes(self.samdb, attrname, [val])
            if len(normalised) != 1:
                print("Unable to normalise value '%s'" % val)
                mod_list.append((val, ''))
            elif (normalised[0] != val):
                print("value '%s' should be '%s'" % (val, normalised[0]))
                mod_list.append((val, normalised[0]))
        if not self.fix:
            return
        if not confirm('Fix normalisation for %s from %s?' % (attrname, dn), self.yes):
            print("Not fixing attribute %s" % attrname)
            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)

        try:
            self.samdb.modify(m, controls=["relax:0"], validate=False)
        except Exception, msg:
            print("Failed to normalise attribute %s : %s" % (attrname, msg))
            return
        print("Normalised attribute %s" % attrname)


    ################################################################
    # check one object - calls to individual error handlers above
    def check_object(self, dn):
        '''check one object'''
        if self.verbose:
            print("Checking object %s" % dn)
        res = self.samdb.search(base=dn, scope=ldb.SCOPE_BASE, controls=["extended_dn:1:1"], attrs=['*', 'ntSecurityDescriptor'])
        if len(res) != 1:
            print("Object %s disappeared during check" % dn)
            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 == '':
                    self.empty_attribute(dn, attrname)
                    error_count += 1
                    continue

            # check for incorrectly normalised attributes
            for val in obj[attrname]:
                normalised = self.samdb.dsdb_normalise_attributes(self.samdb, attrname, [val])
                if len(normalised) != 1 or normalised[0] != val:
                    self.normalise_mismatch(dn, attrname, obj[attrname])
                    error_count += 1
                    break
        return error_count