samba-tool: added attribute normalisation checks
[kai/samba.git] / source4 / scripting / python / samba / netcmd / dbcheck.py
1 #!/usr/bin/env python
2 #
3 # Samba4 AD database checker
4 #
5 # Copyright (C) Andrew Tridgell 2011
6 #
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.
11 #
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.
16 #
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/>.
19 #
20
21 import samba, ldb
22 import samba.getopt as options
23 from samba.auth import system_session
24 from samba.samdb import SamDB
25 from samba.dcerpc import security
26 from samba.netcmd import (
27     Command,
28     CommandError,
29     Option
30     )
31
32 def confirm(self, msg):
33     '''confirm an action with the user'''
34     if self.yes:
35         print("%s [YES]" % msg)
36         return True
37     v = raw_input(msg + ' [y/N] ')
38     return v.upper() in ['Y', 'YES']
39
40
41 def empty_attribute(self, dn, attrname):
42     '''fix empty attributes'''
43     print("ERROR: Empty attribute %s in %s" % (attrname, dn))
44     if not self.fix:
45         return
46     if not confirm(self, 'Remove empty attribute %s from %s?' % (attrname, dn)):
47         print("Not fixing empty attribute %s" % attrname)
48         return
49
50     m = ldb.Message()
51     m.dn = dn
52     m[attrname] = ldb.MessageElement('', ldb.FLAG_MOD_DELETE, attrname)
53     try:
54         self.samdb.modify(m, controls=["relax:0"], validate=False)
55     except Exception, msg:
56         print("Failed to remove empty attribute %s : %s" % (attrname, msg))
57         return
58     print("Removed empty attribute %s" % attrname)
59
60
61 def normalise_mismatch(self, dn, attrname, values):
62     '''fix attribute normalisation errors'''
63     print("ERROR: Normalisation error for attribute %s in %s" % (attrname, dn))
64     mod_list = []
65     for val in values:
66         normalised = self.samdb.dsdb_normalise_attributes(self.samdb, attrname, [val])
67         if len(normalised) != 1:
68             print("Unable to normalise value '%s'" % val)
69             mod_list.append((val, ''))
70         elif (normalised[0] != val):
71             print("value '%s' should be '%s'" % (val, normalised[0]))
72             mod_list.append((val, normalised[0]))
73     if not self.fix:
74         return
75     if not confirm(self, 'Fix normalisation for %s from %s?' % (attrname, dn)):
76         print("Not fixing attribute %s" % attrname)
77         return
78
79     m = ldb.Message()
80     m.dn = dn
81     for i in range(0, len(mod_list)):
82         (val, nval) = mod_list[i]
83         m['value_%u' % i] = ldb.MessageElement(val, ldb.FLAG_MOD_DELETE, attrname)
84         if nval != '':
85             m['normv_%u' % i] = ldb.MessageElement(nval, ldb.FLAG_MOD_ADD, attrname)
86
87     try:
88         self.samdb.modify(m, controls=["relax:0"], validate=False)
89     except Exception, msg:
90         print("Failed to normalise attribute %s : %s" % (attrname, msg))
91         return
92     print("Normalised attribute %s" % attrname)
93
94
95
96 def check_object(self, dn):
97     '''check one object'''
98     if self.verbose:
99         print("Checking object %s" % dn)
100     res = self.samdb.search(base=dn, scope=ldb.SCOPE_BASE, controls=["extended_dn:1:1"], attrs=['*', 'ntSecurityDescriptor'])
101     if len(res) != 1:
102         print("Object %s disappeared during check" % dn)
103         return
104     obj = res[0]
105     for attrname in obj:
106         if attrname == 'dn':
107             continue
108
109         # check for empty attributes
110         for val in obj[attrname]:
111             if val == '':
112                 empty_attribute(self, dn, attrname)
113                 continue
114
115         # check for incorrectly normalised attributes
116         for val in obj[attrname]:
117             normalised = self.samdb.dsdb_normalise_attributes(self.samdb, attrname, [val])
118             if len(normalised) != 1 or normalised[0] != val:
119                 normalise_mismatch(self, dn, attrname, obj[attrname])
120                 break
121
122
123 class cmd_dbcheck(Command):
124     """check local AD database for errors"""
125     synopsis = "dbcheck <DN> [options]"
126
127     takes_optiongroups = {
128         "sambaopts": options.SambaOptions,
129         "versionopts": options.VersionOptions,
130         "credopts": options.CredentialsOptionsDouble,
131     }
132
133     takes_args = ["DN?"]
134
135     takes_options = [
136         Option("--scope", dest="scope", default="SUB",
137             help="Pass search scope that builds DN list. Options: SUB, ONE, BASE"),
138         Option("--fix", dest="fix", default=False, action='store_true',
139                help='Fix any errors found'),
140         Option("--yes", dest="yes", default=False, action='store_true',
141                help="don't confirm changes, just do them all"),
142         Option("--cross-ncs", dest="cross_ncs", default=False, action='store_true',
143                help="cross naming context boundaries"),
144         Option("-v", "--verbose", dest="verbose", action="store_true", default=False,
145             help="Print more details of checking"),
146         ]
147
148     def run(self, DN=None, verbose=False, fix=False, yes=False, cross_ncs=False,
149             scope="SUB", credopts=None, sambaopts=None, versionopts=None):
150         self.lp = sambaopts.get_loadparm()
151         self.creds = credopts.get_credentials(self.lp, fallback_machine=True)
152
153         self.samdb = SamDB(session_info=system_session(), url=None,
154                            credentials=self.creds, lp=self.lp)
155         self.verbose = verbose
156         self.fix = fix
157         self.yes = yes
158
159         scope_map = { "SUB": ldb.SCOPE_SUBTREE, "BASE":ldb.SCOPE_BASE, "ONE":ldb.SCOPE_ONELEVEL }
160         scope = scope.upper()
161         if not scope in scope_map:
162             raise CommandError("Unknown scope %s" % scope)
163         self.search_scope = scope_map[scope]
164
165         controls = []
166         if cross_ncs:
167             controls.append("search_options:1:2")
168
169         res = self.samdb.search(base=DN, scope=self.search_scope, attrs=['dn'], controls=controls)
170         print('Checking %u objects' % len(res))
171         for object in res:
172             check_object(self, object.dn)
173         print('Checked %u objects' % len(res))